docs: 删除多个文档,精简项目文档结构,提升可维护性
This commit is contained in:
parent
85ddc6ac8b
commit
d0eb178f87
@ -1,440 +0,0 @@
|
||||
# Canvas 2D 绘制技术详解
|
||||
|
||||
## 📖 概述
|
||||
|
||||
本文档详细分析场景编辑器中的自定义绘制函数,这些函数基于 **HTML5 Canvas 2D API** 在浏览器页面上绘制各种图形元素(点位、路线、区域、机器人)。
|
||||
|
||||
## 🎯 核心技术栈
|
||||
|
||||
### 1. HTML5 Canvas 2D API
|
||||
|
||||
- **技术原理**:Canvas 是 HTML5 提供的位图绘制 API
|
||||
- **绘制方式**:使用 JavaScript 在 Canvas 画布上逐像素绘制
|
||||
- **坐标系统**:左上角为原点 (0,0),X轴向右,Y轴向下
|
||||
- **绘制上下文**:通过 `CanvasRenderingContext2D` 对象进行所有绘制操作
|
||||
|
||||
### 2. Meta2D 引擎集成
|
||||
|
||||
- **自定义绘制**:通过 `registerCanvasDraw()` 注册自定义绘制函数
|
||||
- **图形对象**:每个绘制函数接收 `MapPen` 对象,包含图形的所有属性
|
||||
- **渲染时机**:引擎在每次重绘时自动调用对应的绘制函数
|
||||
|
||||
---
|
||||
|
||||
## 🎨 绘制函数详细分析
|
||||
|
||||
### 1. 点位绘制函数 `drawPoint()`
|
||||
|
||||
#### 函数签名和参数
|
||||
|
||||
```typescript
|
||||
function drawPoint(ctx: CanvasRenderingContext2D, pen: MapPen): void;
|
||||
```
|
||||
|
||||
#### 代码逐行分析
|
||||
|
||||
```typescript
|
||||
// 1. 获取全局主题配置
|
||||
const theme = sTheme.editor;
|
||||
```
|
||||
|
||||
**分析**:从全局主题服务获取编辑器主题配置,用于确定颜色、样式等视觉属性。
|
||||
|
||||
```typescript
|
||||
// 2. 从计算属性中提取绘制参数
|
||||
const { active, iconSize: r = 0, fontSize = 14, lineHeight = 1.5, fontFamily } = pen.calculative ?? {};
|
||||
```
|
||||
|
||||
**分析**:
|
||||
|
||||
- `active`:图形是否处于选中状态
|
||||
- `iconSize`:图标大小,重命名为 `r`(半径)
|
||||
- `fontSize/lineHeight/fontFamily`:文本绘制参数
|
||||
|
||||
```typescript
|
||||
// 3. 获取世界坐标系下的矩形区域
|
||||
const { x = 0, y = 0, width: w = 0, height: h = 0 } = pen.calculative?.worldRect ?? {};
|
||||
```
|
||||
|
||||
**分析**:Meta2D 引擎会自动计算图形在世界坐标系下的实际位置和大小。
|
||||
|
||||
```typescript
|
||||
// 4. 获取业务属性
|
||||
const { type } = pen.point ?? {};
|
||||
const { label = '' } = pen ?? {};
|
||||
```
|
||||
|
||||
```typescript
|
||||
// 5. 保存当前画布状态
|
||||
ctx.save();
|
||||
```
|
||||
|
||||
**分析**:`save()` 保存当前的绘制状态(变换矩阵、样式等),避免影响其他图形。
|
||||
|
||||
#### 小点位绘制(类型1-9)
|
||||
|
||||
```typescript
|
||||
switch (type) {
|
||||
case MapPointType.普通点:
|
||||
case MapPointType.等待点:
|
||||
case MapPointType.避让点:
|
||||
case MapPointType.临时避让点:
|
||||
// 绘制圆角菱形
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + w / 2 - r, y + r);
|
||||
ctx.arcTo(x + w / 2, y, x + w - r, y + h / 2 - r, r);
|
||||
ctx.arcTo(x + w, y + h / 2, x + w / 2 + r, y + h - r, r);
|
||||
ctx.arcTo(x + w / 2, y + h, x + r, y + h / 2 + r, r);
|
||||
ctx.arcTo(x, y + h / 2, x + r, y + h / 2 - r, r);
|
||||
ctx.closePath();
|
||||
```
|
||||
|
||||
**分析**:
|
||||
|
||||
- `beginPath()`:开始新的绘制路径
|
||||
- `moveTo()`:移动画笔到起始点
|
||||
- `arcTo()`:绘制圆弧连接线,创建圆角效果
|
||||
- `closePath()`:闭合路径形成完整图形
|
||||
|
||||
```typescript
|
||||
// 填充背景色
|
||||
ctx.fillStyle = get(theme, `point-s.fill-${type}`) ?? '';
|
||||
ctx.fill();
|
||||
|
||||
// 绘制边框
|
||||
ctx.strokeStyle = get(theme, active ? 'point-s.strokeActive' : 'point-s.stroke') ?? '';
|
||||
```
|
||||
|
||||
**分析**:根据点位类型和激活状态设置不同的填充色和边框色。
|
||||
|
||||
#### 临时避让点特殊标记
|
||||
|
||||
```typescript
|
||||
if (type === MapPointType.临时避让点) {
|
||||
ctx.lineCap = 'round'; // 设置线条端点为圆形
|
||||
ctx.beginPath();
|
||||
// 绘制8个短线标记,形成放射状效果
|
||||
ctx.moveTo(x + 0.66 * r, y + h / 2 - 0.66 * r);
|
||||
ctx.lineTo(x + r, y + h / 2 - r);
|
||||
// ... 其他7个方向的短线
|
||||
}
|
||||
```
|
||||
|
||||
**分析**:在菱形的8个方向绘制短线,形成特殊的视觉标识。
|
||||
|
||||
#### 大点位绘制(类型11+)
|
||||
|
||||
```typescript
|
||||
case MapPointType.电梯点:
|
||||
case MapPointType.自动门点:
|
||||
case MapPointType.充电点:
|
||||
case MapPointType.停靠点:
|
||||
case MapPointType.动作点:
|
||||
case MapPointType.禁行点:
|
||||
ctx.roundRect(x, y, w, h, r); // 绘制圆角矩形
|
||||
ctx.strokeStyle = get(theme, active ? 'point-l.strokeActive' : 'point-l.stroke') ?? '';
|
||||
ctx.stroke();
|
||||
```
|
||||
|
||||
**分析**:大点位使用圆角矩形,通过 `roundRect()` API 一次性绘制。
|
||||
|
||||
#### 文本标签绘制
|
||||
|
||||
```typescript
|
||||
// 设置文本样式
|
||||
ctx.fillStyle = get(theme, 'color') ?? '';
|
||||
ctx.font = `${fontSize}px/${lineHeight} ${fontFamily}`;
|
||||
ctx.textAlign = 'center'; // 水平居中
|
||||
ctx.textBaseline = 'top'; // 垂直顶部对齐
|
||||
|
||||
// 在点位上方绘制标签
|
||||
ctx.fillText(label, x + w / 2, y - fontSize * lineHeight);
|
||||
```
|
||||
|
||||
```typescript
|
||||
// 恢复画布状态
|
||||
ctx.restore();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. 路线绘制函数 `drawLine()`
|
||||
|
||||
#### 核心绘制逻辑
|
||||
|
||||
```typescript
|
||||
// 1. 获取路线的两个端点坐标
|
||||
const [p1, p2] = pen.calculative?.worldAnchors ?? [];
|
||||
const { x: x1 = 0, y: y1 = 0 } = p1 ?? {};
|
||||
const { x: x2 = 0, y: y2 = 0 } = p2 ?? {};
|
||||
```
|
||||
|
||||
```typescript
|
||||
// 2. 获取路线属性
|
||||
const { type, direction = 1, pass = 0, c1, c2 } = pen.route ?? {};
|
||||
const { x: dx1 = 0, y: dy1 = 0 } = c1 ?? {}; // 控制点1偏移
|
||||
const { x: dx2 = 0, y: dy2 = 0 } = c2 ?? {}; // 控制点2偏移
|
||||
```
|
||||
|
||||
#### 路线类型绘制
|
||||
|
||||
```typescript
|
||||
ctx.moveTo(x1, y1); // 移动到起点
|
||||
switch (type) {
|
||||
case MapRouteType.直线:
|
||||
ctx.lineTo(x2, y2); // 直接连线到终点
|
||||
break;
|
||||
|
||||
case MapRouteType.二阶贝塞尔曲线:
|
||||
// 使用一个控制点绘制曲线
|
||||
ctx.quadraticCurveTo(x1 + dx1 * s, y1 + dy1 * s, x2, y2);
|
||||
break;
|
||||
|
||||
case MapRouteType.三阶贝塞尔曲线:
|
||||
// 使用两个控制点绘制更复杂的曲线
|
||||
ctx.bezierCurveTo(x1 + dx1 * s, y1 + dy1 * s, x2 + dx2 * s, y2 + dy2 * s, x2, y2);
|
||||
break;
|
||||
}
|
||||
```
|
||||
|
||||
**贝塞尔曲线原理**:
|
||||
|
||||
- **二阶贝塞尔曲线**:由起点、一个控制点、终点定义的曲线
|
||||
- **三阶贝塞尔曲线**:由起点、两个控制点、终点定义的更灵活曲线
|
||||
- **数学公式**:基于参数方程计算曲线上的每个点
|
||||
|
||||
#### 禁行路线绘制
|
||||
|
||||
```typescript
|
||||
if (pass === MapRoutePassType.禁行) {
|
||||
ctx.setLineDash([s * 5]); // 设置虚线样式
|
||||
}
|
||||
ctx.stroke(); // 绘制路线
|
||||
```
|
||||
|
||||
#### 方向箭头绘制
|
||||
|
||||
```typescript
|
||||
// 1. 计算箭头角度
|
||||
let r = (() => {
|
||||
switch (type) {
|
||||
case MapRouteType.直线:
|
||||
return Math.atan2(y2 - y1, x2 - x1); // 直线的角度
|
||||
case MapRouteType.二阶贝塞尔曲线:
|
||||
// 根据控制点计算切线角度
|
||||
return direction < 0 ? Math.atan2(dy1 * s, dx1 * s) : Math.atan2(y2 - y1 - dy1 * s, x2 - x1 - dx1 * s);
|
||||
// ...
|
||||
}
|
||||
})();
|
||||
```
|
||||
|
||||
```typescript
|
||||
// 2. 移动坐标系到箭头位置
|
||||
if (direction < 0) {
|
||||
ctx.translate(x1, y1); // 反向箭头在起点
|
||||
} else {
|
||||
ctx.translate(x2, y2); // 正向箭头在终点
|
||||
r += Math.PI; // 旋转180度
|
||||
}
|
||||
|
||||
// 3. 绘制箭头(两条线段形成尖角)
|
||||
ctx.moveTo(Math.cos(r + Math.PI / 5) * s * 10, Math.sin(r + Math.PI / 5) * s * 10);
|
||||
ctx.lineTo(0, 0);
|
||||
ctx.lineTo(Math.cos(r - Math.PI / 5) * s * 10, Math.sin(r - Math.PI / 5) * s * 10);
|
||||
```
|
||||
|
||||
**箭头绘制原理**:
|
||||
|
||||
- 使用三角函数计算箭头两条边的端点
|
||||
- `Math.PI / 5` (36度) 是箭头的张开角度
|
||||
- 通过坐标变换将箭头定位到正确位置和角度
|
||||
|
||||
---
|
||||
|
||||
### 3. 区域绘制函数 `drawArea()`
|
||||
|
||||
#### 矩形区域绘制
|
||||
|
||||
```typescript
|
||||
// 1. 绘制填充矩形
|
||||
ctx.rect(x, y, w, h); // 定义矩形路径
|
||||
ctx.fillStyle = get(theme, `area.fill-${type}`) ?? ''; // 设置填充色
|
||||
ctx.fill(); // 填充矩形
|
||||
|
||||
// 2. 绘制边框
|
||||
ctx.strokeStyle = get(theme, active ? 'area.strokeActive' : `area.stroke-${type}`) ?? '';
|
||||
ctx.stroke(); // 绘制边框
|
||||
```
|
||||
|
||||
#### 区域标签
|
||||
|
||||
```typescript
|
||||
// 在区域上方居中显示标签
|
||||
ctx.fillStyle = get(theme, 'color') ?? '';
|
||||
ctx.font = `${fontSize}px/${lineHeight} ${fontFamily}`;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'top';
|
||||
ctx.fillText(label, x + w / 2, y - fontSize * lineHeight);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. 机器人绘制函数 `drawRobot()`
|
||||
|
||||
#### 机器人本体绘制
|
||||
|
||||
```typescript
|
||||
const ox = x + w / 2; // 机器人中心X坐标
|
||||
const oy = y + h / 2; // 机器人中心Y坐标
|
||||
|
||||
// 绘制椭圆形机器人
|
||||
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();
|
||||
```
|
||||
|
||||
#### 路径轨迹绘制
|
||||
|
||||
```typescript
|
||||
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();
|
||||
}
|
||||
```
|
||||
|
||||
#### 路径终点箭头
|
||||
|
||||
```typescript
|
||||
// 计算路径最后两个点的方向
|
||||
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.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();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Canvas 2D API 核心方法说明
|
||||
|
||||
### 路径绘制方法
|
||||
|
||||
| 方法 | 功能 | 示例 |
|
||||
| --------------------------------------------- | ------------------ | -------------------- |
|
||||
| `beginPath()` | 开始新的绘制路径 | 每次绘制新图形前调用 |
|
||||
| `moveTo(x, y)` | 移动画笔到指定位置 | 设置绘制起点 |
|
||||
| `lineTo(x, y)` | 画直线到指定位置 | 绘制线段 |
|
||||
| `arcTo(x1, y1, x2, y2, r)` | 绘制圆弧连接 | 创建圆角效果 |
|
||||
| `quadraticCurveTo(cpx, cpy, x, y)` | 二阶贝塞尔曲线 | 简单曲线 |
|
||||
| `bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)` | 三阶贝塞尔曲线 | 复杂曲线 |
|
||||
| `rect(x, y, w, h)` | 矩形路径 | 绘制矩形 |
|
||||
| `ellipse(x, y, rx, ry, rotation, start, end)` | 椭圆路径 | 绘制椭圆/圆形 |
|
||||
| `closePath()` | 闭合当前路径 | 连接起点和终点 |
|
||||
|
||||
### 样式设置方法
|
||||
|
||||
| 属性/方法 | 功能 | 示例 |
|
||||
| -------------------- | ---------------- | ------------------------------- |
|
||||
| `fillStyle` | 设置填充颜色 | `ctx.fillStyle = '#ff0000'` |
|
||||
| `strokeStyle` | 设置描边颜色 | `ctx.strokeStyle = '#0000ff'` |
|
||||
| `lineWidth` | 设置线条宽度 | `ctx.lineWidth = 2` |
|
||||
| `lineCap` | 设置线条端点样式 | `'round'`, `'square'`, `'butt'` |
|
||||
| `setLineDash([...])` | 设置虚线样式 | `ctx.setLineDash([5, 5])` |
|
||||
|
||||
### 变换方法
|
||||
|
||||
| 方法 | 功能 | 说明 |
|
||||
| -------------------------------- | ------------ | -------------- |
|
||||
| `translate(x, y)` | 平移坐标系 | 移动原点位置 |
|
||||
| `rotate(angle)` | 旋转坐标系 | 按弧度旋转 |
|
||||
| `setTransform(a, b, c, d, e, f)` | 重置变换矩阵 | 恢复标准坐标系 |
|
||||
|
||||
### 状态管理方法
|
||||
|
||||
| 方法 | 功能 | 说明 |
|
||||
| ----------- | ------------ | ---------- |
|
||||
| `save()` | 保存当前状态 | 压入状态栈 |
|
||||
| `restore()` | 恢复之前状态 | 弹出状态栈 |
|
||||
|
||||
---
|
||||
|
||||
## 🎯 绘制流程总结
|
||||
|
||||
### 1. 标准绘制流程
|
||||
|
||||
```typescript
|
||||
function customDraw(ctx: CanvasRenderingContext2D, pen: MapPen): void {
|
||||
// 1. 保存画布状态
|
||||
ctx.save();
|
||||
|
||||
// 2. 提取绘制参数
|
||||
const { x, y, width, height } = pen.calculative?.worldRect ?? {};
|
||||
|
||||
// 3. 设置样式属性
|
||||
ctx.fillStyle = '填充色';
|
||||
ctx.strokeStyle = '边框色';
|
||||
|
||||
// 4. 创建绘制路径
|
||||
ctx.beginPath();
|
||||
// ... 具体绘制操作
|
||||
|
||||
// 5. 执行绘制
|
||||
ctx.fill(); // 填充
|
||||
ctx.stroke(); // 描边
|
||||
|
||||
// 6. 恢复画布状态
|
||||
ctx.restore();
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 性能优化要点
|
||||
|
||||
- **状态管理**:及时调用 `save()` 和 `restore()` 避免状态污染
|
||||
- **路径复用**:合理使用 `beginPath()` 清除之前的路径
|
||||
- **批量绘制**:同类型图形可以合并绘制操作
|
||||
- **避免重复计算**:缓存复杂的数学计算结果
|
||||
|
||||
---
|
||||
|
||||
## 📊 技术优势
|
||||
|
||||
### 1. Canvas 2D 的优势
|
||||
|
||||
- **高性能**:直接操作像素,渲染速度快
|
||||
- **灵活性**:可以绘制任意复杂的图形
|
||||
- **交互性**:支持鼠标事件检测和处理
|
||||
- **兼容性**:现代浏览器完全支持
|
||||
|
||||
### 2. 自定义绘制的优势
|
||||
|
||||
- **个性化**:完全定制化的视觉效果
|
||||
- **主题支持**:动态切换颜色主题
|
||||
- **状态反馈**:不同状态显示不同样式
|
||||
- **扩展性**:易于添加新的图形类型
|
||||
|
||||
---
|
||||
|
||||
## 🔚 结语
|
||||
|
||||
本场景编辑器通过 Canvas 2D API 实现了丰富的图形绘制功能,每个绘制函数都经过精心设计,既保证了视觉效果,又兼顾了性能表现。理解这些绘制原理对于进一步扩展和优化编辑器功能具有重要意义。
|
@ -1,600 +0,0 @@
|
||||
# Meta2D引擎作用详解
|
||||
|
||||
## 🤔 您的疑问:Meta2D到底做了什么?
|
||||
|
||||
您的观察很准确!确实,具体的绘制操作都是通过HTML5 Canvas原生API实现的。那么Meta2D引擎到底在做什么呢?
|
||||
|
||||
**简单类比**:如果说Canvas API是"画笔和颜料",那么Meta2D就是"画师的大脑和手" - 它决定什么时候画、画在哪里、画什么样式,以及如何响应用户的操作。
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Meta2D引擎的核心作用
|
||||
|
||||
Meta2D引擎并不是替代Canvas API,而是在Canvas API之上构建了一个完整的**图形管理和渲染框架**。它的主要作用包括:
|
||||
|
||||
### 1. 🎨 渲染管理系统
|
||||
|
||||
```typescript
|
||||
// 我们只需要注册绘制函数
|
||||
this.registerCanvasDraw({
|
||||
point: drawPoint,
|
||||
line: drawLine,
|
||||
area: drawArea,
|
||||
robot: drawRobot,
|
||||
});
|
||||
|
||||
// Meta2D会自动调用这些函数
|
||||
```
|
||||
|
||||
**Meta2D负责**:
|
||||
|
||||
- **何时渲染**:自动检测数据变化,决定何时重绘
|
||||
- **渲染顺序**:管理图层顺序,确保正确的绘制层级
|
||||
- **性能优化**:只重绘需要更新的部分,避免全量重绘
|
||||
- **调用时机**:在正确的时机调用我们的绘制函数
|
||||
|
||||
### 2. 🎮 图形对象管理
|
||||
|
||||
```typescript
|
||||
// 创建一个点位 - 我们只需要定义数据结构
|
||||
const pen: MapPen = {
|
||||
id: 'point1',
|
||||
name: 'point',
|
||||
x: 100,
|
||||
y: 100,
|
||||
width: 24,
|
||||
height: 24,
|
||||
point: { type: MapPointType.普通点 },
|
||||
};
|
||||
|
||||
// Meta2D会管理这个对象的整个生命周期
|
||||
await this.addPen(pen, false, true, true);
|
||||
```
|
||||
|
||||
**Meta2D负责**:
|
||||
|
||||
- **对象存储**:维护所有图形对象的数据结构
|
||||
- **坐标转换**:从逻辑坐标转换为屏幕坐标
|
||||
- **状态管理**:追踪每个对象的状态(选中、激活、可见等)
|
||||
- **生命周期**:管理对象的创建、更新、删除
|
||||
|
||||
### 3. 🖱️ 事件处理系统
|
||||
|
||||
```typescript
|
||||
// 我们只需要监听高级事件
|
||||
this.on('click', (e, data) => {
|
||||
// Meta2D已经处理了鼠标点击的复杂逻辑
|
||||
console.log('点击了图形:', data);
|
||||
});
|
||||
```
|
||||
|
||||
**Meta2D负责**:
|
||||
|
||||
- **事件捕获**:监听原生DOM事件(mousedown、mousemove、mouseup等)
|
||||
- **坐标转换**:将屏幕坐标转换为画布坐标
|
||||
- **碰撞检测**:判断点击了哪个图形对象
|
||||
- **事件分发**:将事件分发给正确的处理器
|
||||
|
||||
### 4. 📐 坐标系统管理
|
||||
|
||||
```typescript
|
||||
// 我们在绘制函数中使用的坐标
|
||||
const { x, y, width, height } = pen.calculative?.worldRect ?? {};
|
||||
```
|
||||
|
||||
**Meta2D负责**:
|
||||
|
||||
- **坐标计算**:自动计算`worldRect`(世界坐标)
|
||||
- **缩放处理**:处理画布缩放时的坐标转换
|
||||
- **视口管理**:管理可视区域和裁剪
|
||||
- **变换矩阵**:处理复杂的坐标变换
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Meta2D的工作流程
|
||||
|
||||
### 1. 初始化阶段
|
||||
|
||||
```typescript
|
||||
export class EditorService extends Meta2d {
|
||||
constructor(container: HTMLDivElement) {
|
||||
// 1. 创建Meta2D实例,传入配置
|
||||
super(container, EDITOR_CONFIG);
|
||||
|
||||
// 2. 注册自定义绘制函数
|
||||
this.#register();
|
||||
|
||||
// 3. 监听事件
|
||||
this.on('*', (e, v) => this.#listen(e, v));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 图形创建阶段
|
||||
|
||||
```typescript
|
||||
// 用户调用
|
||||
await this.addPoint({ x: 100, y: 100 }, MapPointType.普通点);
|
||||
|
||||
// Meta2D内部流程:
|
||||
// 1. 创建图形对象数据结构
|
||||
// 2. 分配唯一ID
|
||||
// 3. 计算坐标和尺寸
|
||||
// 4. 添加到图形列表
|
||||
// 5. 触发重绘
|
||||
```
|
||||
|
||||
### 3. 渲染阶段
|
||||
|
||||
```typescript
|
||||
// Meta2D的渲染循环(简化版)
|
||||
function render() {
|
||||
// 1. 清空画布
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// 2. 遍历所有图形对象
|
||||
for (const pen of this.store.data.pens) {
|
||||
// 3. 计算世界坐标
|
||||
this.updateWorldRect(pen);
|
||||
|
||||
// 4. 调用对应的绘制函数
|
||||
const drawFn = this.canvasDrawMap[pen.name]; // 获取我们注册的绘制函数
|
||||
if (drawFn) {
|
||||
drawFn(ctx, pen); // 调用 drawPoint、drawLine 等
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 事件处理阶段
|
||||
|
||||
```typescript
|
||||
// 用户点击画布
|
||||
canvas.addEventListener('click', (e) => {
|
||||
// Meta2D内部处理:
|
||||
// 1. 获取点击坐标
|
||||
const point = { x: e.offsetX, y: e.offsetY };
|
||||
|
||||
// 2. 转换为画布坐标
|
||||
const worldPoint = this.screenToWorld(point);
|
||||
|
||||
// 3. 碰撞检测 - 判断点击了哪个图形
|
||||
const hitPen = this.hitTest(worldPoint);
|
||||
|
||||
// 4. 触发相应事件
|
||||
if (hitPen) {
|
||||
this.emit('click', e, hitPen);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Meta2D的核心价值
|
||||
|
||||
### 1. 抽象层次提升
|
||||
|
||||
```typescript
|
||||
// 没有Meta2D,我们需要手动处理:
|
||||
canvas.addEventListener('mousedown', (e) => {
|
||||
// 计算点击坐标
|
||||
// 检测点击了哪个图形
|
||||
// 处理拖拽逻辑
|
||||
// 重绘画布
|
||||
// ... 大量底层代码
|
||||
});
|
||||
|
||||
// 有了Meta2D,我们只需要:
|
||||
this.on('mousedown', (e, pen) => {
|
||||
// 直接处理业务逻辑
|
||||
console.log('点击了图形:', pen.id);
|
||||
});
|
||||
```
|
||||
|
||||
### 2. 数据驱动渲染
|
||||
|
||||
```typescript
|
||||
// 数据变化自动触发重绘
|
||||
this.setValue({ id: 'point1', x: 200 }); // Meta2D会自动重绘
|
||||
```
|
||||
|
||||
### 3. 复杂交互支持
|
||||
|
||||
```typescript
|
||||
// 选择、拖拽、缩放、旋转等复杂交互
|
||||
this.active(['point1', 'point2']); // 多选
|
||||
this.inactive(); // 取消选择
|
||||
this.delete([pen]); // 删除图形
|
||||
```
|
||||
|
||||
### 4. 性能优化
|
||||
|
||||
- **脏矩形重绘**:只重绘变化的区域
|
||||
- **离屏渲染**:复杂图形使用离屏Canvas
|
||||
- **层级管理**:合理的图层分离
|
||||
- **事件优化**:高效的碰撞检测算法
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ 架构分层对比
|
||||
|
||||
### 传统Canvas开发
|
||||
|
||||
```
|
||||
┌─────────────────────┐
|
||||
│ 业务逻辑层 │
|
||||
├─────────────────────┤
|
||||
│ 手动管理层 │ ← 需要自己实现
|
||||
│ (对象管理/事件/渲染) │
|
||||
├─────────────────────┤
|
||||
│ Canvas 2D API │
|
||||
├─────────────────────┤
|
||||
│ 浏览器引擎 │
|
||||
└─────────────────────┘
|
||||
```
|
||||
|
||||
### 使用Meta2D
|
||||
|
||||
```
|
||||
┌─────────────────────┐
|
||||
│ 业务逻辑层 │ ← 我们专注于这里
|
||||
├─────────────────────┤
|
||||
│ Meta2D 引擎 │ ← 引擎处理复杂逻辑
|
||||
├─────────────────────┤
|
||||
│ Canvas 2D API │ ← 底层绘制API
|
||||
├─────────────────────┤
|
||||
│ 浏览器引擎 │
|
||||
└─────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 实际代码示例
|
||||
|
||||
### 没有Meta2D的代码(复杂)
|
||||
|
||||
```typescript
|
||||
class ManualCanvas {
|
||||
private pens: MapPen[] = [];
|
||||
private selectedPens: MapPen[] = [];
|
||||
|
||||
constructor(private canvas: HTMLCanvasElement) {
|
||||
this.canvas.addEventListener('click', this.onClick.bind(this));
|
||||
this.canvas.addEventListener('mousemove', this.onMouseMove.bind(this));
|
||||
// ... 更多事件监听
|
||||
}
|
||||
|
||||
onClick(e: MouseEvent) {
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
|
||||
// 手动碰撞检测
|
||||
for (const pen of this.pens) {
|
||||
if (this.isPointInPen(x, y, pen)) {
|
||||
this.selectPen(pen);
|
||||
this.render(); // 手动重绘
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const ctx = this.canvas.getContext('2d')!;
|
||||
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
// 手动绘制每个图形
|
||||
for (const pen of this.pens) {
|
||||
this.drawPen(ctx, pen);
|
||||
}
|
||||
}
|
||||
|
||||
// ... 大量的手动管理代码
|
||||
}
|
||||
```
|
||||
|
||||
### 使用Meta2D的代码(简洁)
|
||||
|
||||
```typescript
|
||||
class EditorService extends Meta2d {
|
||||
constructor(container: HTMLDivElement) {
|
||||
super(container, EDITOR_CONFIG);
|
||||
|
||||
// 注册绘制函数
|
||||
this.registerCanvasDraw({ point: drawPoint });
|
||||
|
||||
// 监听事件
|
||||
this.on('click', (e, pen) => {
|
||||
// 直接处理业务逻辑
|
||||
this.handlePenClick(pen);
|
||||
});
|
||||
}
|
||||
|
||||
async addPoint(p: Point, type: MapPointType) {
|
||||
const pen: MapPen = {
|
||||
// ... 定义数据结构
|
||||
};
|
||||
|
||||
// Meta2D自动处理渲染
|
||||
await this.addPen(pen);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 总结
|
||||
|
||||
Meta2D引擎的作用就像是一个**智能管家**:
|
||||
|
||||
1. **您专注于业务**:定义数据结构和绘制逻辑
|
||||
2. **引擎处理细节**:坐标转换、事件处理、渲染优化
|
||||
3. **原生API执行**:最终通过Canvas API完成绘制
|
||||
|
||||
这种分工让您可以:
|
||||
|
||||
- 🎯 **专注业务逻辑**:不需要处理复杂的底层细节
|
||||
- 🚀 **提高开发效率**:大量重复的工作由引擎完成
|
||||
- 🎨 **获得更好的性能**:引擎内置了各种优化策略
|
||||
- 🔧 **更容易维护**:清晰的架构分层
|
||||
|
||||
**Meta2D = 图形管理框架 + 事件处理系统 + 渲染优化引擎**
|
||||
|
||||
它不是替代Canvas API,而是在Canvas API之上构建了一个完整的企业级图形编辑解决方案!
|
||||
|
||||
---
|
||||
|
||||
## 📱 项目中的实际应用
|
||||
|
||||
### 1. 响应式数据流集成
|
||||
|
||||
```typescript
|
||||
export class EditorService extends Meta2d {
|
||||
// Meta2D处理底层变化,我们用RxJS处理业务逻辑
|
||||
readonly #change$$ = new Subject<boolean>();
|
||||
|
||||
public readonly current = useObservable<MapPen>(
|
||||
this.#change$$.pipe(
|
||||
debounceTime(100),
|
||||
map(() => <MapPen>clone(this.store.active?.[0])),
|
||||
),
|
||||
);
|
||||
|
||||
// Meta2D的事件 → RxJS流 → Vue响应式数据
|
||||
#listen(e: unknown, v: any) {
|
||||
switch (e) {
|
||||
case 'add':
|
||||
case 'delete':
|
||||
case 'update':
|
||||
this.#change$$.next(true); // 通知数据变化
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 复杂业务逻辑简化
|
||||
|
||||
```typescript
|
||||
// 创建区域时的智能关联
|
||||
public async addArea(p1: Point, p2: Point, type = MapAreaType.库区, id?: string) {
|
||||
// Meta2D自动处理选中状态
|
||||
const selected = <MapPen[]>this.store.active;
|
||||
|
||||
// 根据区域类型自动关联相关元素
|
||||
switch (type) {
|
||||
case MapAreaType.库区:
|
||||
selected?.filter(({ point }) => point?.type === MapPointType.动作点)
|
||||
.forEach(({ id }) => points.push(id!));
|
||||
break;
|
||||
case MapAreaType.互斥区:
|
||||
selected?.filter(({ point }) => point?.type).forEach(({ id }) => points.push(id!));
|
||||
selected?.filter(({ route }) => route?.type).forEach(({ id }) => routes.push(id!));
|
||||
break;
|
||||
}
|
||||
|
||||
// Meta2D自动处理图形创建和渲染
|
||||
const area = await this.addPen(pen, true, true, true);
|
||||
this.bottom(area); // 自动层级管理
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 主题系统集成
|
||||
|
||||
```typescript
|
||||
// 监听主题变化,Meta2D自动重绘
|
||||
watch(
|
||||
() => sTheme.theme,
|
||||
(theme) => {
|
||||
this.setTheme(theme); // Meta2D内置主题系统
|
||||
|
||||
// 重新应用主题到自定义绘制
|
||||
this.find('point').forEach((pen) => {
|
||||
if (pen.point?.type >= 10) {
|
||||
this.canvas.updateValue(pen, this.#mapPointImage(pen.point.type));
|
||||
}
|
||||
});
|
||||
|
||||
this.render(); // 触发重绘
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
```
|
||||
|
||||
### 4. 实时数据更新
|
||||
|
||||
```typescript
|
||||
// 机器人实时位置更新
|
||||
public refreshRobot(id: RobotInfo['id'], info: Partial<RobotRealtimeInfo>): void {
|
||||
const { x: cx = 37, y: cy = 37, active, angle, path: points } = info;
|
||||
|
||||
// Meta2D自动处理坐标转换和重绘
|
||||
this.setValue({
|
||||
id,
|
||||
x: cx - 37,
|
||||
y: cy - 37,
|
||||
rotate: angle,
|
||||
robot: { ...robot, active, path },
|
||||
visible: true
|
||||
}, { render: true, history: false, doEvent: false });
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 事件系统的实际使用
|
||||
|
||||
```typescript
|
||||
constructor(container: HTMLDivElement) {
|
||||
super(container, EDITOR_CONFIG);
|
||||
|
||||
// Meta2D统一事件处理
|
||||
this.on('*', (e, v) => this.#listen(e, v));
|
||||
|
||||
// 具体事件映射到业务逻辑
|
||||
#listen(e: unknown, v: any) {
|
||||
switch (e) {
|
||||
case 'click':
|
||||
case 'mousedown':
|
||||
case 'mouseup':
|
||||
// 转换为响应式数据流
|
||||
this.#mouse$$.next({ type: e, value: pick(v, 'x', 'y') });
|
||||
break;
|
||||
case 'active':
|
||||
case 'inactive':
|
||||
// 选中状态变化
|
||||
this.#change$$.next(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 如果没有Meta2D会怎样?
|
||||
|
||||
假设我们要自己实现同样的功能,需要处理的复杂度:
|
||||
|
||||
### 1. 坐标系统管理
|
||||
|
||||
```typescript
|
||||
// 需要手动处理缩放、平移、坐标转换
|
||||
class CoordinateSystem {
|
||||
private scale = 1;
|
||||
private offsetX = 0;
|
||||
private offsetY = 0;
|
||||
|
||||
screenToWorld(screenPoint: Point): Point {
|
||||
return {
|
||||
x: (screenPoint.x - this.offsetX) / this.scale,
|
||||
y: (screenPoint.y - this.offsetY) / this.scale,
|
||||
};
|
||||
}
|
||||
|
||||
worldToScreen(worldPoint: Point): Point {
|
||||
return {
|
||||
x: worldPoint.x * this.scale + this.offsetX,
|
||||
y: worldPoint.y * this.scale + this.offsetY,
|
||||
};
|
||||
}
|
||||
|
||||
// 还需要处理矩阵变换、旋转等复杂情况...
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 碰撞检测系统
|
||||
|
||||
```typescript
|
||||
// 需要为每种图形实现碰撞检测
|
||||
class HitTest {
|
||||
hitTestPoint(point: Point, pen: MapPen): boolean {
|
||||
const rect = this.getPenRect(pen);
|
||||
return point.x >= rect.x && point.x <= rect.x + rect.width && point.y >= rect.y && point.y <= rect.y + rect.height;
|
||||
}
|
||||
|
||||
hitTestLine(point: Point, pen: MapPen): boolean {
|
||||
// 需要实现点到线段的距离计算
|
||||
// 还要考虑贝塞尔曲线的复杂情况
|
||||
}
|
||||
|
||||
hitTestArea(point: Point, pen: MapPen): boolean {
|
||||
// 矩形碰撞检测
|
||||
}
|
||||
|
||||
// 还需要处理旋转、缩放后的碰撞检测...
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 渲染管理系统
|
||||
|
||||
```typescript
|
||||
// 需要手动管理渲染队列和优化
|
||||
class RenderManager {
|
||||
private dirtyRects: Rect[] = [];
|
||||
private renderQueue: MapPen[] = [];
|
||||
|
||||
markDirty(pen: MapPen) {
|
||||
this.dirtyRects.push(this.getPenRect(pen));
|
||||
this.renderQueue.push(pen);
|
||||
}
|
||||
|
||||
render() {
|
||||
// 计算需要重绘的区域
|
||||
const mergedRect = this.mergeDirtyRects();
|
||||
|
||||
// 清空脏区域
|
||||
this.ctx.clearRect(mergedRect.x, mergedRect.y, mergedRect.width, mergedRect.height);
|
||||
|
||||
// 重绘相关图形
|
||||
for (const pen of this.getIntersectingPens(mergedRect)) {
|
||||
this.drawPen(pen);
|
||||
}
|
||||
}
|
||||
|
||||
// 大量的优化逻辑...
|
||||
}
|
||||
```
|
||||
|
||||
**这些复杂的底层逻辑,Meta2D都已经帮我们处理好了!**
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Meta2D带来的开发体验提升
|
||||
|
||||
### 开发效率对比
|
||||
|
||||
| 功能 | 纯Canvas开发 | 使用Meta2D |
|
||||
| -------------------- | ------------ | ----------------------------- |
|
||||
| 创建一个可点击的图形 | ~100行代码 | ~10行代码 |
|
||||
| 实现拖拽功能 | ~200行代码 | 内置支持 |
|
||||
| 多选和批量操作 | ~300行代码 | `this.active([...])` |
|
||||
| 撤销重做 | ~500行代码 | 内置支持 |
|
||||
| 图形层级管理 | ~100行代码 | `this.top()`, `this.bottom()` |
|
||||
| 响应式数据绑定 | 需要自己实现 | 内置事件系统 |
|
||||
|
||||
### 维护成本对比
|
||||
|
||||
| 场景 | 纯Canvas | Meta2D |
|
||||
| -------------- | ---------------- | ------------------ |
|
||||
| 添加新图形类型 | 修改多个系统 | 添加绘制函数即可 |
|
||||
| 性能优化 | 需要深度优化 | 引擎已优化 |
|
||||
| Bug修复 | 涉及多个底层模块 | 通常只涉及业务逻辑 |
|
||||
| 功能扩展 | 可能需要重构架构 | 基于现有API扩展 |
|
||||
|
||||
---
|
||||
|
||||
## 🏆 总结
|
||||
|
||||
Meta2D引擎就像是为Canvas开发者提供的一个**超级工具箱**:
|
||||
|
||||
- 🎨 **您负责创意**:定义什么样的图形、什么样的交互
|
||||
- 🔧 **Meta2D负责实现**:处理所有复杂的底层逻辑
|
||||
- 🚀 **Canvas负责绘制**:最终的像素级渲染
|
||||
|
||||
这种架构让我们的场景编辑器项目能够:
|
||||
|
||||
- ✅ 快速开发复杂的图形编辑功能
|
||||
- ✅ 获得企业级的性能和稳定性
|
||||
- ✅ 专注于业务逻辑而不是底层实现
|
||||
- ✅ 轻松维护和扩展功能
|
||||
|
||||
**Meta2D不是画笔,而是整个画室的管理系统!**
|
@ -1,646 +0,0 @@
|
||||
# WebSocket增强服务技术设计文档
|
||||
|
||||
## 概述
|
||||
|
||||
本文档详细解释了 `src/services/ws.ts` 的技术设计思路、架构选择和实现细节。这个文件实现了一个增强的WebSocket服务,在保持原有接口不变的前提下,添加了心跳检测、自动重连、错误处理等企业级功能。
|
||||
|
||||
## 设计目标
|
||||
|
||||
### 主要目标
|
||||
|
||||
1. **零侵入性**:业务代码无需修改,完全透明的功能增强
|
||||
2. **企业级稳定性**:心跳检测、自动重连、错误恢复
|
||||
3. **可配置性**:全局配置,易于调整和优化
|
||||
4. **类型安全**:完整的TypeScript类型支持
|
||||
5. **内存安全**:正确的资源管理,防止内存泄漏
|
||||
|
||||
### 兼容性目标
|
||||
|
||||
- 保持原有 `create(path): Promise<WebSocket>` 接口不变
|
||||
- 返回标准WebSocket实例,支持所有原生API
|
||||
- 业务代码中的 `ws.onmessage`, `ws.close()` 等调用完全兼容
|
||||
|
||||
## 架构设计
|
||||
|
||||
### 整体架构图
|
||||
|
||||
```
|
||||
┌─────────────────────┐ ┌──────────────────────┐ ┌─────────────────────┐
|
||||
│ 业务代码 │ │ EnhancedWebSocket │ │ 原生WebSocket │
|
||||
│ │ │ (包装器) │ │ │
|
||||
│ ws.onmessage = ... │───▶│ 事件拦截和过滤 │───▶│ 实际网络连接 │
|
||||
│ ws.send(data) │ │ 心跳检测逻辑 │ │ │
|
||||
│ ws.close() │ │ 重连管理 │ │ │
|
||||
└─────────────────────┘ └──────────────────────┘ └─────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ WS_CONFIG │
|
||||
│ (全局配置) │
|
||||
│ - 心跳间隔 │
|
||||
│ - 重连策略 │
|
||||
│ - 超时设置 │
|
||||
└──────────────────────┘
|
||||
```
|
||||
|
||||
### 设计模式选择
|
||||
|
||||
#### 1. 包装器模式 (Wrapper Pattern)
|
||||
|
||||
```typescript
|
||||
class EnhancedWebSocket {
|
||||
private ws: WebSocket; // 包装原生WebSocket
|
||||
}
|
||||
```
|
||||
|
||||
**为什么选择包装器而不是继承?**
|
||||
|
||||
1. **继承的问题**:
|
||||
|
||||
```typescript
|
||||
// 继承方式的问题
|
||||
class EnhancedWebSocket extends WebSocket {
|
||||
constructor(url: string) {
|
||||
super(url); // 连接立即开始,无法在事件处理器设置前进行拦截
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **包装器的优势**:
|
||||
```typescript
|
||||
// 包装器方式的优势
|
||||
class EnhancedWebSocket {
|
||||
constructor(path: string, baseUrl: string) {
|
||||
this.ws = new WebSocket(baseUrl + path); // 控制创建时机
|
||||
this.setupHandlers(); // 立即设置我们的处理器
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. 代理模式 (Proxy Pattern)
|
||||
|
||||
通过getter/setter拦截用户对事件处理器的设置:
|
||||
|
||||
```typescript
|
||||
get onmessage(): ((event: MessageEvent) => void) | null {
|
||||
return this.userOnMessage;
|
||||
}
|
||||
|
||||
set onmessage(handler: ((event: MessageEvent) => void) | null) {
|
||||
this.userOnMessage = handler; // 保存用户的处理器
|
||||
// 我们的处理器已经在构造时设置,会调用用户的处理器
|
||||
}
|
||||
```
|
||||
|
||||
## 核心技术实现
|
||||
|
||||
### 1. Class 设计选择
|
||||
|
||||
#### 为什么使用 Class?
|
||||
|
||||
```typescript
|
||||
class EnhancedWebSocket {
|
||||
// 私有状态管理
|
||||
private ws: WebSocket;
|
||||
private path: string;
|
||||
private heartbeatTimer?: NodeJS.Timeout;
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**选择Class的原因:**
|
||||
|
||||
1. **状态封装**:WebSocket连接需要管理多个状态(连接、定时器、配置等)
|
||||
2. **方法绑定**:事件处理器需要访问实例状态,Class提供了自然的this绑定
|
||||
3. **生命周期管理**:连接的创建、维护、销毁有清晰的生命周期
|
||||
4. **类型安全**:TypeScript对Class有更好的类型推导和检查
|
||||
|
||||
**与函数式方案的对比:**
|
||||
|
||||
```typescript
|
||||
// 函数式方案的问题
|
||||
function createEnhancedWS(path: string) {
|
||||
let heartbeatTimer: NodeJS.Timeout;
|
||||
let reconnectTimer: NodeJS.Timeout;
|
||||
// 需要大量闭包来管理状态,复杂度高
|
||||
}
|
||||
|
||||
// Class方案的优势
|
||||
class EnhancedWebSocket {
|
||||
private heartbeatTimer?: NodeJS.Timeout; // 清晰的状态管理
|
||||
private reconnectTimer?: NodeJS.Timeout;
|
||||
// 方法可以直接访问状态
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Private 成员设计
|
||||
|
||||
#### 为什么大量使用 private?
|
||||
|
||||
```typescript
|
||||
class EnhancedWebSocket {
|
||||
private ws: WebSocket; // 内部WebSocket实例
|
||||
private path: string; // 连接路径
|
||||
private heartbeatTimer?: NodeJS.Timeout; // 心跳定时器
|
||||
private reconnectTimer?: NodeJS.Timeout; // 重连定时器
|
||||
private reconnectAttempts: number = 0; // 重连次数
|
||||
private isManualClose: boolean = false; // 手动关闭标志
|
||||
private isHeartbeatTimeout: boolean = false; // 心跳超时标志
|
||||
}
|
||||
```
|
||||
|
||||
**Private的重要性:**
|
||||
|
||||
1. **封装原则**:防止外部直接访问和修改内部状态
|
||||
2. **API稳定性**:内部实现可以随时重构,不影响公共接口
|
||||
3. **状态一致性**:防止外部代码破坏内部状态的一致性
|
||||
4. **错误预防**:避免用户误用内部API导致的bug
|
||||
|
||||
**示例对比:**
|
||||
|
||||
```typescript
|
||||
// 如果没有private,用户可能这样做
|
||||
const ws = new EnhancedWebSocket('/test');
|
||||
ws.heartbeatTimer = undefined; // 💥 破坏了心跳检测
|
||||
ws.reconnectAttempts = -1; // 💥 破坏了重连逻辑
|
||||
|
||||
// 有了private,这些操作被编译器阻止
|
||||
// ✅ 确保了内部状态的安全性
|
||||
```
|
||||
|
||||
### 3. Constructor 设计
|
||||
|
||||
#### 构造函数的关键作用
|
||||
|
||||
```typescript
|
||||
constructor(path: string, baseUrl: string) {
|
||||
this.path = path;
|
||||
this.baseUrl = baseUrl;
|
||||
this.ws = new WebSocket(baseUrl + path); // 创建实际连接
|
||||
this.setupHandlers(); // 立即设置事件处理器
|
||||
}
|
||||
```
|
||||
|
||||
**设计要点:**
|
||||
|
||||
1. **立即执行**:构造时立即创建连接和设置处理器
|
||||
2. **状态初始化**:确保所有私有状态都有正确的初始值
|
||||
3. **参数验证**:(可以添加)对输入参数进行验证
|
||||
4. **最小权限**:只接收必要的参数,其他配置使用全局配置
|
||||
|
||||
**为什么不延迟创建连接?**
|
||||
|
||||
```typescript
|
||||
// ❌ 错误方案:延迟创建
|
||||
constructor(path: string, baseUrl: string) {
|
||||
this.path = path;
|
||||
this.baseUrl = baseUrl;
|
||||
// 不创建连接,等用户调用connect()
|
||||
}
|
||||
|
||||
// ✅ 正确方案:立即创建
|
||||
constructor(path: string, baseUrl: string) {
|
||||
// 立即创建,因为原有接口期望构造后就有连接
|
||||
this.ws = new WebSocket(baseUrl + path);
|
||||
this.setupHandlers();
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Getter/Setter 设计
|
||||
|
||||
#### 透明的属性代理
|
||||
|
||||
```typescript
|
||||
// 只读属性的getter
|
||||
get readyState(): number {
|
||||
return this.ws.readyState; // 直接代理到内部WebSocket
|
||||
}
|
||||
|
||||
get url(): string {
|
||||
return this.ws.url;
|
||||
}
|
||||
|
||||
// 可写属性的getter/setter
|
||||
get binaryType(): BinaryType {
|
||||
return this.ws.binaryType;
|
||||
}
|
||||
|
||||
set binaryType(value: BinaryType) {
|
||||
this.ws.binaryType = value;
|
||||
}
|
||||
```
|
||||
|
||||
**为什么需要这些getter/setter?**
|
||||
|
||||
1. **API兼容性**:用户期望能够访问标准WebSocket的所有属性
|
||||
2. **透明代理**:用户感觉在使用标准WebSocket,实际上是我们的增强版本
|
||||
3. **状态同步**:确保外部看到的状态与内部WebSocket状态一致
|
||||
|
||||
#### 事件处理器的特殊getter/setter
|
||||
|
||||
```typescript
|
||||
// 事件处理器的拦截
|
||||
get onmessage(): ((event: MessageEvent) => void) | null {
|
||||
return this.userOnMessage; // 返回用户设置的处理器
|
||||
}
|
||||
|
||||
set onmessage(handler: ((event: MessageEvent) => void) | null) {
|
||||
this.userOnMessage = handler; // 保存用户的处理器
|
||||
// 我们的内部处理器会调用用户的处理器
|
||||
}
|
||||
```
|
||||
|
||||
**关键设计思路:**
|
||||
|
||||
1. **双层处理**:我们的处理器 + 用户的处理器
|
||||
2. **透明性**:用户感觉直接在设置WebSocket的事件处理器
|
||||
3. **控制权**:我们先处理(如过滤心跳),再传递给用户
|
||||
|
||||
### 5. 事件处理架构
|
||||
|
||||
#### 事件流设计
|
||||
|
||||
```
|
||||
WebSocket原生事件 → 我们的处理器 → 过滤/处理 → 用户的处理器
|
||||
```
|
||||
|
||||
#### 具体实现
|
||||
|
||||
```typescript
|
||||
private setupHandlers(): void {
|
||||
// 1. 设置我们的处理器
|
||||
this.ws.onmessage = (event) => {
|
||||
const messageData = event.data;
|
||||
|
||||
// 2. 我们先处理(心跳检测)
|
||||
let isHeartbeatResponse = false;
|
||||
if (typeof messageData === 'string' && messageData === WS_CONFIG.heartbeatResponseType) {
|
||||
isHeartbeatResponse = true;
|
||||
}
|
||||
|
||||
if (isHeartbeatResponse) {
|
||||
this.clearHeartbeatTimeout(); // 清除心跳超时
|
||||
return; // 不传递给用户
|
||||
}
|
||||
|
||||
// 3. 传递给用户的处理器
|
||||
if (this.userOnMessage) {
|
||||
this.userOnMessage(event);
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**设计优势:**
|
||||
|
||||
1. **消息过滤**:自动过滤心跳消息,用户无感知
|
||||
2. **状态管理**:自动处理连接状态变化
|
||||
3. **错误恢复**:自动处理连接错误和重连
|
||||
|
||||
### 6. 定时器管理
|
||||
|
||||
#### 定时器生命周期管理
|
||||
|
||||
```typescript
|
||||
class EnhancedWebSocket {
|
||||
private heartbeatTimer?: NodeJS.Timeout; // 心跳发送定时器
|
||||
private heartbeatTimeoutTimer?: NodeJS.Timeout; // 心跳响应超时定时器
|
||||
private reconnectTimer?: NodeJS.Timeout; // 重连定时器
|
||||
}
|
||||
```
|
||||
|
||||
**为什么需要三个定时器?**
|
||||
|
||||
1. **heartbeatTimer**:定期发送心跳包
|
||||
2. **heartbeatTimeoutTimer**:检测心跳响应超时
|
||||
3. **reconnectTimer**:延迟重连
|
||||
|
||||
#### 定时器清理策略
|
||||
|
||||
```typescript
|
||||
// 停止心跳检测
|
||||
private stopHeartbeat(): void {
|
||||
if (this.heartbeatTimer) {
|
||||
clearInterval(this.heartbeatTimer);
|
||||
this.heartbeatTimer = undefined; // 重置为undefined
|
||||
}
|
||||
this.clearHeartbeatTimeout(); // 同时清理超时检测
|
||||
}
|
||||
|
||||
// 清除心跳响应超时检测
|
||||
private clearHeartbeatTimeout(): void {
|
||||
if (this.heartbeatTimeoutTimer) {
|
||||
clearTimeout(this.heartbeatTimeoutTimer);
|
||||
this.heartbeatTimeoutTimer = undefined; // 重置为undefined
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**内存安全保证:**
|
||||
|
||||
1. **及时清理**:每次停止时都清理定时器
|
||||
2. **状态重置**:清理后设置为undefined
|
||||
3. **多重清理**:在多个关键点都进行清理(连接关闭、手动关闭等)
|
||||
|
||||
### 7. 状态标志设计
|
||||
|
||||
#### 关键状态标志
|
||||
|
||||
```typescript
|
||||
private isManualClose: boolean = false; // 是否手动关闭
|
||||
private isHeartbeatTimeout: boolean = false; // 是否心跳超时
|
||||
private reconnectAttempts: number = 0; // 重连次数
|
||||
```
|
||||
|
||||
**为什么需要这些标志?**
|
||||
|
||||
1. **区分关闭原因**:手动关闭 vs 异常断开 vs 心跳超时
|
||||
2. **重连决策**:根据不同原因决定是否重连
|
||||
3. **状态跟踪**:跟踪重连进度和次数
|
||||
|
||||
#### 状态转换逻辑
|
||||
|
||||
```typescript
|
||||
// 心跳超时时
|
||||
private startHeartbeatTimeout(): void {
|
||||
this.heartbeatTimeoutTimer = setTimeout(() => {
|
||||
this.isHeartbeatTimeout = true; // 设置心跳超时标志
|
||||
this.ws.close(1000, 'Heartbeat timeout');
|
||||
}, WS_CONFIG.heartbeatTimeout);
|
||||
}
|
||||
|
||||
// 连接关闭时的决策
|
||||
this.ws.onclose = (event) => {
|
||||
// 如果不是手动关闭,或者是心跳超时导致的关闭,则重连
|
||||
if (!this.isManualClose || this.isHeartbeatTimeout) {
|
||||
this.scheduleReconnect();
|
||||
}
|
||||
|
||||
this.isHeartbeatTimeout = false; // 重置标志
|
||||
};
|
||||
```
|
||||
|
||||
### 8. addEventListener/removeEventListener 实现
|
||||
|
||||
#### 为什么需要这些方法?
|
||||
|
||||
```typescript
|
||||
addEventListener<K extends keyof WebSocketEventMap>(
|
||||
type: K,
|
||||
listener: (this: WebSocket, ev: WebSocketEventMap[K]) => any,
|
||||
options?: boolean | AddEventListenerOptions
|
||||
): void {
|
||||
this.ws.addEventListener(type, listener, options);
|
||||
}
|
||||
|
||||
removeEventListener<K extends keyof WebSocketEventMap>(
|
||||
type: K,
|
||||
listener: (this: WebSocket, ev: WebSocketEventMap[K]) => any,
|
||||
options?: boolean | EventListenerOptions
|
||||
): void {
|
||||
this.ws.removeEventListener(type, listener, options);
|
||||
}
|
||||
```
|
||||
|
||||
**重要性:**
|
||||
|
||||
1. **完整的API兼容性**:某些业务代码可能使用addEventListener而不是onXXX
|
||||
2. **事件管理**:支持多个监听器
|
||||
3. **标准兼容**:遵循WebSocket标准API
|
||||
|
||||
**类型安全:**
|
||||
|
||||
- 使用泛型 `<K extends keyof WebSocketEventMap>` 确保事件类型正确
|
||||
- listener参数的类型根据事件类型自动推导
|
||||
|
||||
### 9. 心跳检测机制
|
||||
|
||||
#### 心跳超时检测逻辑
|
||||
|
||||
```typescript
|
||||
// 发送心跳时,只在没有超时检测时才设置新的
|
||||
this.heartbeatTimer = setInterval(() => {
|
||||
if (this.ws.readyState === WebSocket.OPEN) {
|
||||
this.ws.send(WS_CONFIG.heartbeatMessage);
|
||||
|
||||
if (!this.heartbeatTimeoutTimer) {
|
||||
// 关键:避免重复设置
|
||||
this.startHeartbeatTimeout();
|
||||
}
|
||||
}
|
||||
}, WS_CONFIG.heartbeatInterval);
|
||||
```
|
||||
|
||||
**设计要点:**
|
||||
|
||||
1. **避免重复设置**:只有在没有超时检测时才设置新的
|
||||
2. **超时逻辑**:设定时间内没收到响应就断开连接
|
||||
3. **状态同步**:收到响应时清除超时检测
|
||||
|
||||
#### 心跳响应处理
|
||||
|
||||
```typescript
|
||||
// 检查是否为心跳响应(支持字符串和JSON格式)
|
||||
let isHeartbeatResponse = false;
|
||||
|
||||
// 1. 检查简单字符串格式
|
||||
if (typeof messageData === 'string' && messageData === WS_CONFIG.heartbeatResponseType) {
|
||||
isHeartbeatResponse = true;
|
||||
}
|
||||
|
||||
// 2. 检查JSON格式
|
||||
if (!isHeartbeatResponse && typeof messageData === 'string') {
|
||||
try {
|
||||
const data = JSON.parse(messageData);
|
||||
if (data.type === WS_CONFIG.heartbeatResponseType) {
|
||||
isHeartbeatResponse = true;
|
||||
}
|
||||
} catch (e) {
|
||||
// JSON解析失败,不是JSON格式的心跳响应
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**兼容性设计**:支持两种心跳响应格式,适应不同的服务器实现。
|
||||
|
||||
### 10. 重连机制
|
||||
|
||||
#### 指数退避算法
|
||||
|
||||
```typescript
|
||||
private scheduleReconnect(): void {
|
||||
if (this.isManualClose || this.reconnectAttempts >= WS_CONFIG.maxReconnectAttempts) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.reconnectAttempts++;
|
||||
|
||||
// 指数退避重连策略
|
||||
const delay = Math.min(
|
||||
WS_CONFIG.reconnectBaseDelay * Math.pow(2, this.reconnectAttempts - 1),
|
||||
WS_CONFIG.maxReconnectDelay
|
||||
);
|
||||
|
||||
this.reconnectTimer = setTimeout(() => {
|
||||
this.reconnect();
|
||||
}, delay);
|
||||
}
|
||||
```
|
||||
|
||||
**算法解释:**
|
||||
|
||||
- 第1次重连:1000ms 后
|
||||
- 第2次重连:2000ms 后
|
||||
- 第3次重连:4000ms 后
|
||||
- 第4次重连:8000ms 后
|
||||
- 第5次重连:16000ms 后(受maxReconnectDelay限制,实际为30000ms)
|
||||
|
||||
**设计考虑:**
|
||||
|
||||
1. **指数退避**:避免对服务器造成压力
|
||||
2. **最大延迟限制**:防止延迟过长
|
||||
3. **次数限制**:避免无限重连
|
||||
4. **服务器友好**:给服务器恢复时间
|
||||
|
||||
### 11. 类型安全设计
|
||||
|
||||
#### 严格的类型定义
|
||||
|
||||
```typescript
|
||||
// 事件处理器类型
|
||||
private userOnMessage: ((event: MessageEvent) => void) | null = null;
|
||||
private userOnClose: ((event: CloseEvent) => void) | null = null;
|
||||
private userOnError: ((event: Event) => void) | null = null;
|
||||
private userOnOpen: ((event: Event) => void) | null = null;
|
||||
```
|
||||
|
||||
**类型安全的好处:**
|
||||
|
||||
1. **编译时检查**:在编译时捕获类型错误
|
||||
2. **IDE支持**:更好的自动补全和错误提示
|
||||
3. **重构安全**:类型系统确保重构的正确性
|
||||
|
||||
#### 泛型的使用
|
||||
|
||||
```typescript
|
||||
addEventListener<K extends keyof WebSocketEventMap>(
|
||||
type: K,
|
||||
listener: (this: WebSocket, ev: WebSocketEventMap[K]) => any,
|
||||
options?: boolean | AddEventListenerOptions
|
||||
): void
|
||||
```
|
||||
|
||||
**泛型的价值:**
|
||||
|
||||
- `K extends keyof WebSocketEventMap`:确保事件类型只能是WebSocket支持的类型
|
||||
- `ev: WebSocketEventMap[K]`:根据事件类型自动推导事件对象类型
|
||||
|
||||
### 12. 资源管理
|
||||
|
||||
#### 完整的清理机制
|
||||
|
||||
```typescript
|
||||
close(code?: number, reason?: string): void {
|
||||
console.log(`手动关闭WebSocket: ${this.path}`);
|
||||
this.isManualClose = true;
|
||||
this.isHeartbeatTimeout = false; // 重置心跳超时标志
|
||||
this.stopHeartbeat(); // 清理心跳定时器
|
||||
this.clearReconnectTimer(); // 清理重连定时器
|
||||
this.ws.close(code, reason); // 关闭实际连接
|
||||
}
|
||||
```
|
||||
|
||||
**资源清理的重要性:**
|
||||
|
||||
1. **内存泄漏预防**:确保所有定时器都被清理
|
||||
2. **状态一致性**:重置所有状态标志
|
||||
3. **优雅关闭**:按正确顺序清理资源
|
||||
|
||||
## 配置设计
|
||||
|
||||
### 全局配置对象
|
||||
|
||||
```typescript
|
||||
const WS_CONFIG = {
|
||||
heartbeatInterval: 3000, // 心跳间隔
|
||||
heartbeatTimeout: 5000, // 心跳响应超时时间
|
||||
maxReconnectAttempts: 5, // 最大重连次数
|
||||
reconnectBaseDelay: 1000, // 重连基础延迟
|
||||
maxReconnectDelay: 30000, // 最大重连延迟
|
||||
heartbeatMessage: 'ping', // 心跳消息
|
||||
heartbeatResponseType: 'pong', // 心跳响应类型
|
||||
};
|
||||
```
|
||||
|
||||
**配置设计原则:**
|
||||
|
||||
1. **集中管理**:所有配置在一个地方,易于维护
|
||||
2. **合理默认值**:开箱即用的配置
|
||||
3. **易于调整**:生产环境可以快速调整参数
|
||||
4. **文档化**:每个配置都有清晰的注释
|
||||
|
||||
## 接口兼容性
|
||||
|
||||
### 原有接口保持不变
|
||||
|
||||
```typescript
|
||||
// 原有接口
|
||||
function create(path: string): Promise<WebSocket> {
|
||||
const baseUrl = import.meta.env.ENV_WEBSOCKET_BASE ?? '';
|
||||
const ws = new EnhancedWebSocket(path, baseUrl) as any;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
ws.close();
|
||||
reject(new Error('WebSocket connection timeout'));
|
||||
}, 10000);
|
||||
|
||||
ws.addEventListener('open', () => {
|
||||
clearTimeout(timeout);
|
||||
resolve(ws); // 返回增强的WebSocket,但类型为WebSocket
|
||||
});
|
||||
|
||||
ws.addEventListener('error', (e: any) => {
|
||||
clearTimeout(timeout);
|
||||
reject(e);
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**兼容性保证:**
|
||||
|
||||
1. **相同的函数签名**:`create(path: string): Promise<WebSocket>`
|
||||
2. **相同的返回类型**:返回Promise<WebSocket>
|
||||
3. **相同的使用方式**:业务代码无需任何修改
|
||||
|
||||
## 总结
|
||||
|
||||
### 技术选择总结
|
||||
|
||||
| 技术选择 | 原因 | 替代方案 | 为什么不选择替代方案 |
|
||||
| ------------- | -------------------------------- | ------------ | ----------------------- |
|
||||
| Class | 状态封装、方法绑定、生命周期管理 | 函数+闭包 | 复杂度高,类型支持差 |
|
||||
| 包装器模式 | 控制创建时机、事件拦截 | 继承 | 无法在事件设置前拦截 |
|
||||
| Private成员 | 封装、API稳定性、状态保护 | Public成员 | 容易被误用,状态不安全 |
|
||||
| Getter/Setter | 透明代理、API兼容性 | 直接方法 | 不符合WebSocket API习惯 |
|
||||
| 多定时器 | 职责分离、精确控制 | 单定时器 | 逻辑混乱,难以维护 |
|
||||
| 状态标志 | 精确控制重连逻辑 | 仅依赖状态码 | WebSocket状态码限制多 |
|
||||
|
||||
### 架构优势
|
||||
|
||||
1. **零侵入性**:业务代码完全无需修改
|
||||
2. **高可靠性**:多重保障确保连接稳定
|
||||
3. **高可维护性**:清晰的架构和完整的类型支持
|
||||
4. **高性能**:最小的性能开销
|
||||
5. **高扩展性**:易于添加新功能
|
||||
|
||||
### 最佳实践体现
|
||||
|
||||
1. **单一职责原则**:每个方法只负责一个功能
|
||||
2. **开闭原则**:对扩展开放,对修改封闭
|
||||
3. **依赖倒置原则**:依赖抽象(接口)而非具体实现
|
||||
4. **接口隔离原则**:用户只看到需要的接口
|
||||
5. **里氏替换原则**:增强版本完全可以替换原版本
|
||||
|
||||
这个实现展示了如何在保持向后兼容的同时,提供企业级的功能增强,是一个很好的渐进式增强的例子。
|
@ -1,719 +0,0 @@
|
||||
# AMR调度系统技术交付文档
|
||||
|
||||
## 文档信息
|
||||
|
||||
- **项目名称**: AMR调度系统 (arm_system)
|
||||
- **版本**: 1.0.0
|
||||
- **技术栈**: Vue 3 + TypeScript + Vite + Ant Design Vue + Meta2D
|
||||
- **文档版本**: v1.0
|
||||
|
||||
---
|
||||
|
||||
## 1. 项目概述
|
||||
|
||||
### 1.1 项目简介
|
||||
|
||||
AMR调度系统是一个基于Web的智能仓储解决方案,集成了场景编辑器、机器人监控、运动仿真等核心功能。系统采用现代化的前端技术架构,提供直观的可视化界面和强大的编辑功能。
|
||||
|
||||
### 1.2 核心特性
|
||||
|
||||
- **场景编辑器**: 支持点位、路线、区域的创建和编辑
|
||||
- **机器人监控**: 实时监控AMR机器人状态和运动轨迹
|
||||
- **运动仿真**: 提供机器人运动路径的仿真和验证
|
||||
- **库位管理**: 智能管理仓储库位状态和分配
|
||||
- **多主题支持**: 支持明暗主题切换
|
||||
- **国际化**: 支持中英文双语界面
|
||||
|
||||
### 1.3 技术亮点
|
||||
|
||||
- 基于Meta2D引擎的2D图形渲染
|
||||
- WebSocket实时数据通信
|
||||
- 响应式设计,支持多种设备
|
||||
- 模块化架构,易于扩展和维护
|
||||
|
||||
---
|
||||
|
||||
## 2. 技术架构
|
||||
|
||||
### 2.1 技术栈详情
|
||||
|
||||
#### 前端框架
|
||||
|
||||
- **Vue 3.5.13**: 采用Composition API,提供响应式数据绑定
|
||||
- **TypeScript 5.7.2**: 强类型支持,提升代码质量和开发效率
|
||||
- **Vite 6.3.1**: 现代化构建工具,支持快速开发和热重载
|
||||
|
||||
#### UI组件库
|
||||
|
||||
- **Ant Design Vue 4.2.6**: 企业级UI组件库
|
||||
- **@ant-design/icons-vue 7.0.1**: 图标组件库
|
||||
|
||||
#### 图形引擎
|
||||
|
||||
- **@meta2d/core 1.0.78**: 2D图形渲染引擎,支持复杂图形绘制
|
||||
|
||||
#### 状态管理
|
||||
|
||||
- **RxJS 7.8.2**: 响应式编程库,处理异步数据流
|
||||
- **@vueuse/rxjs 13.1.0**: Vue与RxJS的集成工具
|
||||
|
||||
#### 样式处理
|
||||
|
||||
- **Sass**: CSS预处理器,支持变量、嵌套、混合等特性
|
||||
- **SCSS**: Sass的SCSS语法
|
||||
|
||||
#### 开发工具
|
||||
|
||||
- **ESLint 9.25.0**: 代码质量检查
|
||||
- **Prettier 3.5.3**: 代码格式化
|
||||
- **Stylelint 16.19.1**: 样式代码检查
|
||||
|
||||
### 2.2 项目结构
|
||||
|
||||
```
|
||||
web-amr/
|
||||
├── src/ # 源代码目录
|
||||
│ ├── apis/ # API接口定义
|
||||
│ │ ├── map/ # 地图相关API
|
||||
│ │ ├── robot/ # 机器人相关API
|
||||
│ │ └── scene/ # 场景相关API
|
||||
│ ├── assets/ # 静态资源
|
||||
│ │ ├── fonts/ # 字体文件
|
||||
│ │ ├── icons/ # 图标资源
|
||||
│ │ ├── images/ # 图片资源
|
||||
│ │ ├── locales/ # 国际化文件
|
||||
│ │ └── themes/ # 主题配置
|
||||
│ ├── components/ # 公共组件
|
||||
│ ├── pages/ # 页面组件
|
||||
│ ├── services/ # 核心服务
|
||||
│ └── workers/ # Web Worker
|
||||
├── public/ # 公共资源
|
||||
├── docs/ # 项目文档
|
||||
├── mocks/ # 模拟数据
|
||||
└── dist/ # 构建输出
|
||||
```
|
||||
|
||||
### 2.3 核心服务架构
|
||||
|
||||
#### 编辑器服务 (EditorService)
|
||||
|
||||
- 继承自Meta2D引擎
|
||||
- 管理场景文件的加载、保存
|
||||
- 处理点位、路线、区域的创建和编辑
|
||||
- 管理机器人组和实时状态更新
|
||||
|
||||
#### WebSocket服务 (EnhancedWebSocket)
|
||||
|
||||
- 支持心跳检测和自动重连
|
||||
- 处理实时数据通信
|
||||
- 优化连接稳定性和性能
|
||||
|
||||
#### 图层管理服务 (LayerManagerService)
|
||||
|
||||
- 管理不同图层的显示和隐藏
|
||||
- 控制图层的层级顺序
|
||||
- 提供图层操作接口
|
||||
|
||||
#### 区域操作服务 (AreaOperationService)
|
||||
|
||||
- 处理区域的选择、编辑、删除等操作
|
||||
- 管理区域与点位的关联关系
|
||||
|
||||
---
|
||||
|
||||
## 3. 功能模块详解
|
||||
|
||||
### 3.1 场景编辑器模块
|
||||
|
||||
#### 3.1.1 核心功能
|
||||
|
||||
- **场景加载**: 支持JSON格式场景文件的导入和加载
|
||||
- **场景保存**: 将编辑后的场景保存为JSON格式
|
||||
- **场景推送**: 将场景配置推送到后端系统
|
||||
|
||||
#### 3.1.2 编辑功能
|
||||
|
||||
- **点位管理**: 创建、编辑、删除各种类型的点位
|
||||
- **路线绘制**: 支持直线、贝塞尔曲线等路线类型
|
||||
- **区域定义**: 创建和管理仓储区域
|
||||
- **机器人配置**: 设置机器人组和机器人参数
|
||||
|
||||
#### 3.1.3 技术实现
|
||||
|
||||
```typescript
|
||||
// 场景加载示例
|
||||
public async load(
|
||||
map?: string,
|
||||
editable = false,
|
||||
detail?: Partial<GroupSceneDetail>,
|
||||
isImport = false,
|
||||
): Promise<void> {
|
||||
const scene: StandardScene = map ? JSON.parse(map) : {};
|
||||
// 加载机器人组和机器人信息
|
||||
this.#loadRobots(scene.robotGroups, scene.robots);
|
||||
// 加载场景点位
|
||||
await this.#loadScenePoints(scene.points, isImport);
|
||||
// 加载场景路线
|
||||
this.#loadSceneRoutes(scene.routes, isImport);
|
||||
// 加载场景区域
|
||||
await this.#loadSceneAreas(scene.areas, isImport);
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 机器人监控模块
|
||||
|
||||
#### 3.2.1 监控功能
|
||||
|
||||
- **实时位置**: 显示机器人的实时坐标和角度
|
||||
- **状态监控**: 监控机器人的运行状态、电量、故障等
|
||||
- **路径显示**: 可视化机器人的运动路径
|
||||
- **告警提示**: 显示机器人的告警和故障信息
|
||||
|
||||
#### 3.2.2 数据通信
|
||||
|
||||
- **WebSocket连接**: 建立与后端的实时数据连接
|
||||
- **心跳检测**: 定期发送心跳包,确保连接稳定
|
||||
- **自动重连**: 连接断开时自动尝试重连
|
||||
|
||||
#### 3.2.3 性能优化
|
||||
|
||||
```typescript
|
||||
// 使用requestAnimationFrame优化渲染性能
|
||||
const batchUpdateRobots = (updates: Array<{ id: string; data: RobotRealtimeInfo }>) => {
|
||||
if (!editor.value || updates.length === 0) return;
|
||||
|
||||
// 批量更新机器人数据,减少渲染调用次数
|
||||
updates.forEach(({ id, data }) => {
|
||||
const { x, y, active, angle, path: points, isWaring, isFault, ...rest } = data;
|
||||
editor.value?.updateRobot(id, rest);
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
### 3.3 运动仿真模块
|
||||
|
||||
#### 3.3.1 仿真功能
|
||||
|
||||
- **路径规划**: 模拟机器人的运动路径
|
||||
- **碰撞检测**: 检测机器人运动过程中的潜在碰撞
|
||||
- **时间估算**: 计算机器人到达目标点的时间
|
||||
- **场景验证**: 验证场景配置的合理性
|
||||
|
||||
#### 3.3.2 仿真算法
|
||||
|
||||
- **A\*算法**: 用于路径规划
|
||||
- **贝塞尔曲线**: 用于平滑路径生成
|
||||
- **物理引擎**: 模拟机器人的运动物理特性
|
||||
|
||||
### 3.4 库位管理模块
|
||||
|
||||
#### 3.4.1 库位功能
|
||||
|
||||
- **库位状态**: 管理库位的占用、锁定、禁用等状态
|
||||
- **库位分配**: 智能分配库位给机器人
|
||||
- **库位监控**: 实时监控库位的使用情况
|
||||
- **库位统计**: 提供库位使用率等统计信息
|
||||
|
||||
#### 3.4.2 数据结构
|
||||
|
||||
```typescript
|
||||
export interface StorageLocationInfo {
|
||||
id: string; // 库位ID
|
||||
layer_index: number; // 层索引
|
||||
layer_name: string; // 层名称
|
||||
operate_point_id: string; // 操作点ID
|
||||
station_name: string; // 站点名称
|
||||
scene_id: string; // 场景ID
|
||||
storage_area_id: string; // 库区ID
|
||||
area_name: string; // 库区名称
|
||||
is_occupied: boolean; // 是否占用
|
||||
is_locked: boolean; // 是否锁定
|
||||
is_disabled: boolean; // 是否禁用
|
||||
goods_content: string; // 货物内容
|
||||
goods_weight: number | null; // 货物重量
|
||||
goods_volume: number | null; // 货物体积
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 数据模型
|
||||
|
||||
### 4.1 场景数据模型
|
||||
|
||||
#### 4.1.1 标准场景结构
|
||||
|
||||
```typescript
|
||||
export interface StandardScene {
|
||||
scale?: number; // 缩放比例
|
||||
origin?: { x: number; y: number }; // 默认载入原点
|
||||
robotGroups?: Array<RobotGroup>; // 机器人组信息
|
||||
robots?: Array<RobotInfo>; // 机器人信息
|
||||
points?: Array<StandardScenePoint>; // 标准点位信息
|
||||
routes?: Array<StandardSceneRoute>; // 标准线路信息
|
||||
areas?: Array<StandardSceneArea>; // 标准区域信息
|
||||
width?: number; // 场景宽度
|
||||
height?: number; // 场景高度
|
||||
ratio?: number; // 坐标缩放比例
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.1.2 点位数据结构
|
||||
|
||||
```typescript
|
||||
export interface StandardScenePoint {
|
||||
id: string; // 点位ID
|
||||
name: string; // 点位名称
|
||||
desc?: string; // 描述
|
||||
x: number; // X坐标
|
||||
y: number; // Y坐标
|
||||
type: number; // 点位类型
|
||||
extensionType?: number; // 扩展类型
|
||||
robots?: Array<string>; // 绑定机器人ID集合
|
||||
actions?: Array<string>; // 绑定动作点ID集合
|
||||
associatedStorageLocations?: string[]; // 库位名称
|
||||
deviceId?: string; // 设备ID
|
||||
enabled?: 0 | 1; // 是否启用
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 机器人数据模型
|
||||
|
||||
#### 4.2.1 机器人信息
|
||||
|
||||
```typescript
|
||||
export interface RobotInfo {
|
||||
gid?: string; // 机器人组ID
|
||||
id: string; // 机器人ID
|
||||
label: string; // 机器人名称
|
||||
brand: RobotBrand; // 机器人品牌
|
||||
type: RobotType; // 机器人类型
|
||||
ip?: string; // 机器人IP地址
|
||||
battery?: number; // 机器人电量
|
||||
isConnected?: boolean; // 连接状态
|
||||
state?: RobotState; // 机器人状态
|
||||
canOrder?: boolean; // 接单状态
|
||||
canStop?: boolean; // 急停状态
|
||||
canControl?: boolean; // 控制状态
|
||||
targetPoint?: string; // 目标点位
|
||||
isLoading?: 0 | 1; // 载货状态
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.2.2 实时机器人信息
|
||||
|
||||
```typescript
|
||||
export interface RobotRealtimeInfo extends RobotInfo {
|
||||
x: number; // 实时X坐标
|
||||
y: number; // 实时Y坐标
|
||||
active?: boolean; // 是否运行
|
||||
angle?: number; // 旋转角度
|
||||
path?: Array<Point>; // 规划路径
|
||||
isWaring?: boolean; // 是否告警
|
||||
isFault?: boolean; // 是否故障
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 地图数据模型
|
||||
|
||||
#### 4.3.1 地图元素
|
||||
|
||||
```typescript
|
||||
export interface MapPen extends Pen {
|
||||
label?: string; // 展示名称
|
||||
desc?: string; // 描述
|
||||
point?: MapPointInfo; // 点位信息
|
||||
route?: MapRouteInfo; // 线路信息
|
||||
area?: MapAreaInfo; // 区域信息
|
||||
robot?: MapRobotInfo; // 实时机器人信息
|
||||
attrs?: Record<string, unknown>; // 额外属性
|
||||
activeAttrs?: Array<string>; // 已激活的额外属性
|
||||
properties?: unknown; // 第三方附加参数
|
||||
statusStyle?: string; // 状态颜色
|
||||
strokeStyle?: string; // 边框颜色
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. API接口设计
|
||||
|
||||
### 5.1 场景管理API
|
||||
|
||||
#### 5.1.1 场景操作
|
||||
|
||||
- `GET /api/scene/{id}`: 获取场景详情
|
||||
- `POST /api/scene/{id}`: 保存场景配置
|
||||
- `PUT /api/scene/{id}/push`: 推送场景配置
|
||||
- `GET /api/scene/group/{groupId}`: 获取群组场景
|
||||
|
||||
#### 5.1.2 场景监控
|
||||
|
||||
- `GET /ws/scene/{id}/monitor`: 监控场景实时数据
|
||||
- `GET /ws/scene/{id}/simulation`: 仿真场景数据
|
||||
|
||||
### 5.2 机器人管理API
|
||||
|
||||
#### 5.2.1 机器人信息
|
||||
|
||||
- `GET /api/robot/group/{groupId}`: 获取机器人组信息
|
||||
- `GET /api/robot/{id}`: 获取机器人详情
|
||||
- `POST /api/robot/register`: 注册机器人
|
||||
- `PUT /api/robot/{id}/seize`: 占用机器人
|
||||
|
||||
#### 5.2.2 机器人同步
|
||||
|
||||
- `POST /api/robot/sync/group/{groupId}`: 同步机器人组数据
|
||||
- `GET /ws/robot/{id}/status`: 获取机器人实时状态
|
||||
|
||||
### 5.3 地图管理API
|
||||
|
||||
#### 5.3.1 地图数据
|
||||
|
||||
- `GET /api/map/area/{id}`: 获取区域信息
|
||||
- `POST /api/map/area`: 创建区域
|
||||
- `PUT /api/map/area/{id}`: 更新区域
|
||||
- `DELETE /api/map/area/{id}`: 删除区域
|
||||
|
||||
---
|
||||
|
||||
## 6. 部署说明
|
||||
|
||||
### 6.1 环境要求
|
||||
|
||||
#### 6.1.1 开发环境
|
||||
|
||||
- **Node.js**: >= 18.0.0
|
||||
- **包管理器**: pnpm >= 8.0.0
|
||||
- **浏览器**: Chrome >= 90, Firefox >= 88, Safari >= 14
|
||||
|
||||
#### 6.1.2 生产环境
|
||||
|
||||
- **Web服务器**: Nginx >= 1.18.0 或 Apache >= 2.4
|
||||
- **HTTPS**: 支持SSL证书(推荐)
|
||||
- **CDN**: 可选,用于静态资源加速
|
||||
|
||||
### 6.2 构建部署
|
||||
|
||||
#### 6.2.1 开发环境启动
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
pnpm install
|
||||
|
||||
# 启动开发服务器
|
||||
pnpm start
|
||||
|
||||
# 访问地址: http://localhost:8888/web-amr
|
||||
```
|
||||
|
||||
#### 6.2.2 生产环境构建
|
||||
|
||||
```bash
|
||||
# 构建生产版本
|
||||
pnpm build
|
||||
|
||||
# 构建输出目录: dist/
|
||||
```
|
||||
|
||||
#### 6.2.3 部署配置
|
||||
|
||||
```nginx
|
||||
# Nginx配置示例
|
||||
server {
|
||||
listen 80;
|
||||
server_name your-domain.com;
|
||||
|
||||
location /web-amr {
|
||||
alias /path/to/dist;
|
||||
try_files $uri $uri/ /web-amr/index.html;
|
||||
|
||||
# 静态资源缓存
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
}
|
||||
|
||||
# API代理
|
||||
location /api/ {
|
||||
proxy_pass http://backend-server:8080/jeecg-boot/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
|
||||
# WebSocket代理
|
||||
location /ws/ {
|
||||
proxy_pass http://backend-server:8080/jeecg-boot/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.3 环境变量配置
|
||||
|
||||
#### 6.3.1 开发环境
|
||||
|
||||
```bash
|
||||
# .env.development
|
||||
VITE_API_BASE_URL=http://localhost:8080/jeecg-boot
|
||||
VITE_WS_BASE_URL=ws://localhost:8080/jeecg-boot
|
||||
VITE_APP_BASE_URL=/web-amr
|
||||
```
|
||||
|
||||
#### 6.3.2 生产环境
|
||||
|
||||
```bash
|
||||
# .env.production
|
||||
VITE_API_BASE_URL=https://your-domain.com/api
|
||||
VITE_WS_BASE_URL=wss://your-domain.com/ws
|
||||
VITE_APP_BASE_URL=/web-amr
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 性能优化
|
||||
|
||||
### 7.1 前端优化
|
||||
|
||||
#### 7.1.1 代码分割
|
||||
|
||||
- 使用Vite的动态导入实现路由级别的代码分割
|
||||
- 组件级别的懒加载,减少初始包大小
|
||||
|
||||
#### 7.1.2 渲染优化
|
||||
|
||||
- 使用`requestAnimationFrame`优化高频数据渲染
|
||||
- 批量更新DOM,减少重绘和回流
|
||||
- 虚拟滚动处理大量数据
|
||||
|
||||
#### 7.1.3 缓存策略
|
||||
|
||||
- 静态资源长期缓存
|
||||
- API响应数据缓存
|
||||
- 浏览器本地存储优化
|
||||
|
||||
### 7.2 网络优化
|
||||
|
||||
#### 7.2.1 WebSocket优化
|
||||
|
||||
- 心跳检测机制,及时发现问题
|
||||
- 自动重连策略,提高连接稳定性
|
||||
- 数据压缩,减少传输量
|
||||
|
||||
#### 7.2.2 HTTP优化
|
||||
|
||||
- 请求合并,减少网络请求次数
|
||||
- 响应数据压缩
|
||||
- 合理的缓存策略
|
||||
|
||||
---
|
||||
|
||||
## 8. 安全考虑
|
||||
|
||||
### 8.1 前端安全
|
||||
|
||||
#### 8.1.1 输入验证
|
||||
|
||||
- 所有用户输入进行验证和过滤
|
||||
- 防止XSS攻击
|
||||
- 防止CSRF攻击
|
||||
|
||||
#### 8.1.2 权限控制
|
||||
|
||||
- 基于角色的访问控制
|
||||
- 前端路由权限验证
|
||||
- API接口权限校验
|
||||
|
||||
### 8.2 通信安全
|
||||
|
||||
#### 8.2.1 HTTPS/WSS
|
||||
|
||||
- 生产环境强制使用HTTPS
|
||||
- WebSocket使用WSS协议
|
||||
- 证书有效期监控
|
||||
|
||||
#### 8.2.2 数据加密
|
||||
|
||||
- 敏感数据加密传输
|
||||
- 本地存储数据加密
|
||||
- API密钥安全管理
|
||||
|
||||
---
|
||||
|
||||
## 9. 监控与日志
|
||||
|
||||
### 9.1 性能监控
|
||||
|
||||
#### 9.1.1 前端监控
|
||||
|
||||
- 页面加载性能
|
||||
- 接口响应时间
|
||||
- 用户交互响应时间
|
||||
- 错误率统计
|
||||
|
||||
#### 9.1.2 业务监控
|
||||
|
||||
- 用户活跃度
|
||||
- 功能使用率
|
||||
- 系统可用性
|
||||
|
||||
### 9.2 日志管理
|
||||
|
||||
#### 9.2.1 日志级别
|
||||
|
||||
- ERROR: 错误信息
|
||||
- WARN: 警告信息
|
||||
- INFO: 一般信息
|
||||
- DEBUG: 调试信息
|
||||
|
||||
#### 9.2.2 日志内容
|
||||
|
||||
- 时间戳
|
||||
- 日志级别
|
||||
- 模块名称
|
||||
- 详细描述
|
||||
- 错误堆栈(错误日志)
|
||||
|
||||
---
|
||||
|
||||
## 10. 维护与支持
|
||||
|
||||
### 10.1 日常维护
|
||||
|
||||
#### 10.1.1 代码维护
|
||||
|
||||
- 定期代码审查
|
||||
- 依赖包更新
|
||||
- 代码重构优化
|
||||
- 性能监控和调优
|
||||
|
||||
#### 10.1.2 系统维护
|
||||
|
||||
- 日志清理
|
||||
- 缓存清理
|
||||
- 数据库优化
|
||||
- 服务器资源监控
|
||||
|
||||
### 10.2 故障处理
|
||||
|
||||
#### 10.2.1 常见问题
|
||||
|
||||
- WebSocket连接断开
|
||||
- 数据加载失败
|
||||
- 页面渲染异常
|
||||
- 性能问题
|
||||
|
||||
#### 10.2.2 解决方案
|
||||
|
||||
- 自动重连机制
|
||||
- 错误重试策略
|
||||
- 降级处理方案
|
||||
- 用户友好的错误提示
|
||||
|
||||
---
|
||||
|
||||
## 11. 扩展开发
|
||||
|
||||
### 11.1 新功能开发
|
||||
|
||||
#### 11.1.1 开发流程
|
||||
|
||||
1. 需求分析和设计
|
||||
2. 接口设计和开发
|
||||
3. 前端组件开发
|
||||
4. 测试和调试
|
||||
5. 部署和上线
|
||||
|
||||
#### 11.1.2 技术规范
|
||||
|
||||
- 遵循现有代码风格
|
||||
- 使用TypeScript强类型
|
||||
- 编写单元测试
|
||||
- 更新相关文档
|
||||
|
||||
### 11.2 第三方集成
|
||||
|
||||
#### 11.2.1 地图服务
|
||||
|
||||
- 支持多种地图提供商
|
||||
- 地图数据格式转换
|
||||
- 坐标系统统一
|
||||
|
||||
#### 11.2.2 设备集成
|
||||
|
||||
- 支持多种机器人品牌
|
||||
- 标准化通信协议
|
||||
- 设备状态监控
|
||||
|
||||
---
|
||||
|
||||
## 12. 项目总结
|
||||
|
||||
### 12.1 技术优势
|
||||
|
||||
#### 12.1.1 架构优势
|
||||
|
||||
- 模块化设计,易于维护和扩展
|
||||
- 响应式架构,支持高并发
|
||||
- 现代化技术栈,开发效率高
|
||||
|
||||
#### 12.1.2 功能优势
|
||||
|
||||
- 完整的仓储管理解决方案
|
||||
- 直观的可视化界面
|
||||
- 强大的编辑和仿真功能
|
||||
|
||||
### 12.2 应用价值
|
||||
|
||||
#### 12.2.1 业务价值
|
||||
|
||||
- 提高仓储管理效率
|
||||
- 降低人工成本
|
||||
- 提升系统可靠性
|
||||
|
||||
#### 12.2.2 技术价值
|
||||
|
||||
- 可复用的技术架构
|
||||
- 标准化的开发流程
|
||||
- 完善的文档体系
|
||||
|
||||
---
|
||||
|
||||
## 附录
|
||||
|
||||
### A. 依赖包版本清单
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"@ant-design/icons-vue": "^7.0.1",
|
||||
"@meta2d/core": "^1.0.78",
|
||||
"@vueuse/rxjs": "^13.1.0",
|
||||
"ant-design-vue": "^4.2.6",
|
||||
"axios": "^1.8.4",
|
||||
"dayjs": "^1.11.13",
|
||||
"lodash-es": "^4.17.21",
|
||||
"rxjs": "^7.8.2",
|
||||
"vue": "^3.5.13",
|
||||
"vue-i18n": "^11.1.3",
|
||||
"vue-router": "^4.5.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### B. 浏览器兼容性
|
||||
|
||||
- Chrome >= 90
|
||||
- Firefox >= 88
|
||||
- Safari >= 14
|
||||
- Edge >= 90
|
||||
|
||||
---
|
||||
|
||||
**文档版本**: v1.0
|
||||
**最后更新**: 2025年8月28日10:25:21
|
||||
**文档状态**: 已完成
|
2013
docs/场景编辑器组件详细分析.md
2013
docs/场景编辑器组件详细分析.md
File diff suppressed because it is too large
Load Diff
@ -1,98 +0,0 @@
|
||||
# 库位功能逻辑分析
|
||||
|
||||
## 1. 概述
|
||||
|
||||
本文档旨在详细解析项目中“库位”(Storage Location)功能的实现逻辑。该功能的核心目标是实时监控与地图中“动作点”关联的库位状态,并将这些状态直观地反馈在前端界面上,包括更新画布上点的边框颜色和在详情卡片中展示详细的库位信息。
|
||||
|
||||
整个功能逻辑主要由 **`StorageLocationService`** 服务和 **`PointDetailCard`** UI组件协作完成,数据通过 WebSocket 从后端实时推送。
|
||||
|
||||
## 2. 核心服务: `StorageLocationService`
|
||||
|
||||
`StorageLocationService` 是库位管理的中枢,封装了所有核心业务逻辑。它在 `movement-supervision.vue` 页面中被实例化和管理。
|
||||
|
||||
**文件路径:** `src/services/storage-location.service.ts`
|
||||
|
||||
### 主要职责
|
||||
|
||||
1. **WebSocket 通信**:
|
||||
|
||||
- 通过调用 `@api/scene` 中的 `monitorStorageLocationById` 方法,建立一个 WebSocket 连接来接收实时的库位状态更新。
|
||||
- 连接建立后,会主动向后端请求一次全量的库位状态数据。
|
||||
|
||||
2. **数据处理与状态管理**:
|
||||
|
||||
- 维护一个核心数据结构 `storageLocations: Ref<Map<string, StorageLocationInfo[]>>`。这是一个响应式的 Map,其中:
|
||||
- `key`: 画布中“动作点”的 ID (`pointId`)。
|
||||
- `value`: 一个数组,包含所有绑定到该动作点的库位信息 (`StorageLocationInfo[]`)。
|
||||
- 通过 `handleStorageLocationUpdate` 方法处理来自 WebSocket 的两种消息:
|
||||
- `storage_location_update`: 用于全量更新所有库位信息。
|
||||
- `storage_location_status_change`: 用于更新单个库位的状态,实现增量更新。
|
||||
|
||||
3. **与编辑器 (`EditorService`) 的交互**:
|
||||
- **ID 映射**: 通过 `buildStationToPointIdMap` 方法,将后端数据中的 `station_name` (如 "AP9") 与画布中“动作点”的唯一 `id` (如 "3351") 进行映射。这是连接后端逻辑与前端视觉呈现的关键桥梁。
|
||||
- **视觉状态更新**: 实现 `updatePointBorderColor` 方法,根据一个点所关联的所有库位的占用状态 (`is_occupied`),动态更新该点在画布上的边框颜色:
|
||||
- **全部占用**: 红色 (`#ff4d4f`)
|
||||
- **部分或全部未占用**: 绿色 (`#52c41a`)
|
||||
|
||||
### 对外接口
|
||||
|
||||
- `getLocationsByPointId(pointId: string)`: 向外部组件提供一个接口,用于根据“动作点”的 ID 获取其关联的所有库位的详细信息数组。
|
||||
|
||||
## 3. UI 展示: `PointDetailCard`
|
||||
|
||||
当用户在 `movement-supervision.vue` 页面中选中一个“动作点”时,`PointDetailCard` 组件会负责展示该点的详细信息,其中就包括了库位的实时状态。
|
||||
|
||||
**文件路径:** `src/components/card/point-detail-card.vue`
|
||||
|
||||
### 主要职责
|
||||
|
||||
1. **接收数据**:
|
||||
|
||||
- 通过 `props` 从父组件接收 `storageLocations` 数组。该数组的数据源是 `StorageLocationService.getLocationsByPointId()` 方法的返回值。
|
||||
|
||||
2. **渲染库位状态**:
|
||||
- 遍历 `storageLocations` 数组,为每一个库位渲染一个信息块。
|
||||
- 使用 `getStorageStatusTag` 方法,根据库位的多个布尔状态 (`is_occupied`, `is_locked`, `is_disabled`, `is_empty_tray`),生成对应文本(如“已占用”、“未锁定”)和颜色样式的标签,为用户提供清晰、直观的状态反馈。
|
||||
|
||||
## 4. 数据流转图
|
||||
|
||||
下面的流程图清晰地展示了从后端数据推送到前端UI渲染的完整过程。
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Backend as Backend (WebSocket)
|
||||
participant StorageLocationService as StorageLocationService
|
||||
participant MovementSupervision as movement-supervision.vue
|
||||
participant PointDetailCard as PointDetailCard
|
||||
participant EditorService as EditorService (Canvas)
|
||||
|
||||
MovementSupervision->>StorageLocationService: new StorageLocationService(editor, sceneId)
|
||||
MovementSupervision->>StorageLocationService: startMonitoring()
|
||||
StorageLocationService->>Backend: 建立 WebSocket 连接
|
||||
Backend-->>StorageLocationService: 推送库位状态消息 (JSON)
|
||||
|
||||
StorageLocationService->>StorageLocationService: handleStorageLocationUpdate(message)
|
||||
StorageLocationService->>EditorService: buildStationToPointIdMap() <br> (获取点位ID与站点名称映射)
|
||||
StorageLocationService->>StorageLocationService: 更新内部 storageLocations Map
|
||||
StorageLocationService->>EditorService: updatePointBorderColor(pointId, color) <br> (更新画布点的边框颜色)
|
||||
|
||||
Note right of MovementSupervision: 用户点击一个动作点
|
||||
MovementSupervision->>StorageLocationService: getLocationsByPointId(current.id)
|
||||
StorageLocationService-->>MovementSupervision: 返回库位信息数组
|
||||
MovementSupervision->>PointDetailCard: :storage-locations="locations"
|
||||
|
||||
PointDetailCard->>PointDetailCard: 渲染库位名称和状态标签
|
||||
```
|
||||
|
||||
## 5. 关联文件清单
|
||||
|
||||
- **`src/pages/movement-supervision.vue`**:
|
||||
- **角色**: 核心页面。负责实例化和管理 `StorageLocationService` 的生命周期,并将获取到的库位数据传递给 `PointDetailCard`。
|
||||
- **`src/services/storage-location.service.ts`**:
|
||||
- **角色**: 核心服务。处理所有与库位相关的业务逻辑,包括数据获取、状态管理和与画布的交互。
|
||||
- **`src/components/card/point-detail-card.vue`**:
|
||||
- **角色**: UI组件。负责展示单个动作点所绑定的库位的详细状态信息。
|
||||
- **`src/apis/scene/api.ts`**:
|
||||
- **角色**: API定义。包含 `monitorStorageLocationById` 方法,用于发起 WebSocket 连接请求。
|
||||
- **`src/core/editor.service.ts`**:
|
||||
- **角色**: 编辑器服务。`StorageLocationService` 依赖此服务来获取画布中的点位信息并更新其视觉样式。
|
@ -1,131 +0,0 @@
|
||||
# 库位渲染与状态集成说明
|
||||
|
||||
本文档说明当前项目中“库位网格”在 Meta2D 渲染链路中的实现方式、扩展点与接入 WebSocket 实时状态的建议方案,并给出从旧的 DOM 覆盖层组件迁移到画布内绘制的步骤与注意事项。
|
||||
|
||||
- 相关文件
|
||||
- 画布渲染入口:`src/services/editor.service.ts` 中 `drawPoint()` 自定义绘制
|
||||
- 库位网格绘制模块:`src/services/draw/storage-location-drawer.ts`(导出 `drawStorageGrid`)
|
||||
- 场景编辑页:`src/pages/scene-editor.vue`
|
||||
- WS 服务(示例/参考):`src/services/storage-location.service.ts`
|
||||
|
||||
## 一、渲染概览
|
||||
|
||||
- 使用 Meta2D 的自定义绘制回调,在 `drawPoint()` 内针对 `MapPointType.动作点` 调用 `drawStorageGrid()`,在“世界坐标”中直接绘制 2x3 栅格。
|
||||
- 这样,库位网格与点位同步缩放和平移,无需计算 DOM 偏移与容器滚动,也无需在模板中叠加覆盖层组件。
|
||||
|
||||
## 二、数据来源与字段约定
|
||||
|
||||
- 点位对象:`MapPen`(`@api/map` 导出的类型)。
|
||||
- 动作点扩展字段:
|
||||
- `pen.point.associatedStorageLocations: string[]`
|
||||
- 用于静态展示与数量溢出提示(+N)。
|
||||
- 后续若接入 WS 实时状态,建议增加:
|
||||
- `pen.point.storageStates?: Record<string, { occupied?: boolean; locked?: boolean; }>`
|
||||
- key 为库位层名(例如 `layer_name`),value 为状态对象。
|
||||
|
||||
## 三、绘制模块 `drawStorageGrid`
|
||||
|
||||
路径:`src/services/draw/storage-location-drawer.ts`
|
||||
|
||||
签名:
|
||||
```ts
|
||||
export function drawStorageGrid(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
pen: MapPen,
|
||||
opts?: { fontFamily?: string },
|
||||
): void
|
||||
```
|
||||
|
||||
职责:
|
||||
- 基于 `pen.calculative.worldRect` 决定栅格的尺寸、间距与摆放位置(点位右上角)。
|
||||
- 前 5 格按默认样式绘制;若数量超出 6,则在最后一格显示 `+N`。
|
||||
- 采用“世界坐标”绘制,随 Meta2D 画布的缩放/平移自然同步。
|
||||
|
||||
可调参数(在代码内集中计算):
|
||||
- 单格尺寸 `cell`:约为点位较短边的 35%,限制在 `[6, 14]`(世界坐标单位)。
|
||||
- 间距 `gap`:约为点位较短边的 8%,限制在 `[2, 6]`。
|
||||
- 圆角统一通过 `roundedRectPath` 构造路径,避免 `roundRect` 类型兼容问题。
|
||||
|
||||
## 四、在 `drawPoint()` 中的接入
|
||||
|
||||
路径:`src/services/editor.service.ts`
|
||||
|
||||
核心片段:
|
||||
```ts
|
||||
// …计算文本等
|
||||
ctx.fillText(label, x + w / 2, y - fontSize * lineHeight);
|
||||
|
||||
// 库位2x3栅格:动作点在画布上直接绘制(静态数据)
|
||||
if (type === MapPointType.动作点) {
|
||||
drawStorageGrid(ctx, pen, { fontFamily });
|
||||
}
|
||||
```
|
||||
|
||||
注意:需确保 `EditorService.#register()` 已注册 point 的自定义绘制函数(项目中已完成)。
|
||||
|
||||
## 五、接入 WS 实时状态(建议)
|
||||
|
||||
有两种模式可选:
|
||||
|
||||
- 模式 A:在 `drawStorageGrid()` 增加状态解析回调
|
||||
- 签名扩展:`opts.stateResolver?: (penId: string, layerName: string) => { occupied?: boolean; locked?: boolean; }`
|
||||
- `drawPoint()` 调用时传入解析函数(从全局 Map 或服务读取),按状态填充不同的底色/边框/角标。
|
||||
- 优点:不污染 `pen` 数据,职责清晰;适合不同页面传入不同策略。
|
||||
|
||||
- 模式 B:将状态写回 `pen` 上
|
||||
- WS 收到后更新 `pen.point.storageStates[layer_name] = { … }`,然后 `editor.render()` 触发重绘。
|
||||
- `drawStorageGrid()` 读取 `pen.point.storageStates` 决定样式。
|
||||
- 优点:渲染模块无额外依赖;缺点:需管理好数据生命周期与清理。
|
||||
|
||||
建议优先使用模式 A,便于解耦与测试。
|
||||
|
||||
### 与 `StorageLocationService` 的对接
|
||||
|
||||
`StorageLocationService.handleStorageLocationUpdate()` 已按 `operate_point_id` 与站点名映射聚合库位数据:
|
||||
- 可在服务层维护 `Map<pointId, Map<layer_name, State>>` 的结构。
|
||||
- `drawPoint()` 中调用 `drawStorageGrid(ctx, pen, { stateResolver })`,`stateResolver` 从该 Map 中读取。
|
||||
- 状态变化后调用 `editor.render()` 重绘即可生效。
|
||||
|
||||
## 六、样式与语义(可自定义)
|
||||
|
||||
- 默认颜色:
|
||||
- 底板:`#00000022`(半透明)
|
||||
- 单格:`#f5f5f5` + `#999` 边框
|
||||
- 溢出格:底 `#e6f4ff`,边框 `#1677ff`,文字 `#1677ff`
|
||||
- 状态上色(建议):
|
||||
- occupied=true:填充偏黄/橙(例:`#ffe58f`)
|
||||
- locked=true:边框高亮(例:`#fa541c`)或角标图标
|
||||
- 冲突(同时 occupied & locked):设置优先级或组合样式
|
||||
|
||||
## 七、性能考量
|
||||
|
||||
- 绘制在同一 Canvas 渲染管线完成,没有额外 DOM 或布局成本。
|
||||
- 仅当 WS 推送或交互(缩放/平移/选择)触发重绘,保持流畅。
|
||||
- 如果点位非常密集,可在 `drawStorageGrid` 内做最小尺寸阈值,过小则不绘制或绘制为简化标记。
|
||||
|
||||
## 八、迁移步骤(从 DOM 覆盖层组件)
|
||||
|
||||
1. 将组件 `storage-location-grid-overlay.vue` 从页面删除(如 `scene-editor.vue`)。
|
||||
2. 确认 `drawPoint()` 已导入并调用 `drawStorageGrid()`。
|
||||
3. 按需对接 WS:在服务层聚合状态,传入解析回调或写入 `pen`。
|
||||
4. 验证缩放/平移/窗口尺寸变化场景,确保网格对齐与可读性。
|
||||
|
||||
## 九、测试清单
|
||||
|
||||
- 缩放到 0.5x、1x、2x 时,栅格位置与尺寸是否稳定且可读。
|
||||
- 拖动画布及窗口 resize 后,栅格仍紧邻动作点右上角。
|
||||
- 当 `associatedStorageLocations.length` <= 6 与 > 6 时展示正确(含 `+N`)。
|
||||
- 接入 WS 后:
|
||||
- occupied/locked 状态能正确上色/标注。
|
||||
- 状态切换时无残影,性能稳定。
|
||||
|
||||
## 十、常见问题 FAQ
|
||||
|
||||
- Q:为何不再使用 DOM 覆盖层组件?
|
||||
- A:避免坐标换算/偏移/滚动同步等复杂问题,统一交由 Meta2D 世界坐标与渲染管线处理。
|
||||
|
||||
- Q:如果需要支持点击库位打开菜单?
|
||||
- A:可在 Meta2D 的事件系统中命中该区域(保留格子范围),或保留一个轻量的浮层仅用于复杂交互(与绘制相互独立)。
|
||||
|
||||
- Q:如何快速调整大小与颜色?
|
||||
- A:修改 `storage-location-drawer.ts` 内的 `cell/gap` 与颜色常量;如需主题化,可迁移到 `sTheme`。
|
@ -1,117 +0,0 @@
|
||||
# 批量编辑功能使用说明
|
||||
|
||||
## 🎯 功能概述
|
||||
|
||||
批量编辑功能允许用户在场景编辑器中同时选中多个点位和路线,并批量修改它们的属性,大大提升了编辑效率。
|
||||
|
||||
## ✨ 主要功能
|
||||
|
||||
### 1. 批量选中
|
||||
|
||||
- **鼠标拖动选中**:在编辑模式下,可以通过鼠标拖动框选多个点位和路线
|
||||
- **实时计数**:工具栏会显示当前选中的元素数量
|
||||
- **清除选择**:一键清除所有选中状态
|
||||
|
||||
### 2. 批量编辑点位
|
||||
|
||||
- **点位类型**:支持修改点位类型
|
||||
- 普通点
|
||||
- 等待点
|
||||
- 避让点
|
||||
- 临时避让点
|
||||
- 库区点
|
||||
- 电梯点
|
||||
- 自动门点
|
||||
- 充电点
|
||||
- 停靠点
|
||||
- 动作点
|
||||
- 禁行点
|
||||
|
||||
### 3. 批量编辑路线
|
||||
|
||||
- **通行类型**:
|
||||
- 无限制
|
||||
- 仅空载可通行
|
||||
- 仅载货可通行
|
||||
- 禁行
|
||||
|
||||
## 🚀 使用方法
|
||||
|
||||
### 步骤 1:启用编辑器
|
||||
|
||||
1. 打开场景编辑器页面
|
||||
2. 点击右上角的"启用编辑器"按钮
|
||||
3. 编辑器工具栏和批量编辑工具栏将出现在页面上
|
||||
|
||||
### 步骤 2:批量选中元素
|
||||
|
||||
1. 在画布上按住鼠标左键
|
||||
2. 拖动鼠标框选需要编辑的点位和路线
|
||||
3. 选中的元素会高亮显示
|
||||
4. 批量编辑工具栏会显示选中元素的数量
|
||||
|
||||
### 步骤 3:批量编辑
|
||||
|
||||
1. 点击"批量编辑"按钮打开编辑面板
|
||||
2. 根据需要选择要修改的属性:
|
||||
- 如果选中了点位,可以修改点位类型
|
||||
- 如果选中了路线,可以修改路线类型、通行类型和方向
|
||||
3. 在预览区域查看即将应用的更改
|
||||
4. 点击"确定"应用更改,或点击"取消"放弃更改
|
||||
|
||||
### 步骤 4:清除选择
|
||||
|
||||
- 点击"清除选择"按钮可以取消所有选中状态
|
||||
- 或者直接点击画布空白区域也可以清除选择
|
||||
|
||||
## 🎨 界面说明
|
||||
|
||||
### 批量编辑工具栏
|
||||
|
||||
- 位置:页面顶部中央(仅在选中元素时显示)
|
||||
- 功能:显示选中数量、打开批量编辑面板、清除选择
|
||||
|
||||
### 批量编辑面板
|
||||
|
||||
- **选中统计**:显示选中的点位和路线数量
|
||||
- **点位编辑区**:当选中点位时显示,用于修改点位类型
|
||||
- **路线编辑区**:当选中路线时显示,用于修改路线通行类型
|
||||
- **预览区域**:显示即将应用的更改,方便确认
|
||||
|
||||
## 💡 使用技巧
|
||||
|
||||
1. **混合选择**:可以同时选中点位和路线进行批量编辑
|
||||
2. **部分更新**:只修改需要更改的属性,其他属性保持不变
|
||||
3. **预览确认**:在应用更改前,预览区域会显示所有即将修改的内容
|
||||
4. **撤销支持**:所有批量编辑操作都支持撤销(Ctrl+Z)
|
||||
|
||||
## 🔧 技术实现
|
||||
|
||||
- **响应式设计**:基于 Vue 3 Composition API
|
||||
- **类型安全**:完整的 TypeScript 类型定义
|
||||
- **性能优化**:批量更新减少渲染次数,自动触发画布重绘
|
||||
- **用户体验**:实时预览和撤销支持,紧凑的弹框设计
|
||||
|
||||
## 📝 注意事项
|
||||
|
||||
1. 批量编辑功能仅在编辑模式下可用
|
||||
2. 选中的元素必须是点位(point)或路线(line)类型
|
||||
3. 区域(area)和机器人(robot)元素不支持批量编辑
|
||||
4. 所有更改都会记录在编辑历史中,支持撤销操作
|
||||
|
||||
## 🐛 故障排除
|
||||
|
||||
### 问题:批量编辑按钮不可用
|
||||
|
||||
- **原因**:没有选中任何元素
|
||||
- **解决**:先通过鼠标拖动选中需要编辑的点位或路线
|
||||
|
||||
### 问题:编辑面板中没有显示选项
|
||||
|
||||
- **原因**:选中的元素类型不支持该编辑选项
|
||||
- **解决**:确保选中的是点位或路线类型的元素
|
||||
|
||||
### 问题:更改没有生效
|
||||
|
||||
- **原因**:可能没有点击"确定"按钮
|
||||
- **解决**:在编辑面板中点击"确定"按钮应用更改
|
@ -1,778 +0,0 @@
|
||||
# 机器人运动监控组件详细分析
|
||||
|
||||
## 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<void> {
|
||||
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 } = <RobotRealtimeInfo>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<RobotRealtimeInfo>): 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<Pick<MapPen, 'image' | 'iconWidth' | 'iconHeight' | 'iconTop'>> {
|
||||
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<string, RobotRealtimeInfo>();
|
||||
|
||||
// 订阅者列表,用于通知状态变化
|
||||
private subscribers = new Set<(robotId: string, info: RobotRealtimeInfo) => void>();
|
||||
|
||||
// 连接管理,避免重复连接
|
||||
private connections = new Map<string, WebSocket>();
|
||||
|
||||
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<WebSocket | null> {
|
||||
// 检查现有连接
|
||||
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核心逻辑
|
||||
<script setup lang="ts">
|
||||
import { GlobalRobotStateManager } from '@/services/global-robot-state';
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const globalStateManager = GlobalRobotStateManager.getInstance();
|
||||
|
||||
// 移除原有的monitorScene函数,改用全局状态管理
|
||||
|
||||
const initializeMonitoring = async () => {
|
||||
// 确保WebSocket连接存在
|
||||
await globalStateManager.getOrCreateConnection(props.sid);
|
||||
|
||||
// 订阅机器人状态变化
|
||||
const unsubscribe = globalStateManager.subscribe((robotId, robotInfo) => {
|
||||
if (!editor.value?.checkRobotById(robotId)) return;
|
||||
|
||||
// 更新机器人基本信息
|
||||
const { id, x, y, active, angle, path, ...rest } = robotInfo;
|
||||
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 });
|
||||
}
|
||||
});
|
||||
|
||||
// 组件卸载时取消订阅
|
||||
onUnmounted(() => {
|
||||
unsubscribe();
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await readScene();
|
||||
await editor.value?.initRobots();
|
||||
await initializeMonitoring(); // 使用改进的初始化方法
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
### 5.3 方案三: EditorService增强
|
||||
|
||||
```typescript
|
||||
// 为EditorService添加状态缓存和同步机制
|
||||
export class EditorService extends Meta2d {
|
||||
// 添加状态缓存
|
||||
private robotStateCache = new Map<string, RobotRealtimeInfo>();
|
||||
|
||||
/**
|
||||
* 改进的坐标转换方法
|
||||
*/
|
||||
private normalizeCoordinates(info: Partial<RobotRealtimeInfo>): { 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<RobotRealtimeInfo>): 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连接复用、状态缓存机制和坐标转换优化等解决方案,可以有效解决这些问题,确保多页面间机器人位置的一致性。
|
||||
|
||||
## 8. 自动门点光圈功能扩展
|
||||
|
||||
### 8.1 功能概述
|
||||
|
||||
在机器人光圈绘制的基础上,新增了自动门点的光圈绘制功能。当WebSocket推送自动门点状态数据时,系统会根据设备状态自动绘制相应颜色的光圈。
|
||||
|
||||
### 8.2 数据结构
|
||||
|
||||
WebSocket推送的自动门点数据格式:
|
||||
|
||||
```typescript
|
||||
{
|
||||
"gid": "",
|
||||
"id": "172.31.57.55-502-17", // 设备ID,用于匹配地图中的自动门点
|
||||
"label": "AutoD01",
|
||||
"type": 99, // 标识为自动门点
|
||||
"deviceStatus": 0, // 设备状态:0=关门,1=开门
|
||||
"active": true,
|
||||
// ... 其他字段
|
||||
}
|
||||
```
|
||||
|
||||
### 8.3 实现细节
|
||||
|
||||
#### 主题颜色配置
|
||||
|
||||
在 `editor-dark.json` 和 `editor-light.json` 中添加自动门点颜色配置:
|
||||
|
||||
```json
|
||||
"autoDoor": {
|
||||
"stroke-closed": "#FF4D4F99", // 关门状态边框(红色)
|
||||
"fill-closed": "#FF4D4F33", // 关门状态填充(红色)
|
||||
"stroke-open": "#1890FF99", // 开门状态边框(蓝色)
|
||||
"fill-open": "#1890FF33" // 开门状态填充(蓝色)
|
||||
}
|
||||
```
|
||||
|
||||
#### 数据模型扩展
|
||||
|
||||
在 `MapPointInfo` 接口中新增字段:
|
||||
|
||||
```typescript
|
||||
interface MapPointInfo {
|
||||
// ... 现有字段
|
||||
deviceStatus?: number; // 设备状态(0=关门,1=开门)
|
||||
active?: boolean; // 是否激活状态,控制光圈显示
|
||||
}
|
||||
```
|
||||
|
||||
#### 绘制函数修改
|
||||
|
||||
在 `drawPoint` 函数中添加自动门点光圈绘制逻辑:
|
||||
|
||||
```typescript
|
||||
// 为自动门点绘制光圈
|
||||
if (type === MapPointType.自动门点 && pointActive && deviceStatus !== undefined) {
|
||||
const ox = x + w / 2;
|
||||
const oy = y + h / 2;
|
||||
const haloRadius = Math.max(w, h) / 2 + 10;
|
||||
|
||||
ctx.ellipse(ox, oy, haloRadius, haloRadius, 0, 0, Math.PI * 2);
|
||||
|
||||
// 根据设备状态选择颜色
|
||||
if (deviceStatus === 0) {
|
||||
// 关门状态 - 红色
|
||||
ctx.fillStyle = get(theme, 'autoDoor.fill-closed') ?? '#FF4D4F33';
|
||||
ctx.strokeStyle = get(theme, 'autoDoor.stroke-closed') ?? '#FF4D4F99';
|
||||
} else {
|
||||
// 开门状态 - 蓝色
|
||||
ctx.fillStyle = get(theme, 'autoDoor.fill-open') ?? '#1890FF33';
|
||||
ctx.strokeStyle = get(theme, 'autoDoor.stroke-open') ?? '#1890FF99';
|
||||
}
|
||||
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
}
|
||||
```
|
||||
|
||||
#### 状态更新方法
|
||||
|
||||
新增 `updateAutoDoorByDeviceId` 方法:
|
||||
|
||||
```typescript
|
||||
public updateAutoDoorByDeviceId(deviceId: string, deviceStatus: number, active = true): void {
|
||||
// 查找匹配的自动门点
|
||||
const autoDoorPoint = this.data().pens.find(
|
||||
(pen) => pen.name === 'point' &&
|
||||
(pen as MapPen).point?.type === MapPointType.自动门点 &&
|
||||
(pen as MapPen).point?.deviceId === deviceId
|
||||
) as MapPen | undefined;
|
||||
|
||||
if (!autoDoorPoint?.id || !autoDoorPoint.point) return;
|
||||
|
||||
// 更新自动门点状态
|
||||
this.updatePen(autoDoorPoint.id, {
|
||||
point: {
|
||||
...autoDoorPoint.point,
|
||||
deviceStatus,
|
||||
active,
|
||||
},
|
||||
}, false);
|
||||
}
|
||||
```
|
||||
|
||||
#### WebSocket数据处理
|
||||
|
||||
修改运动监控组件的WebSocket处理逻辑:
|
||||
|
||||
```typescript
|
||||
ws.onmessage = (e) => {
|
||||
const data = JSON.parse(e.data || '{}');
|
||||
|
||||
// 判断数据类型:type=99为自动门点,其他为机器人
|
||||
if (data.type === 99) {
|
||||
// 自动门点数据处理
|
||||
const { id: deviceId, deviceStatus, active = true } = data;
|
||||
if (deviceId && deviceStatus !== undefined) {
|
||||
latestAutoDoorData.set(deviceId, { deviceId, deviceStatus, active });
|
||||
}
|
||||
} else {
|
||||
// 机器人数据处理
|
||||
const robotData = data as RobotRealtimeInfo;
|
||||
latestRobotData.set(robotData.id, robotData);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 8.4 使用方式
|
||||
|
||||
1. **地图编辑**:在场景编辑器中创建自动门点,并设置对应的设备ID
|
||||
2. **状态监控**:系统自动接收WebSocket推送的自动门点状态数据
|
||||
3. **视觉反馈**:
|
||||
- 关门状态(deviceStatus=0):显示红色光圈
|
||||
- 开门状态(deviceStatus=1):显示蓝色光圈
|
||||
- 无状态数据时:不显示光圈
|
||||
|
||||
### 8.5 技术优势
|
||||
|
||||
1. **复用机器人架构**:充分利用现有的渲染和状态管理机制
|
||||
2. **高性能处理**:采用相同的时间分片和数据缓冲策略
|
||||
3. **类型安全**:完整的TypeScript类型支持
|
||||
4. **主题适配**:支持深色和浅色主题
|
||||
5. **实时性**:与机器人监控相同的实时更新能力
|
||||
|
||||
这种设计展现了系统架构的可扩展性,为未来支持更多设备类型(如电梯、传感器等)奠定了良好的基础。
|
@ -1,677 +0,0 @@
|
||||
# 编辑器服务核心架构分析
|
||||
|
||||
## 1. 概述
|
||||
|
||||
`EditorService` 是整个场景编辑器的核心服务类,继承自 `Meta2d` 图形引擎。它负责管理场景中的所有元素(机器人、点位、路线、区域),处理用户交互,以及场景数据的序列化和反序列化。
|
||||
|
||||
```typescript
|
||||
export class EditorService extends Meta2d {
|
||||
// 继承 Meta2d 获得强大的 2D 图形渲染能力
|
||||
}
|
||||
```
|
||||
|
||||
## 2. 核心架构分析
|
||||
|
||||
### 2.1 继承架构
|
||||
|
||||
```
|
||||
EditorService
|
||||
↓ 继承
|
||||
Meta2d (第三方图形引擎)
|
||||
↓ 提供
|
||||
- Canvas 渲染能力
|
||||
- 图形元素管理
|
||||
- 事件系统
|
||||
- 坐标变换
|
||||
- 撤销重做
|
||||
```
|
||||
|
||||
### 2.2 核心组成模块
|
||||
|
||||
1. **场景文件管理** - 序列化/反序列化
|
||||
2. **机器人管理** - 机器人组和个体管理
|
||||
3. **点位管理** - 各种类型点位的创建和管理
|
||||
4. **路线管理** - 连接点位的路径管理
|
||||
5. **区域管理** - 矩形区域的创建和管理
|
||||
6. **实时交互** - 鼠标事件处理和状态管理
|
||||
7. **自定义绘制** - Canvas 绘制函数
|
||||
8. **事件监听** - 编辑器状态变化监听
|
||||
|
||||
## 3. 场景文件管理详解
|
||||
|
||||
### 3.1 场景数据结构
|
||||
|
||||
```typescript
|
||||
type StandardScene = {
|
||||
robotGroups?: RobotGroup[]; // 机器人组
|
||||
robots?: RobotInfo[]; // 机器人列表
|
||||
points?: StandardScenePoint[]; // 点位数据
|
||||
routes?: StandardSceneRoute[]; // 路线数据
|
||||
areas?: StandardSceneArea[]; // 区域数据
|
||||
blocks?: any[]; // 其他块数据
|
||||
};
|
||||
```
|
||||
|
||||
### 3.2 场景加载过程(为什么场景文件能生成对应区域)
|
||||
|
||||
#### 3.2.1 加载入口函数
|
||||
|
||||
```typescript
|
||||
public async load(map?: string, editable = false, detail?: Partial<GroupSceneDetail>): Promise<void> {
|
||||
// 1. 解析 JSON 字符串为场景对象
|
||||
const scene: StandardScene = map ? JSON.parse(map) : {};
|
||||
|
||||
// 2. 如果有组详情,优先使用组数据
|
||||
if (!isEmpty(detail?.group)) {
|
||||
scene.robotGroups = [detail.group];
|
||||
scene.robots = detail.robots;
|
||||
}
|
||||
|
||||
// 3. 提取各类数据
|
||||
const { robotGroups, robots, points, routes, areas } = scene;
|
||||
|
||||
// 4. 初始化编辑器
|
||||
this.open(); // 打开 Meta2d 画布
|
||||
this.setState(editable); // 设置编辑状态
|
||||
|
||||
// 5. 按顺序加载各类元素
|
||||
this.#loadRobots(robotGroups, robots); // 加载机器人
|
||||
await this.#loadScenePoints(points); // 加载点位
|
||||
this.#loadSceneRoutes(routes); // 加载路线
|
||||
await this.#loadSceneAreas(areas); // 加载区域 ⭐
|
||||
|
||||
// 6. 清空历史记录
|
||||
this.store.historyIndex = undefined;
|
||||
this.store.histories = [];
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.2.2 区域加载详细过程
|
||||
|
||||
```typescript
|
||||
async #loadSceneAreas(areas?: StandardSceneArea[]): Promise<void> {
|
||||
if (!areas?.length) return;
|
||||
|
||||
// 并行处理所有区域
|
||||
await Promise.all(
|
||||
areas.map(async (v) => {
|
||||
// 1. 从场景数据中提取区域信息
|
||||
const { id, name, desc, x, y, w, h, type, points, routes, properties } = v;
|
||||
|
||||
// 2. 调用 addArea 方法在画布上创建实际的图形对象
|
||||
await this.addArea(
|
||||
{ x, y }, // 左上角坐标
|
||||
{ x: x + w, y: y + h }, // 右下角坐标
|
||||
type, // 区域类型
|
||||
id // 区域ID
|
||||
);
|
||||
|
||||
// 3. 设置区域的详细属性
|
||||
this.setValue(
|
||||
{
|
||||
id,
|
||||
label: name, // 显示名称
|
||||
desc, // 描述
|
||||
properties, // 自定义属性
|
||||
area: { type, points, routes } // 区域特定数据
|
||||
},
|
||||
{ render: false, history: false, doEvent: false }
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**关键理解点**:
|
||||
|
||||
- 场景文件中的 `areas` 数组包含了所有区域的完整信息
|
||||
- 每个区域包含位置 `(x, y, w, h)`、类型 `type`、关联的点位和路线
|
||||
- `addArea` 方法负责在 Canvas 上创建实际的可视化图形
|
||||
- `setValue` 方法设置图形对象的业务属性
|
||||
|
||||
## 4. 区域绘制原理详解(为什么可以在页面画一个区域)
|
||||
|
||||
### 4.1 鼠标事件监听系统
|
||||
|
||||
```typescript
|
||||
// 鼠标事件主题
|
||||
readonly #mouse$$ = new Subject<{ type: 'click' | 'mousedown' | 'mouseup'; value: Point }>();
|
||||
|
||||
// 点击事件流
|
||||
public readonly mouseClick = useObservable<Point>(
|
||||
this.#mouse$$.pipe(
|
||||
filter(({ type }) => type === 'click'),
|
||||
debounceTime(100),
|
||||
map(({ value }) => value),
|
||||
),
|
||||
);
|
||||
|
||||
// 拖拽事件流 ⭐ 关键!这是画区域的核心
|
||||
public readonly mouseBrush = useObservable<[Point, Point]>(
|
||||
this.#mouse$$.pipe(
|
||||
filter(({ type }) => type === 'mousedown'), // 监听鼠标按下
|
||||
switchMap(({ value: s }) =>
|
||||
this.#mouse$$.pipe(
|
||||
filter(({ type }) => type === 'mouseup'), // 监听鼠标抬起
|
||||
map(({ value: e }) => <[Point, Point]>[s, e]), // 返回起始和结束点
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
### 4.2 工具栏组件中的区域创建监听
|
||||
|
||||
```typescript
|
||||
// 在 EditorToolbar 组件中
|
||||
const mode = ref<MapAreaType>();
|
||||
|
||||
// 监听鼠标拖拽事件
|
||||
watch(editor.value.mouseBrush, (v) => {
|
||||
if (!mode.value) return; // 如果没有选择区域工具,不处理
|
||||
const [p1, p2] = v ?? []; // 获取起始点和结束点
|
||||
if (isEmpty(p1) || isEmpty(p2)) return; // 验证点位有效性
|
||||
|
||||
// 调用编辑器服务创建区域 ⭐
|
||||
editor.value.addArea(p1, p2, mode.value);
|
||||
mode.value = undefined; // 重置工具状态
|
||||
});
|
||||
```
|
||||
|
||||
### 4.3 addArea 方法详细实现
|
||||
|
||||
```typescript
|
||||
public async addArea(p1: Point, p2: Point, type = MapAreaType.库区, id?: string) {
|
||||
// 1. 获取当前缩放比例
|
||||
const scale = this.data().scale ?? 1;
|
||||
|
||||
// 2. 计算区域宽高
|
||||
const w = Math.abs(p1.x - p2.x);
|
||||
const h = Math.abs(p1.y - p2.y);
|
||||
|
||||
// 3. 最小尺寸检查(防止创建过小的区域)
|
||||
if (w * scale < 50 || h * scale < 60) return;
|
||||
|
||||
// 4. 准备关联数据
|
||||
const points = new Array<string>();
|
||||
const routes = new Array<string>();
|
||||
|
||||
if (!id) {
|
||||
id = s8(); // 生成唯一ID
|
||||
const selected = <MapPen[]>this.store.active; // 获取当前选中的元素
|
||||
|
||||
// 5. 根据区域类型自动关联相关元素
|
||||
switch (type) {
|
||||
case MapAreaType.库区:
|
||||
// 库区只关联动作点
|
||||
selected?.filter(({ point }) => point?.type === MapPointType.动作点)
|
||||
.forEach(({ id }) => points.push(id!));
|
||||
break;
|
||||
case MapAreaType.互斥区:
|
||||
// 互斥区关联所有点位和路线
|
||||
selected?.filter(({ point }) => point?.type)
|
||||
.forEach(({ id }) => points.push(id!));
|
||||
selected?.filter(({ route }) => route?.type)
|
||||
.forEach(({ id }) => routes.push(id!));
|
||||
break;
|
||||
case MapAreaType.非互斥区:
|
||||
// 非互斥区只关联点位
|
||||
selected?.filter(({ point }) => point?.type)
|
||||
.forEach(({ id }) => points.push(id!));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 6. 创建区域图形对象
|
||||
const pen: MapPen = {
|
||||
id,
|
||||
name: 'area', // 图形类型标识
|
||||
tags: ['area', `area-${type}`], // 标签用于查找和分类
|
||||
label: `A${id}`, // 显示标签
|
||||
x: Math.min(p1.x, p2.x), // 左上角 X
|
||||
y: Math.min(p1.y, p2.y), // 左上角 Y
|
||||
width: w, // 宽度
|
||||
height: h, // 高度
|
||||
lineWidth: 1, // 边框宽度
|
||||
area: { type, points, routes }, // 区域业务数据
|
||||
locked: LockState.DisableMoveScale, // 锁定状态(禁止移动缩放)
|
||||
};
|
||||
|
||||
// 7. 添加到画布并设置层级
|
||||
const area = await this.addPen(pen, true, true, true);
|
||||
this.bottom(area); // 将区域放到最底层
|
||||
}
|
||||
```
|
||||
|
||||
**关键理解点**:
|
||||
|
||||
1. **事件流处理**:通过 RxJS 的事件流来处理鼠标拖拽
|
||||
2. **坐标计算**:将鼠标坐标转换为画布坐标系中的区域
|
||||
3. **图形对象创建**:创建符合 Meta2d 要求的图形对象
|
||||
4. **层级管理**:区域作为背景层,放在最底层
|
||||
5. **状态管理**:自动关联当前选中的相关元素
|
||||
|
||||
## 5. 自定义绘制系统
|
||||
|
||||
### 5.1 绘制函数注册
|
||||
|
||||
```typescript
|
||||
#register() {
|
||||
// 注册基础图形
|
||||
this.register({ line: () => new Path2D() });
|
||||
|
||||
// 注册自定义绘制函数 ⭐
|
||||
this.registerCanvasDraw({
|
||||
point: drawPoint, // 点位绘制
|
||||
line: drawLine, // 路线绘制
|
||||
area: drawArea, // 区域绘制 ⭐
|
||||
robot: drawRobot // 机器人绘制
|
||||
});
|
||||
|
||||
// 注册锚点
|
||||
this.registerAnchors({ point: anchorPoint });
|
||||
|
||||
// 注册线条绘制函数
|
||||
this.addDrawLineFn('bezier2', lineBezier2);
|
||||
this.addDrawLineFn('bezier3', lineBezier3);
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 区域绘制函数详解
|
||||
|
||||
```typescript
|
||||
function drawArea(ctx: CanvasRenderingContext2D, pen: MapPen): void {
|
||||
// 1. 获取主题配置
|
||||
const theme = sTheme.editor;
|
||||
|
||||
// 2. 获取绘制参数
|
||||
const { active, fontSize = 14, lineHeight = 1.5, fontFamily } = pen.calculative ?? {};
|
||||
const { x = 0, y = 0, width: w = 0, height: h = 0 } = pen.calculative?.worldRect ?? {};
|
||||
const { type } = pen.area ?? {};
|
||||
const { label = '' } = pen ?? {};
|
||||
|
||||
// 3. 开始绘制
|
||||
ctx.save();
|
||||
|
||||
// 4. 绘制矩形区域
|
||||
ctx.rect(x, y, w, h);
|
||||
|
||||
// 5. 填充颜色(根据区域类型)
|
||||
ctx.fillStyle = get(theme, `area.fill-${type}`) ?? '';
|
||||
ctx.fill();
|
||||
|
||||
// 6. 绘制边框(根据激活状态)
|
||||
ctx.strokeStyle = get(theme, active ? 'area.strokeActive' : `area.stroke-${type}`) ?? '';
|
||||
ctx.stroke();
|
||||
|
||||
// 7. 绘制标签文字
|
||||
ctx.fillStyle = get(theme, 'color') ?? '';
|
||||
ctx.font = `${fontSize}px/${lineHeight} ${fontFamily}`;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'top';
|
||||
ctx.fillText(label, x + w / 2, y - fontSize * lineHeight);
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
```
|
||||
|
||||
**关键理解点**:
|
||||
|
||||
- Canvas 2D API 直接绘制矩形和文字
|
||||
- 主题系统提供颜色配置
|
||||
- 根据区域类型和激活状态使用不同的样式
|
||||
- 文字标签显示在区域上方
|
||||
|
||||
## 6. 响应式状态管理
|
||||
|
||||
### 6.1 数据流设计
|
||||
|
||||
```typescript
|
||||
// 变化事件主题
|
||||
readonly #change$$ = new Subject<boolean>();
|
||||
|
||||
// 区域列表响应式数据
|
||||
public readonly areas = useObservable<MapPen[], MapPen[]>(
|
||||
this.#change$$.pipe(
|
||||
filter((v) => v), // 只响应数据变化事件
|
||||
debounceTime(100), // 防抖处理
|
||||
map(() => this.find('area')), // 查找所有区域
|
||||
),
|
||||
{ initialValue: new Array<MapPen>() },
|
||||
);
|
||||
|
||||
// 当前选中元素
|
||||
public readonly current = useObservable<MapPen>(
|
||||
this.#change$$.pipe(
|
||||
debounceTime(100),
|
||||
map(() => <MapPen>clone(this.store.active?.[0])),
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
### 6.2 事件监听系统
|
||||
|
||||
```typescript
|
||||
#listen(e: unknown, v: any) {
|
||||
switch (e) {
|
||||
case 'opened':
|
||||
this.#load(sTheme.theme);
|
||||
this.#change$$.next(true); // 触发数据更新
|
||||
break;
|
||||
|
||||
case 'add':
|
||||
this.#change$$.next(true); // 元素添加后更新
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
this.#onDelete(v);
|
||||
this.#change$$.next(true); // 元素删除后更新
|
||||
break;
|
||||
|
||||
case 'update':
|
||||
case 'valueUpdate':
|
||||
this.#change$$.next(true); // 元素更新后更新
|
||||
break;
|
||||
|
||||
case 'active':
|
||||
case 'inactive':
|
||||
this.#change$$.next(false); // 选择状态变化
|
||||
break;
|
||||
|
||||
case 'click':
|
||||
case 'mousedown':
|
||||
case 'mouseup':
|
||||
// 将鼠标事件传递给事件流
|
||||
this.#mouse$$.next({ type: e, value: pick(v, 'x', 'y') });
|
||||
break;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 7. 场景保存原理
|
||||
|
||||
### 7.1 保存入口函数
|
||||
|
||||
```typescript
|
||||
public save(): string {
|
||||
// 1. 构建标准场景对象
|
||||
const scene: StandardScene = {
|
||||
robotGroups: this.robotGroups.value,
|
||||
robots: this.robots,
|
||||
// 2. 将画布上的图形对象转换为标准格式
|
||||
points: this.points.value.map((v) => this.#mapScenePoint(v)).filter((v) => !isNil(v)),
|
||||
routes: this.routes.value.map((v) => this.#mapSceneRoute(v)).filter((v) => !isNil(v)),
|
||||
areas: this.areas.value.map((v) => this.#mapSceneArea(v)).filter((v) => !isNil(v)), // ⭐
|
||||
blocks: [],
|
||||
};
|
||||
|
||||
// 3. 序列化为 JSON 字符串
|
||||
return JSON.stringify(scene);
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 区域数据映射
|
||||
|
||||
```typescript
|
||||
#mapSceneArea(pen: MapPen): StandardSceneArea | null {
|
||||
if (!pen.id || isEmpty(pen.area)) return null;
|
||||
|
||||
// 1. 提取基础信息
|
||||
const { id, label, desc, properties } = pen;
|
||||
const { type, points, routes } = pen.area;
|
||||
|
||||
// 2. 获取区域的实际位置和尺寸
|
||||
const { x, y, width, height } = this.getPenRect(pen);
|
||||
|
||||
// 3. 构建标准区域对象
|
||||
const area: StandardSceneArea = {
|
||||
id,
|
||||
name: label || id,
|
||||
desc,
|
||||
x, // 左上角 X 坐标
|
||||
y, // 左上角 Y 坐标
|
||||
w: width, // 宽度
|
||||
h: height, // 高度
|
||||
type, // 区域类型
|
||||
config: {},
|
||||
properties,
|
||||
};
|
||||
|
||||
// 4. 根据区域类型设置关联数据
|
||||
if (MapAreaType.库区 === type) {
|
||||
// 库区只保存动作点
|
||||
area.points = points?.filter((v) =>
|
||||
this.getPenById(v)?.point?.type === MapPointType.动作点
|
||||
);
|
||||
}
|
||||
|
||||
if ([MapAreaType.互斥区, MapAreaType.非互斥区].includes(type)) {
|
||||
// 互斥区和非互斥区保存所有非禁行点
|
||||
area.points = points?.filter((v) => {
|
||||
const { point } = this.getPenById(v) ?? {};
|
||||
if (isNil(point)) return false;
|
||||
if (point.type === MapPointType.禁行点) return false;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
if (MapAreaType.互斥区 === type) {
|
||||
// 互斥区还要保存关联的路线
|
||||
area.routes = routes?.filter((v) => !isEmpty(this.getPenById(v)?.area));
|
||||
}
|
||||
|
||||
return area;
|
||||
}
|
||||
```
|
||||
|
||||
## 8. 机器人管理系统
|
||||
|
||||
### 8.1 机器人数据结构
|
||||
|
||||
```typescript
|
||||
// 机器人映射表(响应式)
|
||||
readonly #robotMap = reactive<Map<RobotInfo['id'], RobotInfo>>(new Map());
|
||||
|
||||
// 机器人组流
|
||||
readonly #robotGroups$$ = new BehaviorSubject<RobotGroup[]>([]);
|
||||
public readonly robotGroups = useObservable<RobotGroup[]>(
|
||||
this.#robotGroups$$.pipe(debounceTime(300))
|
||||
);
|
||||
```
|
||||
|
||||
### 8.2 实时机器人更新
|
||||
|
||||
```typescript
|
||||
public refreshRobot(id: RobotInfo['id'], info: Partial<RobotRealtimeInfo>): void {
|
||||
const pen = this.getPenById(id);
|
||||
const { rotate: or, robot } = pen ?? {};
|
||||
if (!robot?.type) return;
|
||||
|
||||
// 1. 获取当前位置
|
||||
const { x: ox, y: oy } = this.getPenRect(pen!);
|
||||
|
||||
// 2. 解析实时数据
|
||||
const { x: cx = 37, y: cy = 37, active, angle, path: points } = info;
|
||||
|
||||
// 3. 计算新位置(机器人中心点偏移)
|
||||
const x = cx - 37;
|
||||
const y = cy - 37;
|
||||
const rotate = angle ?? or;
|
||||
|
||||
// 4. 处理路径数据
|
||||
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 }));
|
||||
|
||||
// 5. 更新机器人状态
|
||||
const o = { ...robot, ...omitBy({ active, path }, isNil) };
|
||||
|
||||
// 6. 根据激活状态使用不同的更新策略
|
||||
if (isNil(active)) {
|
||||
// 只更新位置和路径
|
||||
this.setValue(
|
||||
{ id, x, y, rotate, robot: o, visible: true },
|
||||
{ render: true, history: false, doEvent: false }
|
||||
);
|
||||
} else {
|
||||
// 同时更新图片资源
|
||||
this.setValue(
|
||||
{
|
||||
id,
|
||||
...this.#mapRobotImage(robot.type, active),
|
||||
x, y, rotate, robot: o, visible: true
|
||||
},
|
||||
{ render: true, history: false, doEvent: false }
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 9. 点位和路线管理
|
||||
|
||||
### 9.1 点位创建
|
||||
|
||||
```typescript
|
||||
public async addPoint(p: Point, type = MapPointType.普通点, id?: string): Promise<void> {
|
||||
id ||= s8();
|
||||
|
||||
// 1. 创建点位图形对象
|
||||
const pen: MapPen = {
|
||||
...p, // 坐标
|
||||
...this.#mapPoint(type), // 尺寸配置
|
||||
...this.#mapPointImage(type), // 图片配置
|
||||
id,
|
||||
name: 'point',
|
||||
tags: ['point'],
|
||||
label: `P${id}`,
|
||||
point: { type },
|
||||
};
|
||||
|
||||
// 2. 调整坐标到中心点
|
||||
pen.x! -= pen.width! / 2;
|
||||
pen.y! -= pen.height! / 2;
|
||||
|
||||
// 3. 添加到画布
|
||||
await this.addPen(pen, false, true, true);
|
||||
}
|
||||
```
|
||||
|
||||
### 9.2 路线创建
|
||||
|
||||
```typescript
|
||||
public addRoute(p: [MapPen, MapPen], type = MapRouteType.直线, id?: string): void {
|
||||
const [p1, p2] = p;
|
||||
if (!p1?.anchors?.length || !p2?.anchors?.length) return;
|
||||
|
||||
// 1. 连接两个点位
|
||||
const line = this.connectLine(p1, p2, undefined, undefined, false);
|
||||
|
||||
// 2. 设置ID
|
||||
id ||= line.id!;
|
||||
this.changePenId(line.id!, id);
|
||||
|
||||
// 3. 设置路线属性
|
||||
const pen: MapPen = { tags: ['route'], route: { type }, lineWidth: 1 };
|
||||
this.setValue({ id, ...pen }, { render: false, history: false, doEvent: false });
|
||||
|
||||
// 4. 更新线条类型
|
||||
this.updateLineType(line, type);
|
||||
|
||||
// 5. 选中并渲染
|
||||
this.active(id);
|
||||
this.render();
|
||||
}
|
||||
```
|
||||
|
||||
## 10. 主题系统集成
|
||||
|
||||
### 10.1 主题响应
|
||||
|
||||
```typescript
|
||||
// 监听主题变化
|
||||
watch(
|
||||
() => sTheme.theme,
|
||||
(v) => this.#load(v),
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
#load(theme: string): void {
|
||||
// 1. 设置 Meta2d 主题
|
||||
this.setTheme(theme);
|
||||
|
||||
// 2. 更新编辑器配置
|
||||
this.setOptions({ color: get(sTheme.editor, 'color') });
|
||||
|
||||
// 3. 更新所有点位图片
|
||||
this.find('point').forEach((pen) => {
|
||||
if (!pen.point?.type) return;
|
||||
if (pen.point.type < 10) return;
|
||||
this.canvas.updateValue(pen, this.#mapPointImage(pen.point.type));
|
||||
});
|
||||
|
||||
// 4. 更新所有机器人图片
|
||||
this.find('robot').forEach((pen) => {
|
||||
if (!pen.robot?.type) return;
|
||||
this.canvas.updateValue(pen, this.#mapRobotImage(pen.robot.type, pen.robot.active));
|
||||
});
|
||||
|
||||
// 5. 重新渲染
|
||||
this.render();
|
||||
}
|
||||
```
|
||||
|
||||
## 11. 性能优化策略
|
||||
|
||||
### 11.1 防抖处理
|
||||
|
||||
```typescript
|
||||
// 所有响应式数据都使用防抖
|
||||
debounceTime(100); // 100ms 防抖
|
||||
debounceTime(300); // 300ms 防抖(机器人组)
|
||||
```
|
||||
|
||||
### 11.2 浅层响应式
|
||||
|
||||
```typescript
|
||||
// 使用 shallowRef 避免深度响应式
|
||||
const editor = shallowRef<EditorService>();
|
||||
```
|
||||
|
||||
### 11.3 并行处理
|
||||
|
||||
```typescript
|
||||
// 场景加载时并行处理
|
||||
await Promise.all(
|
||||
areas.map(async (v) => {
|
||||
await this.addArea(/* ... */);
|
||||
}),
|
||||
);
|
||||
```
|
||||
|
||||
## 12. 总结
|
||||
|
||||
### 12.1 画区域的完整流程
|
||||
|
||||
1. **工具选择**:用户点击工具栏的区域工具,设置 `mode`
|
||||
2. **鼠标监听**:`mouseBrush` 流监听鼠标拖拽事件
|
||||
3. **坐标获取**:获取拖拽的起始点和结束点
|
||||
4. **区域创建**:调用 `addArea` 方法创建区域对象
|
||||
5. **画布绘制**:`drawArea` 函数在 Canvas 上绘制实际图形
|
||||
6. **状态更新**:触发响应式数据更新,通知 Vue 组件
|
||||
|
||||
### 12.2 场景文件生成区域的完整流程
|
||||
|
||||
1. **文件解析**:将 JSON 字符串解析为 `StandardScene` 对象
|
||||
2. **数据提取**:从 `areas` 数组中提取每个区域的信息
|
||||
3. **图形创建**:调用 `addArea` 方法在画布上创建图形对象
|
||||
4. **属性设置**:通过 `setValue` 设置业务属性
|
||||
5. **绘制渲染**:自定义绘制函数在 Canvas 上渲染图形
|
||||
|
||||
### 12.3 架构优势
|
||||
|
||||
1. **分层设计**:业务逻辑与图形引擎分离
|
||||
2. **响应式驱动**:状态变化自动更新 UI
|
||||
3. **事件流处理**:RxJS 提供强大的异步事件处理
|
||||
4. **自定义绘制**:完全控制图形的渲染效果
|
||||
5. **类型安全**:TypeScript 提供完整的类型检查
|
||||
|
||||
这个编辑器服务是一个设计精良的复杂系统,通过合理的架构设计实现了强大的场景编辑功能。
|
524
docs/项目说明文档.md
524
docs/项目说明文档.md
@ -1,524 +0,0 @@
|
||||
# AMR机器人管理系统 - 详细说明文档
|
||||
|
||||
## 📋 项目概述
|
||||
|
||||
AMR(Autonomous Mobile Robot)机器人管理系统是一个基于Vue3 + TypeScript + Ant Design Vue开发的前端应用。该系统主要用于管理和监控AMR机器人,提供场景编辑、机器人组管理、路径规划和实时监控等功能。
|
||||
|
||||
### 🎯 核心功能
|
||||
|
||||
- **场景编辑器** - 可视化编辑机器人运行场景地图
|
||||
- **机器人组管理** - 管理机器人分组和配置
|
||||
- **路径规划** - 设计和管理机器人运行路径
|
||||
- **实时监控** - 监控机器人运行状态和位置
|
||||
- **点位管理** - 管理充电点、停靠点、动作点等特殊点位
|
||||
- **区域管理** - 管理库区、互斥区等功能区域
|
||||
|
||||
## 🏗️ 技术架构
|
||||
|
||||
### 前端技术栈
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
A[Vue 3.5.13] --> B[TypeScript]
|
||||
A --> C[Ant Design Vue 4.2.6]
|
||||
A --> D[Vue Router 4.5.0]
|
||||
A --> E[Vue I18n 11.1.3]
|
||||
A --> F[Vite 6.3.1]
|
||||
|
||||
B --> G[RxJS 7.8.2]
|
||||
C --> H[@ant-design/icons-vue]
|
||||
D --> I[动态路由]
|
||||
E --> J[多语言支持]
|
||||
F --> K[热重载开发]
|
||||
|
||||
L[Meta2d 1.0.78] --> M[2D图形引擎]
|
||||
N[VueUse] --> O[组合式API工具]
|
||||
P[Lodash-es] --> Q[工具函数库]
|
||||
R[Axios] --> S[HTTP客户端]
|
||||
T[Day.js] --> U[时间处理]
|
||||
```
|
||||
|
||||
### 项目结构图
|
||||
|
||||
```
|
||||
web-amr/
|
||||
├── 📁 src/ # 源代码目录
|
||||
│ ├── 📁 pages/ # 页面组件
|
||||
│ │ ├── 🎨 scene-editor.vue # 场景编辑器页面
|
||||
│ │ ├── 🎨 group-editor.vue # 机器人组编辑页面
|
||||
│ │ ├── 📊 movement-supervision.vue # 运行监控页面
|
||||
│ │ └── ❌ exception.vue # 异常页面
|
||||
│ ├── 📁 components/ # 通用组件
|
||||
│ │ ├── 📁 modal/ # 弹窗组件
|
||||
│ │ ├── 📁 card/ # 卡片组件
|
||||
│ │ ├── 🤖 robot-groups.vue # 机器人组组件
|
||||
│ │ ├── 📍 pen-groups.vue # 点位组组件
|
||||
│ │ └── 🛠️ editor-toolbar.vue # 编辑器工具栏
|
||||
│ ├── 📁 apis/ # API接口层
|
||||
│ │ ├── 📁 scene/ # 场景相关API
|
||||
│ │ ├── 📁 robot/ # 机器人相关API
|
||||
│ │ └── 📁 map/ # 地图相关API
|
||||
│ ├── 📁 services/ # 核心服务层
|
||||
│ │ ├── ⚙️ editor.service.ts # 编辑器服务(核心)
|
||||
│ │ ├── 🎨 theme.service.ts # 主题服务
|
||||
│ │ ├── 🌍 locale.service.ts # 国际化服务
|
||||
│ │ ├── 🛤️ router.ts # 路由配置
|
||||
│ │ ├── 🌐 http.ts # HTTP请求服务
|
||||
│ │ └── 🔌 ws.ts # WebSocket服务
|
||||
│ ├── 📁 assets/ # 静态资源
|
||||
│ │ ├── 📁 themes/ # 主题文件
|
||||
│ │ ├── 📁 images/ # 图片资源
|
||||
│ │ ├── 📁 locales/ # 语言包
|
||||
│ │ ├── 📁 icons/ # 图标资源
|
||||
│ │ └── 📁 fonts/ # 字体文件
|
||||
│ ├── 🎯 main.ts # 应用入口
|
||||
│ ├── 📱 App.vue # 根组件
|
||||
│ └── 🎨 style.scss # 全局样式
|
||||
├── 📁 mocks/ # 模拟数据
|
||||
├── 📁 public/ # 公共资源
|
||||
├── ⚙️ vite.config.ts # Vite配置
|
||||
├── 📦 package.json # 项目配置
|
||||
└── 📝 README.md # 说明文档
|
||||
```
|
||||
|
||||
## 🔧 核心模块详细说明
|
||||
|
||||
### 1. 场景编辑器模块 (Scene Editor)
|
||||
|
||||
**功能描述**: 提供可视化的2D场景编辑功能,允许用户创建和修改机器人运行环境。
|
||||
|
||||
**技术实现**:
|
||||
|
||||
- 基于 `Meta2d` 图形引擎构建可视化编辑器
|
||||
- 使用 `EditorService` 服务类封装所有编辑功能
|
||||
- 支持点、线、面的绘制和编辑
|
||||
- 实时保存和加载场景配置
|
||||
|
||||
**核心组件**:
|
||||
|
||||
```vue
|
||||
<!-- 场景编辑器主页面 -->
|
||||
<template>
|
||||
<a-layout class="full">
|
||||
<!-- 顶部工具栏 -->
|
||||
<a-layout-header> <编辑模式切换> <导入导出功能> <推送到服务器> </a-layout-header>
|
||||
|
||||
<!-- 主体布局 -->
|
||||
<a-layout>
|
||||
<!-- 左侧面板 -->
|
||||
<a-layout-sider> <机器人组管理> <库区管理> <高级组管理> </a-layout-sider>
|
||||
|
||||
<!-- 中央编辑区 -->
|
||||
<a-layout-content>
|
||||
<div ref="container" class="editor-container"></div>
|
||||
</a-layout-content>
|
||||
</a-layout>
|
||||
</a-layout>
|
||||
|
||||
<!-- 右侧属性面板 -->
|
||||
<详细信息卡片>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 2. 机器人管理模块 (Robot Management)
|
||||
|
||||
**功能描述**: 管理机器人设备,包括注册、分组、状态监控等功能。
|
||||
|
||||
**数据结构**:
|
||||
|
||||
```typescript
|
||||
interface RobotInfo {
|
||||
id: string; // 机器人ID
|
||||
label: string; // 机器人名称
|
||||
brand: string; // 品牌
|
||||
type: RobotType; // 机器人类型
|
||||
ip: string; // IP地址
|
||||
isConnected?: boolean; // 连接状态
|
||||
state?: number; // 运行状态
|
||||
canOrder?: boolean; // 可否下达指令
|
||||
canStop?: boolean; // 可否停止
|
||||
canControl?: boolean; // 可否控制
|
||||
}
|
||||
|
||||
interface RobotGroup {
|
||||
sid: string; // 场景ID
|
||||
id: string; // 组ID
|
||||
label: string; // 组名称
|
||||
robots: string[]; // 机器人ID列表
|
||||
}
|
||||
```
|
||||
|
||||
**核心功能实现**:
|
||||
|
||||
- **机器人注册**: 通过表单收集机器人基本信息
|
||||
- **分组管理**: 支持创建、删除、重命名机器人组
|
||||
- **状态监控**: 实时显示机器人连接和运行状态
|
||||
- **批量操作**: 支持批量添加、移除机器人
|
||||
|
||||
### 3. 地图编辑系统 (Map Editor System)
|
||||
|
||||
**功能描述**: 基于Meta2d的强大地图编辑功能,支持多种地图元素的创建和编辑。
|
||||
|
||||
**地图元素类型**:
|
||||
|
||||
#### 📍 点位系统 (Point System)
|
||||
|
||||
```typescript
|
||||
enum MapPointType {
|
||||
普通点 = 0,
|
||||
充电点 = 1,
|
||||
停靠点 = 2,
|
||||
动作点 = 3,
|
||||
等待点 = 4,
|
||||
禁行点 = 5,
|
||||
}
|
||||
```
|
||||
|
||||
#### 🛤️ 路径系统 (Route System)
|
||||
|
||||
```typescript
|
||||
enum MapRouteType {
|
||||
直线 = 'line',
|
||||
二阶贝塞尔曲线 = 'bezier2',
|
||||
三阶贝塞尔曲线 = 'bezier3',
|
||||
}
|
||||
```
|
||||
|
||||
#### 🏢 区域系统 (Area System)
|
||||
|
||||
```typescript
|
||||
enum MapAreaType {
|
||||
库区 = 1,
|
||||
互斥区 = 2,
|
||||
非互斥区 = 3,
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 编辑器服务 (Editor Service)
|
||||
|
||||
**功能描述**: 系统核心服务类,继承自Meta2d,提供完整的编辑器功能。
|
||||
|
||||
**核心方法**:
|
||||
|
||||
```typescript
|
||||
class EditorService extends Meta2d {
|
||||
// 场景管理
|
||||
async load(map?: string, editable?: boolean): Promise<void>;
|
||||
save(): string;
|
||||
|
||||
// 机器人管理
|
||||
addRobots(gid: string, robots: RobotInfo[]): void;
|
||||
removeRobots(ids: string[]): void;
|
||||
updateRobot(id: string, value: Partial<RobotInfo>): void;
|
||||
|
||||
// 点位管理
|
||||
async addPoint(p: Point, type?: MapPointType, id?: string): Promise<void>;
|
||||
updatePoint(id: string, info: Partial<MapPointInfo>): void;
|
||||
changePointType(id: string, type: MapPointType): void;
|
||||
|
||||
// 路径管理
|
||||
addRoute(p: [string, string], type?: MapRouteType, id?: string): void;
|
||||
updateRoute(id: string, info: Partial<MapRouteInfo>): void;
|
||||
|
||||
// 区域管理
|
||||
async addArea(p1: Point, p2: Point, type?: MapAreaType, id?: string): Promise<void>;
|
||||
updateArea(id: string, info: Partial<MapAreaInfo>): void;
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 实时监控模块 (Movement Supervision)
|
||||
|
||||
**功能描述**: 实时监控机器人运行状态,显示位置、路径、状态等信息。
|
||||
|
||||
**技术实现**:
|
||||
|
||||
- 使用WebSocket建立实时连接
|
||||
- 基于RxJS处理实时数据流
|
||||
- Canvas渲染机器人实时位置和路径
|
||||
|
||||
**数据流程**:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as 客户端
|
||||
participant W as WebSocket服务
|
||||
participant S as 后端服务
|
||||
participant R as 机器人设备
|
||||
|
||||
C->>W: 建立WebSocket连接
|
||||
W->>S: 订阅机器人状态
|
||||
R->>S: 上报状态数据
|
||||
S->>W: 推送实时数据
|
||||
W->>C: 广播状态更新
|
||||
C->>C: 更新界面显示
|
||||
```
|
||||
|
||||
## 🎨 UI/UX设计特色
|
||||
|
||||
### 主题系统
|
||||
|
||||
- **双主题支持**: 明亮主题和暗黑主题
|
||||
- **动态切换**: 实时主题切换无需刷新
|
||||
- **一致性设计**: 基于Ant Design设计语言
|
||||
|
||||
### 响应式布局
|
||||
|
||||
```scss
|
||||
// 布局适配
|
||||
.full {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.editor-container {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
// 悬浮工具栏
|
||||
.toolbar-container {
|
||||
position: fixed;
|
||||
bottom: 40px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
```
|
||||
|
||||
### 交互体验
|
||||
|
||||
- **拖拽编辑**: 支持元素拖拽创建和编辑
|
||||
- **快捷键**: 常用操作快捷键支持
|
||||
- **上下文菜单**: 右键菜单功能
|
||||
- **实时预览**: 编辑结果实时反馈
|
||||
|
||||
## 🔗 API接口设计
|
||||
|
||||
### RESTful API结构
|
||||
|
||||
#### 场景管理API
|
||||
|
||||
```typescript
|
||||
// 获取场景详情
|
||||
POST /scene/getById
|
||||
Request: { id: string }
|
||||
Response: SceneDetail
|
||||
|
||||
// 保存场景
|
||||
POST /scene/saveById
|
||||
Request: { id: string; json: string; png?: string }
|
||||
|
||||
// 推送场景到数据库
|
||||
POST /scene/pushById
|
||||
Request: { id: string }
|
||||
|
||||
// 获取组场景
|
||||
POST /scene/getByGroupId
|
||||
Request: { id: string; sid: string }
|
||||
Response: GroupSceneDetail
|
||||
```
|
||||
|
||||
#### 机器人管理API
|
||||
|
||||
```typescript
|
||||
// 获取所有机器人
|
||||
POST / robot / getAll;
|
||||
Response: Array<RobotInfo>;
|
||||
|
||||
// 注册机器人
|
||||
POST / robot / register;
|
||||
Request: RobotDetail;
|
||||
Response: RobotInfo;
|
||||
```
|
||||
|
||||
#### WebSocket实时通信
|
||||
|
||||
```typescript
|
||||
// 场景监控WebSocket
|
||||
WebSocket /scene/monitor/:id
|
||||
|
||||
// 实时数据格式
|
||||
interface RobotRealtimeInfo {
|
||||
id: string;
|
||||
x: number; // X坐标
|
||||
y: number; // Y坐标
|
||||
angle: number; // 角度
|
||||
active: boolean; // 是否激活
|
||||
path: [number, number][]; // 路径轨迹
|
||||
}
|
||||
```
|
||||
|
||||
## 🚀 开发和部署
|
||||
|
||||
### 开发环境启动
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
npm install
|
||||
# 或
|
||||
pnpm install
|
||||
|
||||
# 启动开发服务器
|
||||
npm run start
|
||||
# 访问 http://localhost:8888
|
||||
```
|
||||
|
||||
### 构建和部署
|
||||
|
||||
```bash
|
||||
# 构建生产版本
|
||||
npm run build
|
||||
|
||||
# 预览构建结果
|
||||
npm run preview
|
||||
```
|
||||
|
||||
### 项目配置
|
||||
|
||||
#### Vite配置特色
|
||||
|
||||
```typescript
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
Components({
|
||||
dts: true,
|
||||
resolvers: [
|
||||
AntDesignVueResolver({
|
||||
importStyle: false,
|
||||
resolveIcons: true,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
|
||||
// 路径别名
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, 'src/pages/'),
|
||||
'@api': resolve(__dirname, 'src/apis/'),
|
||||
'@common': resolve(__dirname, 'src/components/'),
|
||||
'@core': resolve(__dirname, 'src/services/'),
|
||||
},
|
||||
},
|
||||
|
||||
// 开发服务器
|
||||
server: {
|
||||
port: 8888,
|
||||
host: true,
|
||||
proxy: {
|
||||
// API代理配置
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## 🔧 核心服务详解
|
||||
|
||||
### 1. 主题服务 (Theme Service)
|
||||
|
||||
- 支持明暗主题切换
|
||||
- 动态CSS变量管理
|
||||
- 主题配置持久化存储
|
||||
|
||||
### 2. 国际化服务 (Locale Service)
|
||||
|
||||
- 多语言支持框架
|
||||
- 动态语言切换
|
||||
- 语言包懒加载
|
||||
|
||||
### 3. HTTP服务 (HTTP Service)
|
||||
|
||||
- 基于Axios的请求封装
|
||||
- 请求/响应拦截器
|
||||
- 错误统一处理
|
||||
|
||||
### 4. WebSocket服务 (WS Service)
|
||||
|
||||
- 实时通信连接管理
|
||||
- 自动重连机制
|
||||
- 消息队列处理
|
||||
|
||||
## 📈 性能优化策略
|
||||
|
||||
### 代码分割
|
||||
|
||||
- 路由级别懒加载
|
||||
- 组件按需导入
|
||||
- 第三方库代码分割
|
||||
|
||||
### 构建优化
|
||||
|
||||
```typescript
|
||||
build: {
|
||||
target: 'es2020',
|
||||
outDir: 'dist',
|
||||
sourcemap: false,
|
||||
minify: 'esbuild',
|
||||
chunkSizeWarningLimit: 2000
|
||||
}
|
||||
```
|
||||
|
||||
### 运行时优化
|
||||
|
||||
- 虚拟滚动大列表
|
||||
- 图片懒加载
|
||||
- 防抖节流处理
|
||||
- 内存泄漏防护
|
||||
|
||||
## 🛡️ 代码质量保障
|
||||
|
||||
### 类型安全
|
||||
|
||||
- 完整的TypeScript类型定义
|
||||
- 严格的类型检查配置
|
||||
- 接口类型统一管理
|
||||
|
||||
### 代码规范
|
||||
|
||||
```json
|
||||
{
|
||||
"eslint": "代码质量检查",
|
||||
"prettier": "代码格式化",
|
||||
"stylelint": "样式规范检查",
|
||||
"husky": "Git钩子管理"
|
||||
}
|
||||
```
|
||||
|
||||
### 测试策略
|
||||
|
||||
- 组件单元测试
|
||||
- API接口测试
|
||||
- E2E端到端测试
|
||||
|
||||
## 🔮 未来发展规划
|
||||
|
||||
### 功能扩展
|
||||
|
||||
- [ ] 3D场景编辑支持
|
||||
- [ ] AI路径优化算法
|
||||
- [ ] 多机器人协作调度
|
||||
- [ ] 实时视频监控集成
|
||||
|
||||
### 技术升级
|
||||
|
||||
- [ ] Vue 3.6+ 新特性应用
|
||||
- [ ] WebGL图形渲染优化
|
||||
- [ ] PWA渐进式Web应用
|
||||
- [ ] 微前端架构迁移
|
||||
|
||||
## 📞 技术支持
|
||||
|
||||
### 开发团队联系方式
|
||||
|
||||
- 技术负责人: [联系信息]
|
||||
- 项目经理: [联系信息]
|
||||
- 运维支持: [联系信息]
|
||||
|
||||
### 文档更新
|
||||
|
||||
- 最后更新时间: 2024年12月
|
||||
- 文档版本: v1.0.0
|
||||
- 维护状态: 积极维护中
|
||||
|
||||
---
|
||||
|
||||
**注意**: 本文档基于项目当前状态生成,随着项目发展会持续更新。如有疑问或建议,请联系开发团队。
|
Loading…
x
Reference in New Issue
Block a user