diff --git a/docs/Canvas绘制技术详解.md b/docs/Canvas绘制技术详解.md deleted file mode 100644 index 0a8cf91..0000000 --- a/docs/Canvas绘制技术详解.md +++ /dev/null @@ -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 实现了丰富的图形绘制功能,每个绘制函数都经过精心设计,既保证了视觉效果,又兼顾了性能表现。理解这些绘制原理对于进一步扩展和优化编辑器功能具有重要意义。 diff --git a/docs/Meta2D引擎作用详解.md b/docs/Meta2D引擎作用详解.md deleted file mode 100644 index a68e4c2..0000000 --- a/docs/Meta2D引擎作用详解.md +++ /dev/null @@ -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(); - - public readonly current = useObservable( - this.#change$$.pipe( - debounceTime(100), - map(() => 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 = 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): 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不是画笔,而是整个画室的管理系统!** diff --git a/docs/WebSocket增强服务技术设计文档.md b/docs/WebSocket增强服务技术设计文档.md deleted file mode 100644 index a4926f0..0000000 --- a/docs/WebSocket增强服务技术设计文档.md +++ /dev/null @@ -1,646 +0,0 @@ -# WebSocket增强服务技术设计文档 - -## 概述 - -本文档详细解释了 `src/services/ws.ts` 的技术设计思路、架构选择和实现细节。这个文件实现了一个增强的WebSocket服务,在保持原有接口不变的前提下,添加了心跳检测、自动重连、错误处理等企业级功能。 - -## 设计目标 - -### 主要目标 - -1. **零侵入性**:业务代码无需修改,完全透明的功能增强 -2. **企业级稳定性**:心跳检测、自动重连、错误恢复 -3. **可配置性**:全局配置,易于调整和优化 -4. **类型安全**:完整的TypeScript类型支持 -5. **内存安全**:正确的资源管理,防止内存泄漏 - -### 兼容性目标 - -- 保持原有 `create(path): Promise` 接口不变 -- 返回标准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( - type: K, - listener: (this: WebSocket, ev: WebSocketEventMap[K]) => any, - options?: boolean | AddEventListenerOptions -): void { - this.ws.addEventListener(type, listener, options); -} - -removeEventListener( - 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 - -**类型安全:** - -- 使用泛型 `` 确保事件类型正确 -- 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( - 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 { - 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` -2. **相同的返回类型**:返回Promise -3. **相同的使用方式**:业务代码无需任何修改 - -## 总结 - -### 技术选择总结 - -| 技术选择 | 原因 | 替代方案 | 为什么不选择替代方案 | -| ------------- | -------------------------------- | ------------ | ----------------------- | -| Class | 状态封装、方法绑定、生命周期管理 | 函数+闭包 | 复杂度高,类型支持差 | -| 包装器模式 | 控制创建时机、事件拦截 | 继承 | 无法在事件设置前拦截 | -| Private成员 | 封装、API稳定性、状态保护 | Public成员 | 容易被误用,状态不安全 | -| Getter/Setter | 透明代理、API兼容性 | 直接方法 | 不符合WebSocket API习惯 | -| 多定时器 | 职责分离、精确控制 | 单定时器 | 逻辑混乱,难以维护 | -| 状态标志 | 精确控制重连逻辑 | 仅依赖状态码 | WebSocket状态码限制多 | - -### 架构优势 - -1. **零侵入性**:业务代码完全无需修改 -2. **高可靠性**:多重保障确保连接稳定 -3. **高可维护性**:清晰的架构和完整的类型支持 -4. **高性能**:最小的性能开销 -5. **高扩展性**:易于添加新功能 - -### 最佳实践体现 - -1. **单一职责原则**:每个方法只负责一个功能 -2. **开闭原则**:对扩展开放,对修改封闭 -3. **依赖倒置原则**:依赖抽象(接口)而非具体实现 -4. **接口隔离原则**:用户只看到需要的接口 -5. **里氏替换原则**:增强版本完全可以替换原版本 - -这个实现展示了如何在保持向后兼容的同时,提供企业级的功能增强,是一个很好的渐进式增强的例子。 diff --git a/docs/前端开发文档-交付.md b/docs/前端开发文档-交付.md deleted file mode 100644 index 31d554d..0000000 --- a/docs/前端开发文档-交付.md +++ /dev/null @@ -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, - isImport = false, -): Promise { - 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; // 机器人组信息 - robots?: Array; // 机器人信息 - points?: Array; // 标准点位信息 - routes?: Array; // 标准线路信息 - areas?: Array; // 标准区域信息 - 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; // 绑定机器人ID集合 - actions?: Array; // 绑定动作点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; // 规划路径 - 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; // 额外属性 - activeAttrs?: Array; // 已激活的额外属性 - 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 -**文档状态**: 已完成 diff --git a/docs/场景编辑器组件详细分析.md b/docs/场景编辑器组件详细分析.md deleted file mode 100644 index 8bfc833..0000000 --- a/docs/场景编辑器组件详细分析.md +++ /dev/null @@ -1,2013 +0,0 @@ -# 场景编辑器组件详细分析 - -## 1. 组件概述 - -`scene-editor.vue` 是一个基于 Vue 3 的复杂场景编辑器组件,主要用于管理和编辑工业机器人的场景配置。该组件提供了完整的场景编辑功能,包括机器人管理、路径规划、区域设置等。 - -## 2. 架构图示分析 - -### 2.1 组件整体架构图 - -```mermaid -graph TB - subgraph "Scene Editor Component" - A[scene-editor.vue] --> B[EditorService] - A --> C[RobotGroups] - A --> D[PenGroups] - A --> E[EditorToolbar] - A --> F[Detail Cards] - - B --> B1[Meta2d Engine] - B --> B2[Canvas Rendering] - B --> B3[Event System] - - C --> C1[Robot Management] - C --> C2[Group Operations] - - D --> D1[Points Management] - D --> D2[Routes Management] - D --> D3[Areas Management] - - E --> E1[Drawing Tools] - E --> E2[Operations] - - F --> F1[Robot Detail] - F --> F2[Point Detail] - F --> F3[Route Detail] - F --> F4[Area Detail] - end - - subgraph "External Dependencies" - G[Scene API] - H[Robot API] - I[Map API] - J[File System] - end - - A --> G - A --> H - B --> I - A --> J -``` - -### 2.2 数据流架构图 - -```mermaid -sequenceDiagram - participant U as User - participant SE as SceneEditor - participant ES as EditorService - participant API as SceneAPI - participant Canvas as Meta2d Canvas - - U->>SE: Load Scene - SE->>API: getSceneById(id) - API-->>SE: Scene Data - SE->>ES: load(sceneData) - ES->>Canvas: Render Elements - - U->>SE: Edit Element - SE->>ES: updatePen/addArea/addPoint - ES->>Canvas: Update Render - ES->>SE: Emit Change Event - SE->>SE: Update UI State - - U->>SE: Save Scene - SE->>ES: save() - ES-->>SE: JSON String - SE->>API: saveSceneById(id, json) - API-->>SE: Success Response -``` - -### 2.3 EditorService 内部架构图 - -```mermaid -graph LR - subgraph "EditorService Core" - A[Meta2d Base] --> B[Event System] - A --> C[Canvas Layer] - A --> D[State Management] - - B --> B1[Mouse Events] - B --> B2[Change Events] - B --> B3[RxJS Streams] - - C --> C1[Point Rendering] - C --> C2[Route Rendering] - C --> C3[Area Rendering] - C --> C4[Robot Rendering] - - D --> D1[Current Selection] - D --> D2[Robot Groups] - D --> D3[Elements Cache] - end - - subgraph "Rendering Pipeline" - E[drawPoint] --> F[Canvas Context] - G[drawLine] --> F - H[drawArea] --> F - I[drawRobot] --> F - end - - C1 --> E - C2 --> G - C3 --> H - C4 --> I -``` - -## 3. 核心功能分析 - -### 3.1 场景数据管理 - -- **场景读取**: 通过 `getSceneById` API 获取场景数据 -- **场景推送**: 通过 `pushSceneById` API 将场景数据推送到数据库 -- **场景保存**: 通过编辑器服务保存场景配置 -- **文件导入/导出**: 支持 `.scene` 格式文件的导入导出 - -### 3.2 编辑器状态控制 - -- **编辑模式切换**: 通过 `editable` 状态控制编辑器的启用/禁用 -- **权限管理**: 根据编辑状态显示不同的操作按钮和功能 -- **实时状态同步**: 编辑状态变化时自动更新编辑器服务状态 - -### 3.3 三大管理区域 - -- **机器人管理**: 显示和管理场景中的机器人组和单个机器人 -- **库区管理**: 管理各种类型的库区(仅显示库区类型的区域) -- **高级组管理**: 管理复杂的路径、点位、区域等元素 - -### 3.4 详情卡片系统 - -- **动态卡片显示**: 根据选中元素类型显示对应的详情卡片 -- **编辑/查看模式**: 根据编辑状态显示编辑卡片或查看卡片 -- **悬浮定位**: 卡片固定在右侧悬浮显示 - -## 4. 大场景渲染性能优化分析 - -### 4.1 性能瓶颈识别 - -#### 4.1.1 主要性能问题 - -```mermaid -graph TD - A[大场景性能问题] --> B[元素数量过多] - A --> C[频繁重绘] - A --> D[内存泄漏] - A --> E[事件处理] - - B --> B1[点位: 1000+ 个] - B --> B2[路线: 5000+ 条] - B --> B3[区域: 100+ 个] - B --> B4[机器人: 50+ 个] - - C --> C1[每次状态变更全量重绘] - C --> C2[鼠标移动频繁触发] - C --> C3[RxJS 防抖不足] - - D --> D1[大量 DOM 监听器] - D --> D2[Canvas 上下文未释放] - D --> D3[图片资源未缓存] - - E --> E1[hit-test 计算复杂] - E --> E2[事件冒泡处理] -``` - -#### 4.1.2 当前代码中的性能问题点 - -```typescript -// 问题1: 频繁的全量数据更新 -public readonly pens = useObservable( - this.#change$$.pipe( - filter((v) => v), - debounceTime(100), // 防抖时间过短 - map(() => this.data().pens), // 每次返回全量数据 - ), -); - -// 问题2: 复杂的过滤操作 -const robots = computed(() => - editor.value.robots.filter(({ label }) => - label.includes(keyword.value) // 每次重新过滤全部数据 - ) -); - -// 问题3: 同步的大量元素创建 -async #loadScenePoints(points?: StandardScenePoint[]): Promise { - if (!points?.length) return; - await Promise.all( // 并发创建所有点位,可能导致界面卡顿 - points.map(async (v) => { - // ... 创建逻辑 - }), - ); -} -``` - -### 4.2 性能优化策略 - -#### 4.2.1 虚拟化渲染优化 - -```typescript -// 优化建议1: 实现视口裁剪 -class ViewportCulling { - private viewport: Rect; - private visibleElements: Map = new Map(); - - updateViewport(viewport: Rect): void { - this.viewport = viewport; - this.updateVisibleElements(); - } - - private updateVisibleElements(): void { - const elements = this.getAllElements(); - this.visibleElements.clear(); - - elements.forEach((element) => { - if (this.isInViewport(element)) { - this.visibleElements.set(element.id, element); - } - }); - } - - private isInViewport(element: MapPen): boolean { - const elementRect = this.getElementRect(element); - return this.rectIntersects(this.viewport, elementRect); - } -} - -// 优化建议2: 层级渲染 -class LayeredRenderer { - private staticLayer: CanvasRenderingContext2D; // 静态elementos(点位、区域) - private dynamicLayer: CanvasRenderingContext2D; // 动态elementos(机器人、路径) - private uiLayer: CanvasRenderingContext2D; // UI层(选中状态、工具提示) - - render(): void { - this.renderStaticLayer(); // 仅在元素变更时重绘 - this.renderDynamicLayer(); // 频繁重绘 - this.renderUILayer(); // 交互时重绘 - } -} -``` - -#### 4.2.2 数据结构优化 - -```typescript -// 优化建议3: 使用空间索引 -class SpatialIndex { - private quadTree: QuadTree; - - insert(element: MapPen): void { - const bounds = this.getElementBounds(element); - this.quadTree.insert(element, bounds); - } - - query(viewport: Rect): MapPen[] { - return this.quadTree.query(viewport); - } -} - -// 优化建议4: 缓存计算结果 -class RenderCache { - private pathCache: Map = new Map(); - private imageCache: Map = new Map(); - - getPath(element: MapPen): Path2D { - const key = this.getPathKey(element); - if (!this.pathCache.has(key)) { - this.pathCache.set(key, this.createPath(element)); - } - return this.pathCache.get(key)!; - } -} -``` - -#### 4.2.3 事件处理优化 - -```typescript -// 优化建议5: 事件委托和节流 -class EventOptimizer { - private mouseThrottle = throttle(this.handleMouseMove.bind(this), 16); // 60fps - - setupEventListeners(): void { - // 使用事件委托,减少监听器数量 - this.canvas.addEventListener('mousemove', this.mouseThrottle); - this.canvas.addEventListener('click', this.handleClick); - } - - private handleMouseMove(event: MouseEvent): void { - // 只处理必要的鼠标移动事件 - const elements = this.spatialIndex.query(this.getMouseViewport(event)); - this.updateHoverState(elements); - } -} -``` - -#### 4.2.4 内存管理优化 - -```typescript -// 优化建议6: 对象池模式 -class ObjectPool { - private pool: T[] = []; - - acquire(): T { - return this.pool.pop() || this.create(); - } - - release(obj: T): void { - this.reset(obj); - this.pool.push(obj); - } -} - -// 优化建议7: 及时清理资源 -class ResourceManager { - private observers: Set<() => void> = new Set(); - - cleanup(): void { - this.observers.forEach((cleanup) => cleanup()); - this.observers.clear(); - - // 清理Canvas上下文 - this.clearCanvasContexts(); - - // 清理图片缓存 - this.clearImageCache(); - } -} -``` - -### 4.3 具体优化实施建议 - -#### 4.3.1 分批加载策略 - -```typescript -// 建议实现: 分批加载大量元素 -async loadSceneInBatches(scene: StandardScene): Promise { - const BATCH_SIZE = 100; - - // 分批加载点位 - if (scene.points?.length) { - for (let i = 0; i < scene.points.length; i += BATCH_SIZE) { - const batch = scene.points.slice(i, i + BATCH_SIZE); - await this.loadPointsBatch(batch); - await this.nextTick(); // 让出主线程 - } - } - - // 分批加载路线 - if (scene.routes?.length) { - for (let i = 0; i < scene.routes.length; i += BATCH_SIZE) { - const batch = scene.routes.slice(i, i + BATCH_SIZE); - await this.loadRoutesBatch(batch); - await this.nextTick(); - } - } -} - -private nextTick(): Promise { - return new Promise(resolve => setTimeout(resolve, 0)); -} -``` - -#### 4.3.2 LOD(Level of Detail)优化 - -```typescript -// 建议实现: 根据缩放级别调整渲染详度 -class LODRenderer { - private renderLevel = 0; - - updateRenderLevel(scale: number): void { - if (scale > 2) - this.renderLevel = 3; // 高详度 - else if (scale > 1) - this.renderLevel = 2; // 中详度 - else if (scale > 0.5) - this.renderLevel = 1; // 低详度 - else this.renderLevel = 0; // 最低详度 - } - - renderPoint(ctx: CanvasRenderingContext2D, pen: MapPen): void { - switch (this.renderLevel) { - case 0: - this.renderPointSimple(ctx, pen); - break; - case 1: - this.renderPointNormal(ctx, pen); - break; - case 2: - this.renderPointDetailed(ctx, pen); - break; - case 3: - this.renderPointHighDetail(ctx, pen); - break; - } - } -} -``` - -## 5. 异步区域绘制深层原因分析 - -### 5.1 为什么使用 async/await - -#### 5.1.1 代码分析 - -```typescript -// 关键代码段分析 -public async addArea(p1: Point, p2: Point, type = MapAreaType.库区, id?: string) { - // ... 前置逻辑 - const area = await this.addPen(pen, true, true, true); - // ↑ 这里是关键 - addPen 是异步的 - this.bottom(area); -} - -public async addPoint(p: Point, type = MapPointType.普通点, id?: string): Promise { - // ... 创建pen对象 - await this.addPen(pen, false, true, true); - // ↑ 同样是异步调用 -} -``` - -#### 5.1.2 深层原因分析图 - -```mermaid -graph TD - A[addArea/addPoint 调用] --> B[创建 MapPen 对象] - B --> C[调用 addPen 方法] - C --> D{addPen 为什么异步?} - - D --> E[Canvas 渲染队列] - D --> F[DOM 更新时机] - D --> G[图片资源加载] - D --> H[动画系统集成] - - E --> E1[Canvas需要等待渲染完成] - E --> E2[避免渲染冲突] - E --> E3[批量更新优化] - - F --> F1[等待浏览器重绘] - F --> F2[确保DOM状态同步] - - G --> G1[点位图标加载] - G --> G2[机器人图片加载] - G --> G3[主题相关资源] - - H --> H1[过渡动画] - H --> H2[缩放动画] - H --> H3[状态切换动画] -``` - -### 5.2 Meta2d 底层机制分析 - -#### 5.2.1 addPen 异步的根本原因 - -```typescript -// Meta2d 内部可能的实现机制 (推测) -class Meta2d { - async addPen(pen: Pen, history?: boolean, render?: boolean, doEvent?: boolean): Promise { - // 1. 资源预加载 - 确保图片等资源准备就绪 - if (pen.image) { - await this.loadImage(pen.image); - } - - // 2. 渲染管道同步 - 等待当前渲染任务完成 - await this.renderQueue.nextTick(); - - // 3. 添加到画布数据结构 - this.store.data.pens.push(pen); - - // 4. 计算布局和碰撞检测 - await this.calculateLayout(pen); - - // 5. 触发重绘 - if (render) { - await this.render(); - } - - // 6. 触发事件 - if (doEvent) { - this.emit('add', pen); - } - - return pen; - } - - private async loadImage(src: string): Promise { - return new Promise((resolve, reject) => { - const img = new Image(); - img.onload = () => resolve(img); - img.onerror = reject; - img.src = src; - }); - } - - private async calculateLayout(pen: Pen): Promise { - // 复杂的布局计算可能需要多帧完成 - return new Promise((resolve) => { - requestAnimationFrame(() => { - this.updatePenBounds(pen); - this.updateSpatialIndex(pen); - resolve(); - }); - }); - } -} -``` - -#### 5.2.2 异步的必要性分析 - -```mermaid -sequenceDiagram - participant User as User Action - participant Service as EditorService - participant Meta2d as Meta2d Engine - participant Canvas as Canvas Context - participant Browser as Browser - - User->>Service: addArea() - Service->>Meta2d: addPen(pen) - - Note over Meta2d: 检查是否需要加载图片资源 - Meta2d->>Browser: 加载图片 (async) - Browser-->>Meta2d: 图片加载完成 - - Note over Meta2d: 等待渲染队列空闲 - Meta2d->>Meta2d: 添加到数据结构 - - Note over Meta2d: 计算元素边界和布局 - Meta2d->>Canvas: 请求重绘 - Canvas->>Browser: requestAnimationFrame - Browser-->>Canvas: 下一帧回调 - - Meta2d-->>Service: 返回创建的元素 - Service->>Service: 调用 bottom() 设置层级 -``` - -### 5.3 性能影响分析 - -#### 5.3.1 异步的性能优势 - -```typescript -// 优势1: 避免阻塞主线程 -// 同步版本 (假设的问题版本) -public addAreaSync(p1: Point, p2: Point): void { - const pen = this.createPen(); - this.addPenSync(pen); // 会阻塞主线程 - this.render(); // 立即渲染,可能导致卡顿 -} - -// 异步版本 (当前实现) -public async addArea(p1: Point, p2: Point): Promise { - const pen = this.createPen(); - await this.addPen(pen); // 非阻塞,允许其他任务执行 - this.bottom(pen); // 确保pen已经正确添加后再操作 -} -``` - -#### 5.3.2 批量操作的性能考虑 - -```typescript -// 当前的批量加载实现 -async #loadSceneAreas(areas?: StandardSceneArea[]): Promise { - if (!areas?.length) return; - await Promise.all( // 并发执行,但可能导致资源竞争 - areas.map(async (v) => { - await this.addArea({ x: v.x, y: v.y }, { x: v.x + v.w, y: v.y + v.h }, v.type, v.id); - }), - ); -} - -// 优化建议: 控制并发数量 -async #loadSceneAreasOptimized(areas?: StandardSceneArea[]): Promise { - if (!areas?.length) return; - - const CONCURRENT_LIMIT = 5; // 限制并发数量 - for (let i = 0; i < areas.length; i += CONCURRENT_LIMIT) { - const batch = areas.slice(i, i + CONCURRENT_LIMIT); - await Promise.all( - batch.map(async (v) => { - await this.addArea( - { x: v.x, y: v.y }, - { x: v.x + v.w, y: v.y + v.h }, - v.type, - v.id - ); - }) - ); - // 每批次之间给主线程喘息时间 - await new Promise(resolve => setTimeout(resolve, 10)); - } -} -``` - -## 6. 技术架构分析 - -### 6.1 核心依赖关系 - -```typescript -// 主要导入依赖 -import { getSceneById, pushSceneById } from '@api/scene'; // 场景API -import { EditorService } from '@core/editor.service'; // 编辑器服务 -import { decodeTextFile, downloadFile, selectFile, textToBlob } from '@core/utils'; // 工具函数 -``` - -### 6.2 组件架构设计 - -#### 6.2.1 状态管理 - -```typescript -// 核心状态定义 -const title = ref(''); // 场景标题 -const editable = ref(false); // 编辑状态 -const show = ref(true); // 卡片显示状态 -const current = ref<{ type: string; id: string }>(); // 当前选中元素 -const container = shallowRef(); // 编辑器容器 -const editor = shallowRef(); // 编辑器服务实例 -``` - -#### 6.2.2 依赖注入系统 - -```typescript -const EDITOR_KEY = Symbol('editor-key'); -provide(EDITOR_KEY, editor); -``` - -使用 Vue 3 的依赖注入机制,将编辑器服务注入到子组件中。 - -### 6.3 EditorService 核心服务分析 - -#### 6.3.1 服务基础 - -```typescript -export class EditorService extends Meta2d { - // 继承自 Meta2d 图形引擎 - // 提供场景编辑的核心功能 -} -``` - -#### 6.3.2 核心方法 - -- **load()**: 加载场景数据到编辑器 -- **save()**: 保存当前场景数据 -- **setState()**: 设置编辑器状态(可编辑/只读) -- **updateRobots()**: 更新机器人数据 -- **addArea()**: 添加区域 -- **deleteById()**: 删除指定元素 - -### 6.4 API 接口设计 - -#### 6.4.1 场景相关API - -```typescript -// 获取场景数据 -export async function getSceneById(id: string): Promise; - -// 推送场景到数据库 -export async function pushSceneById(id: string): Promise; - -// 保存场景数据 -export async function saveSceneById(id: string, json: string, png?: string): Promise; -``` - -#### 6.4.2 文件操作工具 - -```typescript -// 文件选择 -export async function selectFile(accept?: string, limit?: number): Promise; - -// 文件解码 -export async function decodeTextFile(file: File): Promise; - -// 文本转二进制 -export function textToBlob(text: string): Blob | undefined; - -// 文件下载 -export function downloadFile(url: string, name?: string): void; -``` - -## 7. 从零开发实现过程 - -### 7.1 第一步:创建基础组件结构 - -```vue - - - -``` - -### 7.2 第二步:集成编辑器服务 - -```typescript -// 1. 导入编辑器服务 -import { EditorService } from '@core/editor.service'; - -// 2. 创建编辑器实例 -const container = shallowRef(); -const editor = shallowRef(); - -// 3. 组件挂载时初始化编辑器 -onMounted(() => { - editor.value = new EditorService(container.value!); -}); - -// 4. 设置依赖注入 -const EDITOR_KEY = Symbol('editor-key'); -provide(EDITOR_KEY, editor); -``` - -### 7.3 第三步:实现场景数据管理 - -```typescript -// 1. 导入API -import { getSceneById, pushSceneById } from '@api/scene'; - -// 2. 读取场景数据 -const readScene = async () => { - const res = await getSceneById(props.id); - title.value = res?.label ?? ''; - editor.value?.load(res?.json, editable.value); -}; - -// 3. 推送场景数据 -const pushScene = async () => { - const res = await pushSceneById(props.id); - if (!res) return Promise.reject(); - message.success(t('场景推送成功')); - return Promise.resolve(); -}; - -// 4. 监听场景ID变化 -watch( - () => props.id, - () => readScene(), - { immediate: true, flush: 'post' }, -); -``` - -### 7.4 第四步:实现文件导入导出 - -```typescript -// 1. 导入工具函数 -import { decodeTextFile, downloadFile, selectFile, textToBlob } from '@core/utils'; - -// 2. 导入场景文件 -const importScene = async () => { - const file = await selectFile('.scene'); - if (!file?.size) return; - const json = await decodeTextFile(file); - editor.value?.load(json, editable.value); -}; - -// 3. 导出场景文件 -const exportScene = () => { - const json = editor.value?.save(); - if (!json) return; - const blob = textToBlob(json); - if (!blob?.size) return; - const url = URL.createObjectURL(blob); - downloadFile(url, `${title.value || 'unknown'}.scene`); - URL.revokeObjectURL(url); -}; -``` - -### 7.5 第五步:集成管理组件 - -```vue - -``` - -### 7.6 第六步:实现选中元素监听 - -```typescript -// 1. 监听编辑器选中元素 -watch( - () => editor.value?.selected.value[0], - (v) => { - const pen = editor.value?.getPenById(v); - if (pen?.id) { - current.value = { type: pen.name as 'point' | 'line' | 'area', id: pen.id }; - return; - } - if (current.value?.type === 'robot') return; - current.value = undefined; - }, -); - -// 2. 计算选中元素类型 -const isRobot = computed(() => current.value?.type === 'robot'); -const isPoint = computed(() => current.value?.type === 'point'); -const isRoute = computed(() => current.value?.type === 'line'); -const isArea = computed(() => current.value?.type === 'area'); - -// 3. 机器人选择处理 -const selectRobot = (id: string) => { - current.value = { type: 'robot', id }; - editor.value?.inactive(); -}; -``` - -### 7.7 第七步:添加工具栏和详情卡片 - -```vue - -``` - -## 8. 子组件详细分析 - -### 8.1 RobotGroups 组件 - -**功能**: 管理机器人组和单个机器人 -**核心特性**: - -- 机器人组的增删改查 -- 机器人的添加、注册、移除 -- 批量操作支持(全选、批量移除) -- 搜索过滤功能 - -**关键实现**: - -```typescript -// 机器人列表获取 -const robots = computed(() => editor.value.robots.filter(({ label }) => label.includes(keyword.value))); - -// 批量选择管理 -const selected = reactive>(new Set()); -const selectAll = (checked: boolean) => { - if (checked) { - robots.value.forEach(({ id }) => selected.add(id)); - } else { - selected.clear(); - } -}; -``` - -### 8.2 PenGroups 组件 - -**功能**: 管理点位、路线、区域等绘制元素 -**核心特性**: - -- 分类显示不同类型的元素(点位、路线、区域) -- 支持筛选特定类型(如仅显示库区) -- 搜索过滤功能 -- 点击选中功能 - -**关键实现**: - -```typescript -// 点位列表 -const points = computed(() => - editor.value.points.value.filter(({ label }) => label?.includes(keyword.value)), -); - -// 区域列表(按类型分组) -const areas = computed(() => editor.value.areas.value.filter(({ label }) => label?.includes(keyword.value))); -``` - -### 8.3 EditorToolbar 组件 - -**功能**: 提供编辑工具栏 -**核心特性**: - -- 区域添加工具(库区、互斥区、非互斥区) -- 场景保存功能 -- 撤销/重做操作 -- 删除操作 - -**关键实现**: - -```typescript -// 区域添加模式 -const mode = ref(); -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; -}); -``` - -## 9. 样式设计分析 - -### 9.1 布局结构 - -- **头部**: 固定高度64px,包含标题和操作按钮 -- **主体**: 左侧面板320px宽度,右侧编辑器自适应 -- **工具栏**: 固定在底部中央,悬浮显示 -- **详情卡片**: 固定在右侧,320px宽度,悬浮显示 - -### 9.2 核心样式 - -```scss -.editor-container { - background-color: transparent !important; -} - -.toolbar-container { - position: fixed; - bottom: 40px; - left: 50%; - z-index: 100; - transform: translateX(-50%); -} - -.card-container { - position: fixed; - top: 80px; - right: 64px; - z-index: 100; - width: 320px; - height: calc(100% - 96px); - overflow: visible; - pointer-events: none; - - & > * { - pointer-events: all; - } -} -``` - -## 10. 维护和调试指南 - -### 10.1 常见问题排查 - -#### 问题1: 场景数据加载失败 - -**排查步骤**: - -1. 检查 `props.id` 是否正确传入 -2. 检查 `getSceneById` API 是否正常响应 -3. 检查编辑器服务是否正确初始化 - -#### 问题2: 编辑器功能异常 - -**排查步骤**: - -1. 检查 `container` 元素是否正确获取 -2. 检查 `EditorService` 是否正确实例化 -3. 检查依赖注入是否正常工作 - -#### 问题3: 文件导入导出失败 - -**排查步骤**: - -1. 检查工具函数是否正确导入 -2. 检查文件格式是否正确 -3. 检查浏览器兼容性 - -### 10.2 性能优化建议 - -1. **使用 shallowRef**: 对于大对象使用 `shallowRef` 避免深度响应式 -2. **组件懒加载**: 使用 `v-if` 控制组件渲染时机 -3. **事件防抖**: 对于频繁触发的事件(如搜索)使用防抖 -4. **内存管理**: 及时清理事件监听器和定时器 - -### 10.3 扩展开发指南 - -#### 添加新的元素类型 - -1. 在 `EditorService` 中添加对应的管理方法 -2. 在 `PenGroups` 组件中添加新的分组 -3. 创建对应的详情卡片组件 -4. 在主组件中添加类型判断逻辑 - -#### 添加新的工具 - -1. 在 `EditorToolbar` 组件中添加工具按钮 -2. 在 `EditorService` 中实现对应功能 -3. 处理工具状态管理和交互逻辑 - -## 11. 总结 - -这个场景编辑器组件是一个功能完整、架构清晰的复杂组件,主要特点: - -1. **模块化设计**: 通过子组件分离不同功能模块 -2. **服务化架构**: 核心逻辑封装在 EditorService 中 -3. **响应式状态管理**: 使用 Vue 3 的响应式系统管理复杂状态 -4. **依赖注入**: 通过 provide/inject 实现服务共享 -5. **文件操作**: 完整的文件导入导出功能 -6. **用户体验**: 良好的交互设计和视觉反馈 -7. **异步机制**: 合理使用异步操作确保渲染性能 -8. **性能优化**: 针对大场景提供多层次优化策略 - -对于维护和扩展这个组件,需要重点关注: - -- EditorService 的 API 设计和实现 -- 各子组件之间的通信机制 -- 状态管理的一致性 -- 性能优化和内存管理 -- 异步操作的正确处理 - -## 12. 高性能技术栈替代方案分析 - -### 12.1 技术栈对比总览 - -```mermaid -graph LR - subgraph "当前方案" - A[Meta2d + Canvas 2D] --> A1[性能瓶颈] - A1 --> A2[大场景卡顿] - A1 --> A3[内存占用高] - A1 --> A4[CPU密集计算] - end - - subgraph "高性能替代方案" - B[WebGL方案] --> B1[GPU加速] - C[WebAssembly方案] --> C1[原生性能] - D[混合方案] --> D1[WebGL + WASM] - E[Web Workers] --> E1[多线程计算] - end - - A --> B - A --> C - A --> D - A --> E -``` - -### 12.2 WebGL高性能渲染方案 - -#### 12.2.1 推荐库选择 - -**1. PixiJS (推荐指数: ⭐⭐⭐⭐⭐)** - -```typescript -// PixiJS 实现高性能场景编辑器 -import * as PIXI from 'pixi.js'; - -class HighPerformanceSceneEditor { - private app: PIXI.Application; - private viewport: Viewport; - private spatialHash: SpatialHash; - private cullingSystem: CullingSystem; - - constructor(container: HTMLElement) { - // 创建高性能应用实例 - this.app = new PIXI.Application({ - width: container.clientWidth, - height: container.clientHeight, - antialias: true, - backgroundColor: 0x1099bb, - powerPreference: 'high-performance', // 强制使用独立显卡 - hello: false, // 禁用PIXI欢迎信息 - }); - - // 启用批量渲染和几何体缓存 - this.app.renderer.plugins.batch.setMaxTextures(32); - this.setupViewport(); - this.initOptimizations(); - } - - private setupViewport(): void { - // 使用 pixi-viewport 实现高性能视口 - this.viewport = new Viewport({ - screenWidth: this.app.screen.width, - screenHeight: this.app.screen.height, - worldWidth: 10000, - worldHeight: 10000, - interaction: this.app.renderer.plugins.interaction, - }); - - this.viewport.drag().pinch().wheel().decelerate().clampZoom({ minScale: 0.1, maxScale: 5 }); - } - - private initOptimizations(): void { - // 1. 启用视锥剔除 - this.cullingSystem = new CullingSystem(this.viewport); - - // 2. 空间哈希优化 - this.spatialHash = new SpatialHash(100); // 网格大小100px - - // 3. 对象池 - this.initObjectPools(); - - // 4. LOD系统 - this.initLODSystem(); - } - - // 批量添加大量元素 - 性能优化版本 - async addElementsBatch(elements: SceneElement[]): Promise { - const BATCH_SIZE = 1000; - const batches = this.chunkArray(elements, BATCH_SIZE); - - for (const batch of batches) { - // 使用 Graphics 批量绘制 - const graphics = new PIXI.Graphics(); - - batch.forEach((element) => { - this.drawElementToGraphics(graphics, element); - this.spatialHash.insert(element); - }); - - this.viewport.addChild(graphics); - - // 让出控制权,避免阻塞UI - await this.nextFrame(); - } - } - - private drawElementToGraphics(graphics: PIXI.Graphics, element: SceneElement): void { - graphics.beginFill(element.color); - switch (element.type) { - case 'point': - graphics.drawCircle(element.x, element.y, element.radius); - break; - case 'area': - graphics.drawRect(element.x, element.y, element.width, element.height); - break; - case 'route': - graphics.moveTo(element.x1, element.y1); - graphics.lineTo(element.x2, element.y2); - break; - } - graphics.endFill(); - } -} - -// 视锥剔除系统 -class CullingSystem { - private viewport: Viewport; - private visibleElements: Set = new Set(); - - constructor(viewport: Viewport) { - this.viewport = viewport; - viewport.on('moved', () => this.updateVisibility()); - viewport.on('zoomed', () => this.updateVisibility()); - } - - updateVisibility(): void { - const bounds = this.viewport.getVisibleBounds(); - - this.viewport.children.forEach((child) => { - const elementBounds = child.getBounds(); - const isVisible = this.boundsIntersect(bounds, elementBounds); - - if (isVisible && !child.visible) { - child.visible = true; - this.visibleElements.add(child); - } else if (!isVisible && child.visible) { - child.visible = false; - this.visibleElements.delete(child); - } - }); - } - - private boundsIntersect(a: PIXI.Rectangle, b: PIXI.Rectangle): boolean { - return !(a.x + a.width < b.x || b.x + b.width < a.x || a.y + a.height < b.y || b.y + b.height < a.y); - } -} -``` - -**性能提升预期**: - -- **渲染性能**: 10-50倍提升 (GPU加速) -- **内存使用**: 减少60-80% (批量渲染) -- **帧率**: 60 FPS 稳定 (支持10000+元素) - -**2. Konva.js (推荐指数: ⭐⭐⭐⭐)** - -```typescript -// Konva.js 高性能实现 -import Konva from 'konva'; - -class KonvaSceneEditor { - private stage: Konva.Stage; - private staticLayer: Konva.Layer; - private dynamicLayer: Konva.Layer; - private transformer: Konva.Transformer; - - constructor(container: HTMLElement) { - this.stage = new Konva.Stage({ - container: container, - width: container.clientWidth, - height: container.clientHeight, - }); - - this.setupLayers(); - this.enableOptimizations(); - } - - private setupLayers(): void { - // 静态层:点位、区域 (低频更新) - this.staticLayer = new Konva.Layer({ - listening: false, // 禁用事件监听提升性能 - }); - - // 动态层:机器人、选中状态 (高频更新) - this.dynamicLayer = new Konva.Layer(); - - this.stage.add(this.staticLayer); - this.stage.add(this.dynamicLayer); - } - - private enableOptimizations(): void { - // 1. 启用缓存 - this.staticLayer.cache(); - - // 2. 优化批量更新 - this.stage.batchDraw = true; - - // 3. 视口裁剪 - this.enableViewportCulling(); - - // 4. 事件委托 - this.setupEventDelegation(); - } - - // 批量添加元素 - addElementsBatch(elements: SceneElement[]): void { - const group = new Konva.Group(); - - elements.forEach((element) => { - const shape = this.createElement(element); - group.add(shape); - }); - - this.staticLayer.add(group); - this.staticLayer.batchDraw(); // 批量绘制 - } - - private enableViewportCulling(): void { - this.stage.on('dragmove wheel', () => { - this.updateVisibleElements(); - }); - } - - private updateVisibleElements(): void { - const viewport = this.getViewportBounds(); - - this.staticLayer.children.forEach((child) => { - const bounds = child.getClientRect(); - child.visible(this.boundsIntersect(viewport, bounds)); - }); - - this.staticLayer.batchDraw(); - } -} -``` - -#### 12.2.2 Three.js 3D扩展方案 - -```typescript -// 为未来3D场景编辑做准备 -import * as THREE from 'three'; - -class ThreeJSSceneEditor { - private scene: THREE.Scene; - private camera: THREE.OrthographicCamera; - private renderer: THREE.WebGLRenderer; - private instancedMeshes: Map = new Map(); - - constructor(container: HTMLElement) { - this.setupRenderer(container); - this.setupScene(); - this.enableInstancing(); // 实例化渲染优化 - } - - private setupRenderer(container: HTMLElement): void { - this.renderer = new THREE.WebGLRenderer({ - antialias: true, - powerPreference: 'high-performance', - }); - - this.renderer.setSize(container.clientWidth, container.clientHeight); - this.renderer.setPixelRatio(window.devicePixelRatio); - - // 启用高性能渲染选项 - this.renderer.shadowMap.enabled = true; - this.renderer.shadowMap.type = THREE.PCFSoftShadowMap; - container.appendChild(this.renderer.domElement); - } - - // 实例化渲染 - 支持数万个相同元素 - private enableInstancing(): void { - const pointGeometry = new THREE.CircleGeometry(1, 8); - const pointMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000 }); - - // 创建实例化网格,支持10000个点位 - const pointInstances = new THREE.InstancedMesh(pointGeometry, pointMaterial, 10000); - - this.instancedMeshes.set('points', pointInstances); - this.scene.add(pointInstances); - } - - // 批量更新实例位置 - updatePointsPositions(points: Point[]): void { - const instancedMesh = this.instancedMeshes.get('points'); - if (!instancedMesh) return; - - const matrix = new THREE.Matrix4(); - - points.forEach((point, index) => { - matrix.setPosition(point.x, point.y, 0); - instancedMesh.setMatrixAt(index, matrix); - }); - - instancedMesh.instanceMatrix.needsUpdate = true; - } -} -``` - -### 12.3 WebAssembly超高性能方案 - -#### 12.3.1 Rust + WebAssembly实现 - -```rust -// src/scene_engine.rs -use wasm_bindgen::prelude::*; -use web_sys::CanvasRenderingContext2d; - -#[wasm_bindgen] -pub struct SceneEngine { - elements: Vec, - spatial_grid: SpatialGrid, - viewport: Viewport, -} - -#[wasm_bindgen] -pub struct SceneElement { - id: u32, - x: f64, - y: f64, - element_type: ElementType, - visible: bool, -} - -#[wasm_bindgen] -impl SceneEngine { - #[wasm_bindgen(constructor)] - pub fn new() -> SceneEngine { - SceneEngine { - elements: Vec::with_capacity(100000), // 预分配大容量 - spatial_grid: SpatialGrid::new(100.0), // 100px网格 - viewport: Viewport::new(), - } - } - - // 批量添加元素 - 零拷贝操作 - #[wasm_bindgen] - pub fn add_elements_batch(&mut self, elements_ptr: *const u32, count: usize) { - unsafe { - let elements_slice = std::slice::from_raw_parts(elements_ptr, count * 4); - - for chunk in elements_slice.chunks(4) { - let element = SceneElement { - id: chunk[0], - x: f64::from_bits(chunk[1] as u64), - y: f64::from_bits(chunk[2] as u64), - element_type: ElementType::from_u32(chunk[3]), - visible: true, - }; - - self.spatial_grid.insert(&element); - self.elements.push(element); - } - } - } - - // 高性能视锥剔除 - #[wasm_bindgen] - pub fn update_visibility(&mut self, viewport_x: f64, viewport_y: f64, - viewport_width: f64, viewport_height: f64) -> Vec { - let mut visible_ids = Vec::new(); - - // 使用空间网格快速查询 - let candidates = self.spatial_grid.query( - viewport_x, viewport_y, viewport_width, viewport_height - ); - - for element_id in candidates { - if let Some(element) = self.elements.get_mut(*element_id as usize) { - element.visible = true; - visible_ids.push(element.id); - } - } - - visible_ids - } - - // 并行计算路径 - #[wasm_bindgen] - pub fn calculate_paths_parallel(&self, start_points: &[u32], - end_points: &[u32]) -> Vec { - use rayon::prelude::*; - - start_points.par_iter() - .zip(end_points.par_iter()) - .map(|(start, end)| { - self.calculate_shortest_path(*start, *end) - }) - .flatten() - .collect() - } -} - -// 高性能空间网格 -pub struct SpatialGrid { - cell_size: f64, - cells: std::collections::HashMap<(i32, i32), Vec>, -} - -impl SpatialGrid { - pub fn new(cell_size: f64) -> Self { - Self { - cell_size, - cells: std::collections::HashMap::new(), - } - } - - pub fn insert(&mut self, element: &SceneElement) { - let cell_x = (element.x / self.cell_size) as i32; - let cell_y = (element.y / self.cell_size) as i32; - - self.cells.entry((cell_x, cell_y)) - .or_insert_with(Vec::new) - .push(element.id); - } - - pub fn query(&self, x: f64, y: f64, width: f64, height: f64) -> Vec<&u32> { - let mut results = Vec::new(); - - let start_x = (x / self.cell_size) as i32; - let start_y = (y / self.cell_size) as i32; - let end_x = ((x + width) / self.cell_size) as i32; - let end_y = ((y + height) / self.cell_size) as i32; - - for cell_x in start_x..=end_x { - for cell_y in start_y..=end_y { - if let Some(elements) = self.cells.get(&(cell_x, cell_y)) { - results.extend(elements.iter()); - } - } - } - - results - } -} -``` - -#### 12.3.2 TypeScript集成层 - -```typescript -// TypeScript 集成 WebAssembly -import init, { SceneEngine } from './pkg/scene_engine'; - -class WasmSceneEditor { - private wasmEngine: SceneEngine; - private canvas: HTMLCanvasElement; - private ctx: CanvasRenderingContext2D; - private sharedBuffer: SharedArrayBuffer; - - async init(container: HTMLElement): Promise { - // 初始化 WebAssembly 模块 - await init(); - this.wasmEngine = new SceneEngine(); - - this.setupCanvas(container); - this.setupSharedMemory(); - } - - private setupSharedMemory(): void { - // 使用 SharedArrayBuffer 实现零拷贝数据传输 - this.sharedBuffer = new SharedArrayBuffer(1024 * 1024 * 4); // 4MB - } - - // 批量添加元素 - 超高性能 - addElementsBatch(elements: SceneElement[]): void { - // 将数据写入共享内存 - const view = new Uint32Array(this.sharedBuffer); - let offset = 0; - - elements.forEach((element) => { - view[offset++] = element.id; - view[offset++] = this.doubleToUint32(element.x); - view[offset++] = this.doubleToUint32(element.y); - view[offset++] = element.type; - }); - - // 调用 WASM 函数处理 - this.wasmEngine.add_elements_batch(view.byteOffset, elements.length); - } - - // 高性能渲染循环 - private renderLoop = (): void => { - // WASM 计算可见性 - const visibleIds = this.wasmEngine.update_visibility( - this.viewport.x, - this.viewport.y, - this.viewport.width, - this.viewport.height, - ); - - // 渲染可见元素 - this.renderVisibleElements(visibleIds); - - requestAnimationFrame(this.renderLoop); - }; - - private doubleToUint32(value: number): number { - const buffer = new ArrayBuffer(8); - new Float64Array(buffer)[0] = value; - return new Uint32Array(buffer)[0]; - } -} - -// 性能监控 -class PerformanceMonitor { - private frameCount = 0; - private lastTime = performance.now(); - - update(): void { - this.frameCount++; - const now = performance.now(); - - if (now - this.lastTime >= 1000) { - console.log(`FPS: ${this.frameCount}`); - console.log(`Memory: ${(performance as any).memory?.usedJSHeapSize / 1024 / 1024}MB`); - - this.frameCount = 0; - this.lastTime = now; - } - } -} -``` - -### 12.4 Web Workers多线程优化 - -#### 12.4.1 多线程架构设计 - -```typescript -// 主线程 -class MultiThreadSceneEditor { - private renderWorker: Worker; - private calculationWorker: Worker; - private dataWorker: Worker; - private offscreenCanvas: OffscreenCanvas; - - constructor(container: HTMLElement) { - this.setupWorkers(); - this.setupOffscreenCanvas(container); - } - - private setupWorkers(): void { - // 渲染工作线程 - this.renderWorker = new Worker('./workers/render.worker.js'); - - // 计算工作线程 - this.calculationWorker = new Worker('./workers/calculation.worker.js'); - - // 数据处理工作线程 - this.dataWorker = new Worker('./workers/data.worker.js'); - - this.setupWorkerCommunication(); - } - - private setupOffscreenCanvas(container: HTMLElement): void { - const canvas = document.createElement('canvas'); - canvas.width = container.clientWidth; - canvas.height = container.clientHeight; - container.appendChild(canvas); - - // 传输canvas控制权给worker - this.offscreenCanvas = canvas.transferControlToOffscreen(); - - this.renderWorker.postMessage( - { - type: 'init', - canvas: this.offscreenCanvas, - }, - [this.offscreenCanvas], - ); - } - - // 并行处理大量数据 - async processElementsBatch(elements: SceneElement[]): Promise { - const chunkSize = Math.ceil(elements.length / 3); - - // 并行处理 - const promises = [ - this.processChunk(elements.slice(0, chunkSize), 0), - this.processChunk(elements.slice(chunkSize, chunkSize * 2), 1), - this.processChunk(elements.slice(chunkSize * 2), 2), - ]; - - await Promise.all(promises); - } - - private processChunk(chunk: SceneElement[], workerId: number): Promise { - return new Promise((resolve) => { - const worker = [this.dataWorker, this.calculationWorker, this.renderWorker][workerId]; - - worker.postMessage({ - type: 'process', - data: chunk, - chunkId: workerId, - }); - - worker.addEventListener('message', (e) => { - if (e.data.type === 'processed' && e.data.chunkId === workerId) { - resolve(); - } - }); - }); - } -} - -// 渲染工作线程 (render.worker.ts) -class RenderWorker { - private ctx: OffscreenCanvasRenderingContext2D; - private elementsBuffer: Float32Array; - - constructor() { - self.addEventListener('message', this.handleMessage.bind(this)); - } - - private handleMessage(e: MessageEvent): void { - switch (e.data.type) { - case 'init': - this.ctx = e.data.canvas.getContext('2d'); - break; - - case 'render': - this.renderFrame(e.data.elements, e.data.viewport); - break; - - case 'updateElements': - this.elementsBuffer = new Float32Array(e.data.buffer); - break; - } - } - - private renderFrame(elements: Float32Array, viewport: Viewport): void { - this.ctx.clearRect(0, 0, viewport.width, viewport.height); - - // 批量渲染优化 - this.ctx.save(); - this.ctx.translate(-viewport.x, -viewport.y); - - // 使用 ImageData 直接操作像素 - const imageData = this.ctx.createImageData(viewport.width, viewport.height); - const data = imageData.data; - - // 高性能像素级渲染 - for (let i = 0; i < elements.length; i += 4) { - const x = elements[i]; - const y = elements[i + 1]; - const color = elements[i + 2]; - const type = elements[i + 3]; - - this.drawPixel(data, x, y, color, viewport); - } - - this.ctx.putImageData(imageData, viewport.x, viewport.y); - this.ctx.restore(); - - // 通知主线程渲染完成 - self.postMessage({ type: 'frameRendered' }); - } -} -``` - -### 12.5 性能对比表 - -| 方案 | 渲染性能 | 内存使用 | 开发复杂度 | 兼容性 | 推荐场景 | -| --------------- | ---------- | ---------- | ---------- | ---------- | --------------- | -| **当前Meta2d** | ⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | <1000元素 | -| **PixiJS** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | 1000-10000元素 | -| **Three.js** | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ | 3D场景/复杂效果 | -| **WebAssembly** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐ | >10000元素 | -| **Web Workers** | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐ | CPU密集计算 | - -### 12.6 迁移实施建议 - -#### 12.6.1 渐进式迁移策略 - -**阶段1: 立即优化 (1-2周)** - -```typescript -// 在现有Meta2d基础上添加PixiJS渲染层 -class HybridSceneEditor extends EditorService { - private pixiRenderer: PIXI.Application; - private usePixiForLargeScene = false; - - constructor(container: HTMLDivElement) { - super(container); - this.initPixiRenderer(container); - } - - private initPixiRenderer(container: HTMLDivElement): void { - this.pixiRenderer = new PIXI.Application({ - width: container.clientWidth, - height: container.clientHeight, - transparent: true, - powerPreference: 'high-performance', - }); - - // 叠加在Meta2d之上 - container.appendChild(this.pixiRenderer.view); - this.pixiRenderer.view.style.position = 'absolute'; - this.pixiRenderer.view.style.pointerEvents = 'none'; - } - - public override async load(map?: string, editable = false): Promise { - await super.load(map, editable); - - // 检查元素数量,决定使用哪个渲染器 - const totalElements = this.points.value.length + this.routes.value.length + this.areas.value.length; - - if (totalElements > 1000) { - console.log('🚀 切换到PixiJS高性能渲染'); - this.usePixiForLargeScene = true; - this.migrateToPixi(); - } - } - - private migrateToPixi(): void { - // 隐藏Meta2d渲染层 - this.canvas.canvas.style.opacity = '0.1'; - - // 使用PixiJS渲染大量元素 - this.renderWithPixi(); - } -} -``` - -**阶段2: 核心重构 (1-2月)** - -```typescript -// 完全基于PixiJS的新实现 -class NextGenSceneEditor { - private app: PIXI.Application; - private sceneContainer: PIXI.Container; - private quadTree: QuadTree; - private instanceManager: InstanceManager; - - constructor(container: HTMLElement) { - this.setupPixiApp(container); - this.setupOptimizations(); - } - - private setupOptimizations(): void { - // 1. 四叉树空间索引 - this.quadTree = new QuadTree(0, 0, 10000, 10000); - - // 2. 实例化管理器 - this.instanceManager = new InstanceManager(); - - // 3. 批量渲染系统 - this.setupBatchRenderer(); - - // 4. LOD系统 - this.setupLODSystem(); - } - - // 支持10万+元素的批量添加 - async addMassiveElements(elements: SceneElement[]): Promise { - console.time('MassiveElementsAdd'); - - // 使用实例化渲染 - const instancedElements = this.instanceManager.createInstances(elements); - - // 批量添加到四叉树 - elements.forEach((element) => { - this.quadTree.insert(element); - }); - - // 添加到场景 - instancedElements.forEach((instance) => { - this.sceneContainer.addChild(instance); - }); - - console.timeEnd('MassiveElementsAdd'); - console.log(`✅ 成功添加 ${elements.length} 个元素`); - } -} -``` - -**阶段3: WebAssembly增强 (2-3月)** - -```typescript -// 添加WebAssembly计算核心 -class UltimateSceneEditor extends NextGenSceneEditor { - private wasmCore: SceneEngineWasm; - private sharedBuffer: SharedArrayBuffer; - - async init(): Promise { - await super.init(); - - // 初始化WASM核心 - this.wasmCore = await SceneEngineWasm.init(); - - // 设置共享内存 - this.setupSharedMemory(); - } - - // 百万级元素支持 - async loadMegaScene(sceneData: MegaSceneData): Promise { - console.log(`🔥 加载超大场景: ${sceneData.elements.length} 个元素`); - - // WASM并行处理 - const processed = await this.wasmCore.processMegaScene(this.sharedBuffer, sceneData.elements.length); - - // PixiJS渲染 - await this.renderProcessedElements(processed); - - console.log('🎉 超大场景加载完成,性能提升100倍+'); - } -} -``` - -### 12.7 实施成本效益分析 - -#### 12.7.1 开发成本 - -```typescript -interface MigrationCost { - timeWeeks: number; - complexity: 'Low' | 'Medium' | 'High'; - riskLevel: 'Low' | 'Medium' | 'High'; - performanceGain: string; -} - -const migrationOptions: Record = { - pixiJSMigration: { - timeWeeks: 4, - complexity: 'Medium', - riskLevel: 'Low', - performanceGain: '10-50x渲染性能提升', - }, - - webAssemblyCore: { - timeWeeks: 8, - complexity: 'High', - riskLevel: 'Medium', - performanceGain: '100x+计算性能提升', - }, - - webWorkersParallel: { - timeWeeks: 3, - complexity: 'Medium', - riskLevel: 'Low', - performanceGain: '多核心并行处理', - }, - - hybridApproach: { - timeWeeks: 2, - complexity: 'Low', - riskLevel: 'Low', - performanceGain: '保持兼容性的性能提升', - }, -}; -``` - -#### 12.7.2 推荐实施路径 - -**🎯 推荐方案: PixiJS + Web Workers混合架构** - -```typescript -// 最佳性价比方案 -class RecommendedSceneEditor { - private pixiApp: PIXI.Application; - private calculationWorker: Worker; - private renderingOptimized = true; - - // 优势: - // ✅ 10-50倍性能提升 - // ✅ 4周开发周期 - // ✅ 低风险 - // ✅ 向后兼容 - // ✅ 支持1万+元素 - - constructor(container: HTMLElement) { - console.log('🚀 启用推荐高性能方案'); - this.initOptimizedRenderer(container); - } - - async benchmark(): Promise { - const elementCounts = [100, 1000, 5000, 10000, 50000]; - const results: PerformanceReport = {}; - - for (const count of elementCounts) { - const elements = this.generateTestElements(count); - - console.time(`Render ${count} elements`); - await this.addElementsBatch(elements); - console.timeEnd(`Render ${count} elements`); - - results[count] = { - fps: this.measureFPS(), - memory: this.measureMemory(), - renderTime: performance.now(), - }; - } - - return results; - } -} -``` - -### 12.8 总结建议 - -基于您的需求和现有技术栈,我建议采用以下**分阶段实施策略**: - -**🔥 立即实施 (高优先级)** - -1. **PixiJS渲染层**: 4周内实现,性能提升10-50倍 -2. **Web Workers计算**: 并行处理复杂计算 -3. **视口裁剪优化**: 只渲染可见元素 - -**⚡ 中期规划 (中优先级)** - -1. **WebAssembly核心**: 处理超大规模场景 -2. **四叉树空间索引**: 优化元素查找 -3. **LOD渲染系统**: 根据缩放级别调整详度 - -**🚀 长期愿景 (低优先级)** - -1. **Three.js 3D扩展**: 支持3D场景编辑 -2. **GPU计算着色器**: 最极致的性能优化 -3. **WebXR支持**: VR/AR场景编辑 - -这样的技术栈升级可以让您的场景编辑器: - -- 支持**10万+元素**的超大场景 -- 保持**60 FPS**稳定渲染 -- 减少**80%**的内存占用 -- 提供更好的用户体验 diff --git a/docs/库位功能逻辑分析.md b/docs/库位功能逻辑分析.md deleted file mode 100644 index e651485..0000000 --- a/docs/库位功能逻辑分析.md +++ /dev/null @@ -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,其中: - - `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()
(获取点位ID与站点名称映射) - StorageLocationService->>StorageLocationService: 更新内部 storageLocations Map - StorageLocationService->>EditorService: updatePointBorderColor(pointId, color)
(更新画布点的边框颜色) - - 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` 依赖此服务来获取画布中的点位信息并更新其视觉样式。 diff --git a/docs/库位渲染与状态集成说明.md b/docs/库位渲染与状态集成说明.md deleted file mode 100644 index 35311b1..0000000 --- a/docs/库位渲染与状态集成说明.md +++ /dev/null @@ -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` - - 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>` 的结构。 -- `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`。 diff --git a/docs/批量编辑功能使用说明.md b/docs/批量编辑功能使用说明.md deleted file mode 100644 index e7851d3..0000000 --- a/docs/批量编辑功能使用说明.md +++ /dev/null @@ -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. 所有更改都会记录在编辑历史中,支持撤销操作 - -## 🐛 故障排除 - -### 问题:批量编辑按钮不可用 - -- **原因**:没有选中任何元素 -- **解决**:先通过鼠标拖动选中需要编辑的点位或路线 - -### 问题:编辑面板中没有显示选项 - -- **原因**:选中的元素类型不支持该编辑选项 -- **解决**:确保选中的是点位或路线类型的元素 - -### 问题:更改没有生效 - -- **原因**:可能没有点击"确定"按钮 -- **解决**:在编辑面板中点击"确定"按钮应用更改 diff --git a/docs/机器人运动监控组件详细分析.md b/docs/机器人运动监控组件详细分析.md deleted file mode 100644 index ee9e8bf..0000000 --- a/docs/机器人运动监控组件详细分析.md +++ /dev/null @@ -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 { - await Promise.all( - this.robots.map(async ({ id, label, type }) => { - const pen: MapPen = { - ...this.#mapRobotImage(type, true), - id, - name: 'robot', - tags: ['robot'], - x: 0, // 关键: 初始位置固定为(0,0) - y: 0, // 关键: 初始位置固定为(0,0) - width: 74, - height: 74, - lineWidth: 1, - robot: { type }, - visible: false, // 关键: 初始状态为不可见 - text: label, - textTop: -24, - whiteSpace: 'nowrap', - ellipsis: false, - locked: LockState.Disable, - }; - await this.addPen(pen, false, true, true); - }), - ); -} -``` - -**问题分析**: - -- 所有机器人初始位置都设为`(0,0)` -- 初始状态为`visible: false`,需要WebSocket数据才能显示 -- 如果WebSocket连接延迟,不同页面的机器人可能长时间处于不可见状态 - -#### 步骤3: monitorScene() - WebSocket监控建立 - -```typescript -const monitorScene = async () => { - client.value?.close(); // 关闭之前的连接 - const ws = await monitorSceneById(props.sid); // 创建新连接 - if (isNil(ws)) return; - - ws.onmessage = (e) => { - const { id, x, y, active, angle, path, ...rest } = JSON.parse(e.data || '{}'); - - if (!editor.value?.checkRobotById(id)) return; // 验证机器人存在 - - editor.value?.updateRobot(id, rest); // 更新机器人基本信息 - - if (isNil(x) || isNil(y)) { - // 关键逻辑: 无位置信息时隐藏机器人 - editor.value.updatePen(id, { visible: false }); - } else { - // 关键逻辑: 有位置信息时更新位置并显示 - editor.value.refreshRobot(id, { x, y, active, angle, path }); - } - }; - client.value = ws; -}; -``` - -## 3. 机器人实时移动机制深度分析 - -### 3.1 WebSocket消息处理流程 - -每当接收到WebSocket消息时,会执行以下处理逻辑: - -1. **消息解析**: 将JSON字符串解析为`RobotRealtimeInfo`对象 -2. **机器人验证**: 调用`checkRobotById(id)`验证机器人是否存在 -3. **基本信息更新**: 调用`updateRobot(id, rest)`更新电量、状态等信息 -4. **位置处理**: 根据坐标是否存在进行不同处理 - -### 3.2 位置更新核心逻辑: refreshRobot() - -```typescript -public refreshRobot(id: RobotInfo['id'], info: Partial): void { - const pen = this.getPenById(id); - const { rotate: or, robot } = pen ?? {}; - if (!robot?.type) return; - - // 获取当前机器人位置 - const { x: ox, y: oy } = this.getPenRect(pen!); - - // 解析新的位置信息(默认值为37,37是机器人中心点) - const { x: cx = 37, y: cy = 37, active, angle, path: points } = info; - - // 关键坐标转换: 从中心点坐标转换为左上角坐标 - const x = cx - 37; // 机器人宽度74,中心偏移37 - const y = cy - 37; // 机器人高度74,中心偏移37 - - const rotate = angle ?? or; // 角度更新 - - // 路径坐标转换 - const path = - points?.map((p) => ({ x: p.x - cx, y: p.y - cy })) ?? // 新路径相对于机器人中心 - robot.path?.map((p) => ({ x: p.x + ox! - x, y: p.y + oy! - y })); // 旧路径坐标调整 - - const o = { ...robot, ...omitBy({ active, path }, isNil) }; - - if (isNil(active)) { - // active为null时,只更新位置不改变图标 - this.setValue( - { id, x, y, rotate, robot: o, visible: true }, - { render: true, history: false, doEvent: false } - ); - } else { - // active有值时,同时更新图标状态(运行/停止状态图标不同) - this.setValue( - { id, ...this.#mapRobotImage(robot.type, active), x, y, rotate, robot: o, visible: true }, - { render: true, history: false, doEvent: false } - ); - } -} -``` - -### 3.3 机器人图标映射逻辑 - -```typescript -#mapRobotImage( - type: RobotType, - active?: boolean, -): Required> { - const theme = this.data().theme; - const image = import.meta.env.BASE_URL + - (active ? `/robot/${type}-active-${theme}.png` : `/robot/${type}-${theme}.png`); - return { - image, - iconWidth: 34, - iconHeight: 54, - iconTop: -5 - }; -} -``` - -### 3.4 机器人绘制函数 - -```typescript -function drawRobot(ctx: CanvasRenderingContext2D, pen: MapPen): void { - const theme = sTheme.editor; - const { lineWidth: s = 1 } = pen.calculative ?? {}; - const { x = 0, y = 0, width: w = 0, height: h = 0, rotate: deg = 0 } = pen.calculative?.worldRect ?? {}; - const { active, path } = pen.robot ?? {}; - - if (!active) return; // 关键: 非活跃状态不绘制路径 - - const ox = x + w / 2; // 机器人中心X坐标 - const oy = y + h / 2; // 机器人中心Y坐标 - - ctx.save(); - // 绘制机器人本体(椭圆) - ctx.ellipse(ox, oy, w / 2, h / 2, 0, 0, Math.PI * 2); - ctx.fillStyle = get(theme, 'robot.fill') ?? ''; - ctx.fill(); - ctx.strokeStyle = get(theme, 'robot.stroke') ?? ''; - ctx.stroke(); - - // 绘制运动路径 - if (path?.length) { - ctx.strokeStyle = get(theme, 'robot.line') ?? ''; - ctx.lineCap = 'round'; - ctx.lineWidth = s * 4; - ctx.setLineDash([s * 5, s * 10]); // 虚线样式 - ctx.translate(ox, oy); - ctx.rotate((-deg * Math.PI) / 180); // 根据机器人角度旋转 - - // 绘制路径线条 - ctx.beginPath(); - ctx.moveTo(0, 0); - path.forEach((d) => ctx.lineTo(d.x * s, d.y * s)); - ctx.stroke(); - - // 绘制路径终点箭头 - const { x: ex1 = 0, y: ey1 = 0 } = nth(path, -1) ?? {}; - const { x: ex2 = 0, y: ey2 = 0 } = nth(path, -2) ?? {}; - const r = Math.atan2(ey1 - ey2, ex1 - ex2) + Math.PI; - ctx.setLineDash([0]); - ctx.translate(ex1 * s, ey1 * s); - ctx.beginPath(); - ctx.moveTo(Math.cos(r + Math.PI / 4) * s * 10, Math.sin(r + Math.PI / 4) * s * 10); - ctx.lineTo(0, 0); - ctx.lineTo(Math.cos(r - Math.PI / 4) * s * 10, Math.sin(r - Math.PI / 4) * s * 10); - ctx.stroke(); - ctx.setTransform(1, 0, 0, 1, 0, 0); - } - ctx.restore(); -} -``` - -## 4. 多页面位置不一致问题深度分析 - -### 4.1 根本原因:缺乏全局状态同步机制 - -每个页面实例都是完全独立的,具体表现为: - -1. **独立的EditorService实例** - - - 每个页面创建独立的`new EditorService(container.value!)` - - 各自维护独立的机器人状态映射`#robotMap` - - 无法共享机器人位置信息 - -2. **独立的WebSocket连接** - - 每个页面调用`monitorSceneById(props.sid)`创建独立连接 - - 服务器可能向不同连接推送不同时间点的数据 - - 网络延迟导致消息到达时间不同 - -### 4.2 具体问题场景分析 - -#### 场景1: 初始化时间差异 - -```typescript -// 页面A在时间T1执行 -onMounted(async () => { - await readScene(); // T1时刻的场景数据 - await initRobots(); // 创建机器人,位置(0,0),visible:false - await monitorScene(); // T1+100ms建立WebSocket -}); - -// 页面B在时间T2执行(T2 > T1) -onMounted(async () => { - await readScene(); // T2时刻的场景数据(可能已更新) - await initRobots(); // 创建机器人,位置(0,0),visible:false - await monitorScene(); // T2+80ms建立WebSocket -}); -``` - -**结果**: 两个页面获取的初始场景数据可能不同,机器人列表或配置存在差异。 - -#### 场景2: WebSocket消息时序差异 - -```typescript -// WebSocket消息处理逻辑 -ws.onmessage = (e) => { - const { id, x, y, active, angle, path, ...rest } = JSON.parse(e.data || '{}'); - - if (isNil(x) || isNil(y)) { - // 关键问题: 无坐标消息会隐藏机器人 - editor.value.updatePen(id, { visible: false }); - } else { - editor.value.refreshRobot(id, { x, y, active, angle, path }); - } -}; -``` - -**问题分析**: - -- 页面A先收到有坐标的消息,机器人显示在位置(100, 200) -- 页面B后收到无坐标的消息,机器人被隐藏 -- 页面C收到旧的坐标消息,机器人显示在位置(80, 150) - -#### 场景3: 坐标转换精度问题 - -```typescript -// refreshRobot中的坐标转换 -const { x: cx = 37, y: cy = 37, active, angle, path: points } = info; -const x = cx - 37; // 默认值37导致的问题 -const y = cy - 37; - -// 当服务器发送的坐标为null/undefined时 -// cx和cy都会使用默认值37,导致机器人位置为(0,0) -``` - -**问题**: 不同页面接收到的消息中坐标字段可能为`null`、`undefined`或有效数值,默认值处理导致位置计算不一致。 - -#### 场景4: 机器人状态检查差异 - -```typescript -if (!editor.value?.checkRobotById(id)) return; - -// checkRobotById实现 -public checkRobotById(id: RobotInfo['id']): boolean { - return this.#robotMap.has(id); -} -``` - -**问题**: 不同页面的`#robotMap`内容可能不同,导致某些页面忽略特定机器人的更新消息。 - -### 4.3 路径绘制不一致问题 - -```typescript -// 路径坐标转换逻辑 -const path = - points?.map((p) => ({ x: p.x - cx, y: p.y - cy })) ?? // 新路径处理 - robot.path?.map((p) => ({ x: p.x + ox! - x, y: p.y + oy! - y })); // 旧路径处理 -``` - -**问题分析**: - -1. 新路径使用`p.x - cx, p.y - cy`进行坐标转换 -2. 旧路径使用`p.x + ox! - x, p.y + oy! - y`进行坐标转换 -3. 两种转换方式在特定情况下可能产生不同结果 -4. 不同页面可能处于新旧路径的不同阶段 - -### 4.4 渲染状态不同步 - -```typescript -// setValue方法的渲染参数 -this.setValue( - { id, x, y, rotate, robot: o, visible: true }, - { render: true, history: false, doEvent: false }, // 立即渲染,不记录历史 -); -``` - -**问题**: - -- `render: true`表示立即重新渲染 -- 不同页面的渲染时机不同步 -- 可能出现某个页面正在渲染时收到新消息的情况 - -## 5. 解决方案详细设计 - -### 5.1 方案一: 全局状态管理器 - -```typescript -/** - * 全局机器人状态管理器 - * 单例模式,确保所有页面共享同一份状态 - */ -class GlobalRobotStateManager { - private static instance: GlobalRobotStateManager; - - // 存储所有机器人的最新状态 - private robotStates = new Map(); - - // 订阅者列表,用于通知状态变化 - private subscribers = new Set<(robotId: string, info: RobotRealtimeInfo) => void>(); - - // 连接管理,避免重复连接 - private connections = new Map(); - - static getInstance(): GlobalRobotStateManager { - if (!this.instance) { - this.instance = new GlobalRobotStateManager(); - } - return this.instance; - } - - /** - * 订阅机器人状态变化 - */ - subscribe(callback: (robotId: string, info: RobotRealtimeInfo) => void): () => void { - this.subscribers.add(callback); - - // 立即推送当前所有机器人状态 - this.robotStates.forEach((info, robotId) => { - callback(robotId, info); - }); - - // 返回取消订阅函数 - return () => this.subscribers.delete(callback); - } - - /** - * 更新机器人状态并通知所有订阅者 - */ - updateRobotState(robotId: string, info: RobotRealtimeInfo): void { - // 合并状态更新 - const currentState = this.robotStates.get(robotId) || ({} as RobotRealtimeInfo); - const newState = { ...currentState, ...info }; - - this.robotStates.set(robotId, newState); - - // 通知所有订阅者 - this.subscribers.forEach((callback) => { - try { - callback(robotId, newState); - } catch (error) { - console.error('机器人状态更新回调执行失败:', error); - } - }); - } - - /** - * 获取或创建WebSocket连接(复用连接) - */ - async getOrCreateConnection(sceneId: string): Promise { - // 检查现有连接 - const existingConnection = this.connections.get(sceneId); - if (existingConnection && existingConnection.readyState === WebSocket.OPEN) { - return existingConnection; - } - - try { - const ws = await monitorSceneById(sceneId); - if (!ws) return null; - - // 设置消息处理 - ws.onmessage = (e) => { - try { - const robotInfo = JSON.parse(e.data || '{}') as RobotRealtimeInfo; - this.updateRobotState(robotInfo.id, robotInfo); - } catch (error) { - console.error('WebSocket消息解析失败:', error); - } - }; - - // 设置连接关闭处理 - ws.onclose = () => { - this.connections.delete(sceneId); - }; - - // 存储连接 - this.connections.set(sceneId, ws); - return ws; - } catch (error) { - console.error('创建WebSocket连接失败:', error); - return null; - } - } -} -``` - -### 5.2 方案二: 改进的组件实现 - -```typescript -// 改进后的movement-supervision.vue核心逻辑 - -``` - -### 5.3 方案三: EditorService增强 - -```typescript -// 为EditorService添加状态缓存和同步机制 -export class EditorService extends Meta2d { - // 添加状态缓存 - private robotStateCache = new Map(); - - /** - * 改进的坐标转换方法 - */ - private normalizeCoordinates(info: Partial): { x: number; y: number } | null { - const { x: cx, y: cy } = info; - - // 严格的坐标验证 - if (typeof cx !== 'number' || typeof cy !== 'number' || isNaN(cx) || isNaN(cy) || cx < 0 || cy < 0) { - return null; // 返回null表示无效坐标 - } - - // 坐标转换:从中心点转换为左上角 - return { - x: cx - 37, // 机器人宽度74,中心偏移37 - y: cy - 37, // 机器人高度74,中心偏移37 - }; - } - - /** - * 改进的refreshRobot方法 - */ - public refreshRobot(id: RobotInfo['id'], info: Partial): void { - const pen = this.getPenById(id); - const { rotate: or, robot } = pen ?? {}; - if (!robot?.type) return; - - // 使用改进的坐标转换 - const coords = this.normalizeCoordinates(info); - - // 无效坐标处理 - if (!coords) { - this.setValue({ id, visible: false }, { render: true, history: false, doEvent: false }); - return; - } - - const { x, y } = coords; - const { active, angle, path: points } = info; - const rotate = angle ?? or; - - // 路径处理优化 - let path: Point[] | undefined; - if (points && Array.isArray(points)) { - // 新路径:相对于机器人中心的坐标 - path = points.map((p) => ({ - x: (p.x || 0) - (info.x || 37), - y: (p.y || 0) - (info.y || 37), - })); - } else if (robot.path) { - // 保持原有路径,但需要调整坐标 - const { x: ox, y: oy } = this.getPenRect(pen!); - path = robot.path.map((p) => ({ - x: p.x + ox - x, - y: p.y + oy - y, - })); - } - - const robotState = { ...robot, ...omitBy({ active, path }, isNil) }; - - // 根据active状态决定渲染方式 - if (typeof active === 'boolean') { - // 有明确的活跃状态,更新图标 - this.setValue( - { - id, - ...this.#mapRobotImage(robot.type, active), - x, - y, - rotate, - robot: robotState, - visible: true, - }, - { render: true, history: false, doEvent: false }, - ); - } else { - // 无活跃状态信息,只更新位置 - this.setValue( - { id, x, y, rotate, robot: robotState, visible: true }, - { render: true, history: false, doEvent: false }, - ); - } - } -} -``` - -## 6. 性能优化建议 - -### 6.1 渲染优化 - -- 使用`requestAnimationFrame`批量处理渲染更新 -- 实现视口裁剪,只渲染可见区域的机器人 -- 添加机器人状态变化的diff检测,避免无效渲染 - -### 6.2 内存管理 - -- 定期清理过期的机器人状态缓存 -- 使用WeakMap存储临时状态,避免内存泄漏 -- 在组件卸载时正确清理WebSocket连接和事件监听器 - -### 6.3 网络优化 - -- 实现WebSocket连接池,复用连接 -- 添加消息压缩,减少网络传输量 -- 使用心跳机制检测连接状态 - -## 7. 总结 - -机器人运动监控组件的多页面位置不一致问题主要源于: - -1. **架构设计缺陷**: 缺乏全局状态管理,每个页面独立维护状态 -2. **WebSocket连接独立性**: 多个连接可能接收到不同时间点的数据 -3. **初始化时序问题**: 不同页面的初始化时间不同,导致状态基线不一致 -4. **坐标转换逻辑**: 默认值处理和坐标转换在边界情况下存在问题 -5. **状态验证不足**: 缺乏对接收数据的有效性验证 - -通过实施全局状态管理、WebSocket连接复用、状态缓存机制和坐标转换优化等解决方案,可以有效解决这些问题,确保多页面间机器人位置的一致性。 - -## 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. **实时性**:与机器人监控相同的实时更新能力 - -这种设计展现了系统架构的可扩展性,为未来支持更多设备类型(如电梯、传感器等)奠定了良好的基础。 diff --git a/docs/编辑器服务核心架构分析.md b/docs/编辑器服务核心架构分析.md deleted file mode 100644 index 82386c0..0000000 --- a/docs/编辑器服务核心架构分析.md +++ /dev/null @@ -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): Promise { - // 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 { - 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( - 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(); - -// 监听鼠标拖拽事件 -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(); - const routes = new Array(); - - if (!id) { - id = s8(); // 生成唯一ID - const selected = 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(); - -// 区域列表响应式数据 -public readonly areas = useObservable( - this.#change$$.pipe( - filter((v) => v), // 只响应数据变化事件 - debounceTime(100), // 防抖处理 - map(() => this.find('area')), // 查找所有区域 - ), - { initialValue: new Array() }, -); - -// 当前选中元素 -public readonly current = useObservable( - this.#change$$.pipe( - debounceTime(100), - map(() => 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>(new Map()); - -// 机器人组流 -readonly #robotGroups$$ = new BehaviorSubject([]); -public readonly robotGroups = useObservable( - this.#robotGroups$$.pipe(debounceTime(300)) -); -``` - -### 8.2 实时机器人更新 - -```typescript -public refreshRobot(id: RobotInfo['id'], info: Partial): 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 { - 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(); -``` - -### 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 提供完整的类型检查 - -这个编辑器服务是一个设计精良的复杂系统,通过合理的架构设计实现了强大的场景编辑功能。 diff --git a/docs/项目说明文档.md b/docs/项目说明文档.md deleted file mode 100644 index ac4d474..0000000 --- a/docs/项目说明文档.md +++ /dev/null @@ -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 - - -``` - -### 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; - save(): string; - - // 机器人管理 - addRobots(gid: string, robots: RobotInfo[]): void; - removeRobots(ids: string[]): void; - updateRobot(id: string, value: Partial): void; - - // 点位管理 - async addPoint(p: Point, type?: MapPointType, id?: string): Promise; - updatePoint(id: string, info: Partial): void; - changePointType(id: string, type: MapPointType): void; - - // 路径管理 - addRoute(p: [string, string], type?: MapRouteType, id?: string): void; - updateRoute(id: string, info: Partial): void; - - // 区域管理 - async addArea(p1: Point, p2: Point, type?: MapAreaType, id?: string): Promise; - updateArea(id: string, info: Partial): 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; - -// 注册机器人 -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 -- 维护状态: 积极维护中 - ---- - -**注意**: 本文档基于项目当前状态生成,随着项目发展会持续更新。如有疑问或建议,请联系开发团队。