From e4047e62a0daa315ffeed7d4e28d23001020c025 Mon Sep 17 00:00:00 2001 From: xudan Date: Tue, 17 Jun 2025 15:34:56 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E5=9C=B0=E5=9B=BE?= =?UTF-8?q?=E7=82=B9=E4=BD=8D=E3=80=81=E8=B7=AF=E7=BA=BF=E5=92=8C=E5=8C=BA?= =?UTF-8?q?=E5=9F=9F=E7=B1=BB=E5=9E=8B=E6=9E=9A=E4=B8=BE=EF=BC=8C=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E8=AF=A6=E7=BB=86=E6=B3=A8=E9=87=8A=E5=92=8C=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/map/constant.ts | 85 +- src/services/editor.service.ts | 113 ++- 场景编辑器组件详细分析.md | 1579 +++++++++++++++++++++++++++++++- 3 files changed, 1736 insertions(+), 41 deletions(-) diff --git a/src/apis/map/constant.ts b/src/apis/map/constant.ts index 51940bf..0b143f3 100644 --- a/src/apis/map/constant.ts +++ b/src/apis/map/constant.ts @@ -1,77 +1,160 @@ import { KeydownType, type Options } from '@meta2d/core'; import { invert } from 'lodash-es'; +/** + * 地图点位类型枚举 + * 定义了场景编辑器中所有支持的点位类型 + * 数值1-9为小点位,10以上为大点位(有特殊图标) + */ //#region 点位 export enum MapPointType { + /** 普通点 - 基础导航点,机器人可通过 */ 普通点 = 1, + /** 等待点 - 机器人等待或暂停的位置 */ 等待点, + /** 避让点 - 机器人主动避让其他机器人的点位 */ 避让点, + /** 临时避让点 - 动态生成的临时避让位置,有特殊标记 */ 临时避让点, + /** 电梯点 - 机器人乘坐电梯的专用点位 */ 电梯点 = 11, + /** 自动门点 - 需要自动门控制的通行点位 */ 自动门点, + /** 充电点 - 机器人充电的专用位置,可绑定特定机器人 */ 充电点, + /** 停靠点 - 机器人停靠等待的位置,可绑定特定机器人 */ 停靠点, + /** 动作点 - 机器人执行特定动作的位置(如取货、放货) */ 动作点, + /** 禁行点 - 禁止机器人通过的点位 */ 禁行点, } + +/** + * 点位类型映射数组,用于UI显示和选择 + * 过滤掉非数字类型的枚举项 + */ export const MAP_POINT_TYPES = Object.freeze( <[string, MapPointType][]>Object.entries(MapPointType).filter(([, v]) => typeof v === 'number'), ); //#endregion +/** + * 地图路线类型枚举 + * 定义了连接点位之间的路径类型 + */ //#region 线路 export enum MapRouteType { + /** 直线 - 两点间直线连接 */ 直线 = 'line', + /** 二阶贝塞尔曲线 - 带一个控制点的曲线 */ 二阶贝塞尔曲线 = 'bezier2', + /** 三阶贝塞尔曲线 - 带两个控制点的曲线,更灵活 */ 三阶贝塞尔曲线 = 'bezier3', } + +/** + * 路线类型反向映射,用于从字符串值获取枚举键 + */ export const MAP_ROUTE_TYPE = invert(MapRouteType); + +/** + * 路线类型映射数组,用于UI显示和选择 + */ export const MAP_ROUTE_TYPES = Object.freeze(<[string, MapRouteType][]>Object.entries(MapRouteType)); +/** + * 路线通行类型枚举 + * 定义了路线的通行权限和限制 + */ export enum MapRoutePassType { + /** 无限制 - 所有机器人都可以通行 */ 无, + /** 仅空载可通行 - 只有空载的机器人可以通过 */ 仅空载可通行, + /** 仅载货可通行 - 只有载货的机器人可以通过 */ 仅载货可通行, + /** 禁行 - 禁止所有机器人通行,显示为虚线 */ 禁行 = 10, } + +/** + * 路线通行类型映射数组,用于UI显示和选择 + */ export const MAP_ROUTE_PASS_TYPES = Object.freeze( <[string, MapRoutePassType][]>Object.entries(MapRoutePassType).filter(([, v]) => typeof v === 'number'), ); //#endregion +/** + * 地图区域类型枚举 + * 定义了场景中不同功能的区域类型 + */ //#region 区域 export enum MapAreaType { + /** 库区 - 仓储作业区域,包含动作点 */ 库区 = 1, + /** 互斥区 - 同时只能有一个机器人进入的区域 */ 互斥区 = 11, + /** 非互斥区 - 可以同时有多个机器人进入的区域 */ 非互斥区, } + +/** + * 区域类型映射数组,用于UI显示和选择 + */ export const MAP_AREA_TYPES = Object.freeze( <[string, MapAreaType][]>Object.entries(MapAreaType).filter(([, v]) => typeof v === 'number'), ); //#endregion +/** + * 场景编辑器核心配置 + * 基于Meta2D引擎的编辑器配置参数 + */ export const EDITOR_CONFIG: Options = { + /** 键盘事件类型 - 禁用所有键盘快捷键 */ keydown: KeydownType.None, + /** 严格作用域 - 限制编辑操作范围 */ strictScope: true, + /** 移动连接线 - 禁用拖动时自动移动连接的线条 */ moveConnectedLine: false, + /** 禁用输入 - 禁用文本输入功能 */ disableInput: true, + /** 禁用旋转 - 禁用图形旋转功能 */ disableRotate: true, + /** 禁用尺寸调整 - 禁用图形大小调整 */ disableSize: true, + /** 禁用锚点 - 禁用连接锚点显示 */ disableAnchor: true, + /** 禁用空线条 - 不允许创建没有连接点的线条 */ disableEmptyLine: true, + /** 禁用重复线条 - 不允许在同一对点之间创建多条线 */ disableRepeatLine: true, + /** 最小缩放比例 - 画布最小缩放到24% */ minScale: 0.24, + /** 最大缩放比例 - 画布最大缩放到401% */ maxScale: 4.01, - scaleOff: 0.01, + /** 缩放步长 - 每次滚轮滚动的缩放幅度(5%) */ + scaleOff: 0.05, + /** 默认锚点 - 不设置默认锚点 */ defaultAnchors: [], + /** 全局透明度 - 普通状态下图形透明度(0为不透明) */ globalAlpha: 0, + /** 激活状态全局透明度 - 选中状态下图形透明度 */ activeGlobalAlpha: 0, + /** 默认字体大小 - 14像素 */ fontSize: 14, + /** 行高倍数 - 1.5倍行高 */ lineHeight: 1.5, + /** 字体族 - 使用系统默认字体 */ fontFamily: 'system-ui', + /** 文本旋转 - 禁用文本跟随图形旋转 */ textRotate: false, + /** 文本水平对齐 - 居中对齐 */ textAlign: 'center', + /** 文本垂直基线 - 顶部对齐 */ textBaseline: 'top', }; diff --git a/src/services/editor.service.ts b/src/services/editor.service.ts index 03e20fa..7d7ca3f 100644 --- a/src/services/editor.service.ts +++ b/src/services/editor.service.ts @@ -27,8 +27,25 @@ import { clone, get, isEmpty, isNil, isString, nth, omitBy, pick, remove, some } import { BehaviorSubject, debounceTime, filter, map, Subject, switchMap } from 'rxjs'; import { reactive, watch } from 'vue'; +/** + * 场景编辑器服务类 + * 继承自Meta2D,提供完整的场景编辑功能 + * + * 主要功能: + * - 场景文件的加载、保存和管理 + * - 点位、路线、区域的创建和编辑 + * - 机器人组的管理和实时状态更新 + * - 鼠标事件的处理和响应式数据流 + * - 自定义绘制和渲染逻辑 + */ export class EditorService extends Meta2d { - //#region 场景文件 + //#region 场景文件管理 + /** + * 加载场景文件到编辑器 + * @param map 场景文件的JSON字符串,为空则创建新场景 + * @param editable 是否可编辑状态,控制编辑器锁定状态 + * @param detail 群组场景详情,包含机器人组和机器人信息 + */ public async load(map?: string, editable = false, detail?: Partial): Promise { const scene: StandardScene = map ? JSON.parse(map) : {}; if (!isEmpty(detail?.group)) { @@ -45,6 +62,10 @@ export class EditorService extends Meta2d { this.store.historyIndex = undefined; this.store.histories = []; } + /** + * 保存当前场景为JSON字符串 + * @returns 包含完整场景数据的JSON字符串 + */ public save(): string { const scene: StandardScene = { robotGroups: this.robotGroups.value, @@ -57,11 +78,20 @@ export class EditorService extends Meta2d { return JSON.stringify(scene); } + /** + * 加载机器人数据到编辑器 + * @param groups 机器人组列表 + * @param robots 机器人信息列表 + */ #loadRobots(groups?: RobotGroup[], robots?: RobotInfo[]): void { this.#robotMap.clear(); robots?.forEach((v) => this.#robotMap.set(v.id, v)); this.#robotGroups$$.next(groups ?? []); } + /** + * 从场景数据加载点位到画布 + * @param points 标准场景点位数据数组 + */ async #loadScenePoints(points?: StandardScenePoint[]): Promise { if (!points?.length) return; await Promise.all( @@ -75,6 +105,10 @@ export class EditorService extends Meta2d { }), ); } + /** + * 从场景数据加载路线到画布 + * @param routes 标准场景路线数据数组 + */ #loadSceneRoutes(routes?: StandardSceneRoute[]): void { if (!routes?.length) return; routes.map((v) => { @@ -206,6 +240,10 @@ export class EditorService extends Meta2d { } //#endregion + /** + * 设置编辑器状态 + * @param editable 是否可编辑,true为可编辑状态,false为只读状态 + */ public setState(editable?: boolean): void { this.lock(editable ? LockState.None : LockState.DisableEdit); } @@ -214,7 +252,10 @@ export class EditorService extends Meta2d { return super.data(); } + /** 鼠标事件流主体,用于内部事件分发 */ readonly #mouse$$ = new Subject<{ type: 'click' | 'mousedown' | 'mouseup'; value: Point }>(); + + /** 鼠标点击事件的响应式流,防抖处理后的点击坐标 */ public readonly mouseClick = useObservable( this.#mouse$$.pipe( filter(({ type }) => type === 'click'), @@ -222,6 +263,8 @@ export class EditorService extends Meta2d { map(({ value }) => value), ), ); + + /** 鼠标拖拽事件的响应式流,返回起始点和结束点坐标,用于创建区域 */ public readonly mouseBrush = useObservable<[Point, Point]>( this.#mouse$$.pipe( filter(({ type }) => type === 'mousedown'), @@ -234,8 +277,11 @@ export class EditorService extends Meta2d { ), ); - //#region 机器人组 + //#region 机器人组管理 + /** 机器人信息映射表,响应式存储所有机器人数据 */ readonly #robotMap = reactive>(new Map()); + + /** 获取所有机器人信息数组 */ public get robots(): RobotInfo[] { return Array.from(this.#robotMap.values()); } @@ -319,13 +365,18 @@ export class EditorService extends Meta2d { } //#endregion + /** 画布变化事件流,用于触发响应式数据更新 */ readonly #change$$ = new Subject(); + + /** 当前选中的图形对象,响应式更新 */ public readonly current = useObservable( this.#change$$.pipe( debounceTime(100), map(() => clone(this.store.active?.[0])), ), ); + + /** 当前选中的图形ID列表,响应式更新 */ public readonly selected = useObservable( this.#change$$.pipe( filter((v) => !v), @@ -334,6 +385,8 @@ export class EditorService extends Meta2d { ), { initialValue: new Array() }, ); + + /** 画布上所有图形对象列表,响应式更新 */ public readonly pens = useObservable( this.#change$$.pipe( filter((v) => v), @@ -432,6 +485,7 @@ export class EditorService extends Meta2d { //#endregion //#region 点位 + /** 画布上所有点位对象列表,响应式更新 */ public readonly points = useObservable( this.#change$$.pipe( filter((v) => v), @@ -447,6 +501,12 @@ export class EditorService extends Meta2d { return { x: x + width / 2, y: y + height / 2, width, height }; } + /** + * 在指定位置添加点位 + * @param p 点位坐标 + * @param type 点位类型,默认为普通点 + * @param id 点位ID,未指定则自动生成 + */ public async addPoint(p: Point, type = MapPointType.普通点, id?: string): Promise { id ||= s8(); const pen: MapPen = { @@ -503,6 +563,7 @@ export class EditorService extends Meta2d { //#endregion //#region 线路 + /** 画布上所有路线对象列表,响应式更新,包含动态生成的标签 */ public readonly routes = useObservable( this.#change$$.pipe( filter((v) => v), @@ -525,6 +586,12 @@ export class EditorService extends Meta2d { return `${p1.label}${(d ?? direction) > 0 ? '→' : '←'}${p2.label}`; } + /** + * 在两个点位之间添加路线 + * @param p 两个点位的数组 + * @param type 路线类型,默认为直线 + * @param id 路线ID,未指定则自动生成 + */ public addRoute(p: [MapPen, MapPen], type = MapRouteType.直线, id?: string): void { const [p1, p2] = p; if (!p1?.anchors?.length || !p2?.anchors?.length) return; @@ -553,6 +620,7 @@ export class EditorService extends Meta2d { //#endregion //#region 区域 + /** 画布上所有区域对象列表,响应式更新 */ public readonly areas = useObservable( this.#change$$.pipe( filter((v) => v), @@ -571,6 +639,13 @@ export class EditorService extends Meta2d { }); } + /** + * 在指定区域添加功能区域 + * @param p1 区域起始坐标 + * @param p2 区域结束坐标 + * @param type 区域类型,默认为库区 + * @param id 区域ID,未指定则自动生成 + */ public async addArea(p1: Point, p2: Point, type = MapAreaType.库区, id?: string) { const scale = this.data().scale ?? 1; const w = Math.abs(p1.x - p2.x); @@ -621,13 +696,21 @@ export class EditorService extends Meta2d { } //#endregion + /** + * 构造函数 - 初始化场景编辑器 + * @param container 编辑器容器DOM元素 + */ constructor(container: HTMLDivElement) { super(container, EDITOR_CONFIG); + // 禁用第6个子元素的拖放功能 (container.children.item(5)).ondrop = null; + // 监听所有画布事件 this.on('*', (e, v) => this.#listen(e, v)); + // 注册自定义绘制函数和锚点 this.#register(); + // 监听主题变化并重新加载样式 watch( () => sTheme.theme, (v) => this.#load(v), @@ -711,7 +794,12 @@ export class EditorService extends Meta2d { } } -//#region 绘制函数 +//#region 自定义绘制函数 +/** + * 绘制点位的自定义函数 + * @param ctx Canvas 2D绘制上下文 + * @param pen 点位图形对象 + */ function drawPoint(ctx: CanvasRenderingContext2D, pen: MapPen): void { const theme = sTheme.editor; const { active, iconSize: r = 0, fontSize = 14, lineHeight = 1.5, fontFamily } = pen.calculative ?? {}; @@ -777,6 +865,10 @@ function drawPoint(ctx: CanvasRenderingContext2D, pen: MapPen): void { ctx.fillText(label, x + w / 2, y - fontSize * lineHeight); ctx.restore(); } +/** + * 设置点位的连接锚点 + * @param pen 点位图形对象 + */ function anchorPoint(pen: MapPen): void { pen.anchors = [ { penId: pen.id, id: '0', x: 0.5, y: 0.5 }, @@ -787,6 +879,11 @@ function anchorPoint(pen: MapPen): void { ]; } +/** + * 绘制路线的自定义函数 + * @param ctx Canvas 2D绘制上下文 + * @param pen 路线图形对象 + */ function drawLine(ctx: CanvasRenderingContext2D, pen: MapPen): void { const theme = sTheme.editor; const { active, lineWidth: s = 1 } = pen.calculative ?? {}; @@ -874,6 +971,11 @@ function lineBezier3(_: Meta2dStore, pen: MapPen): void { pen.calculative.worldAnchors[1].prev = { x: x2 + dx2 * s, y: y2 + dy2 * s }; } +/** + * 绘制区域的自定义函数 + * @param ctx Canvas 2D绘制上下文 + * @param pen 区域图形对象 + */ function drawArea(ctx: CanvasRenderingContext2D, pen: MapPen): void { const theme = sTheme.editor; const { active, fontSize = 14, lineHeight = 1.5, fontFamily } = pen.calculative ?? {}; @@ -895,6 +997,11 @@ function drawArea(ctx: CanvasRenderingContext2D, pen: MapPen): void { ctx.restore(); } +/** + * 绘制机器人的自定义函数 + * @param ctx Canvas 2D绘制上下文 + * @param pen 机器人图形对象 + */ function drawRobot(ctx: CanvasRenderingContext2D, pen: MapPen): void { const theme = sTheme.editor; const { lineWidth: s = 1 } = pen.calculative ?? {}; diff --git a/场景编辑器组件详细分析.md b/场景编辑器组件详细分析.md index c9f608e..8bfc833 100644 --- a/场景编辑器组件详细分析.md +++ b/场景编辑器组件详细分析.md @@ -4,36 +4,603 @@ `scene-editor.vue` 是一个基于 Vue 3 的复杂场景编辑器组件,主要用于管理和编辑工业机器人的场景配置。该组件提供了完整的场景编辑功能,包括机器人管理、路径规划、区域设置等。 -## 2. 核心功能分析 +## 2. 架构图示分析 -### 2.1 场景数据管理 +### 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` 格式文件的导入导出 -### 2.2 编辑器状态控制 +### 3.2 编辑器状态控制 - **编辑模式切换**: 通过 `editable` 状态控制编辑器的启用/禁用 - **权限管理**: 根据编辑状态显示不同的操作按钮和功能 - **实时状态同步**: 编辑状态变化时自动更新编辑器服务状态 -### 2.3 三大管理区域 +### 3.3 三大管理区域 - **机器人管理**: 显示和管理场景中的机器人组和单个机器人 - **库区管理**: 管理各种类型的库区(仅显示库区类型的区域) - **高级组管理**: 管理复杂的路径、点位、区域等元素 -### 2.4 详情卡片系统 +### 3.4 详情卡片系统 - **动态卡片显示**: 根据选中元素类型显示对应的详情卡片 - **编辑/查看模式**: 根据编辑状态显示编辑卡片或查看卡片 - **悬浮定位**: 卡片固定在右侧悬浮显示 -## 3. 技术架构分析 +## 4. 大场景渲染性能优化分析 -### 3.1 核心依赖关系 +### 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 // 主要导入依赖 @@ -42,9 +609,9 @@ import { EditorService } from '@core/editor.service'; // 编辑器服务 import { decodeTextFile, downloadFile, selectFile, textToBlob } from '@core/utils'; // 工具函数 ``` -### 3.2 组件架构设计 +### 6.2 组件架构设计 -#### 3.2.1 状态管理 +#### 6.2.1 状态管理 ```typescript // 核心状态定义 @@ -56,7 +623,7 @@ const container = shallowRef(); // 编辑器容器 const editor = shallowRef(); // 编辑器服务实例 ``` -#### 3.2.2 依赖注入系统 +#### 6.2.2 依赖注入系统 ```typescript const EDITOR_KEY = Symbol('editor-key'); @@ -65,9 +632,9 @@ provide(EDITOR_KEY, editor); 使用 Vue 3 的依赖注入机制,将编辑器服务注入到子组件中。 -### 3.3 EditorService 核心服务分析 +### 6.3 EditorService 核心服务分析 -#### 3.3.1 服务基础 +#### 6.3.1 服务基础 ```typescript export class EditorService extends Meta2d { @@ -76,7 +643,7 @@ export class EditorService extends Meta2d { } ``` -#### 3.3.2 核心方法 +#### 6.3.2 核心方法 - **load()**: 加载场景数据到编辑器 - **save()**: 保存当前场景数据 @@ -85,9 +652,9 @@ export class EditorService extends Meta2d { - **addArea()**: 添加区域 - **deleteById()**: 删除指定元素 -### 3.4 API 接口设计 +### 6.4 API 接口设计 -#### 3.4.1 场景相关API +#### 6.4.1 场景相关API ```typescript // 获取场景数据 @@ -100,7 +667,7 @@ export async function pushSceneById(id: string): Promise; export async function saveSceneById(id: string, json: string, png?: string): Promise; ``` -#### 3.4.2 文件操作工具 +#### 6.4.2 文件操作工具 ```typescript // 文件选择 @@ -116,9 +683,9 @@ export function textToBlob(text: string): Blob | undefined; export function downloadFile(url: string, name?: string): void; ``` -## 4. 从零开发实现过程 +## 7. 从零开发实现过程 -### 4.1 第一步:创建基础组件结构 +### 7.1 第一步:创建基础组件结构 ```vue