# 编辑器服务核心架构分析 ## 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 提供完整的类型检查 这个编辑器服务是一个设计精良的复杂系统,通过合理的架构设计实现了强大的场景编辑功能。