diff --git a/场景编辑器组件详细分析.md b/场景编辑器组件详细分析.md new file mode 100644 index 0000000..c9f608e --- /dev/null +++ b/场景编辑器组件详细分析.md @@ -0,0 +1,508 @@ +# 场景编辑器组件详细分析 + +## 1. 组件概述 + +`scene-editor.vue` 是一个基于 Vue 3 的复杂场景编辑器组件,主要用于管理和编辑工业机器人的场景配置。该组件提供了完整的场景编辑功能,包括机器人管理、路径规划、区域设置等。 + +## 2. 核心功能分析 + +### 2.1 场景数据管理 + +- **场景读取**: 通过 `getSceneById` API 获取场景数据 +- **场景推送**: 通过 `pushSceneById` API 将场景数据推送到数据库 +- **场景保存**: 通过编辑器服务保存场景配置 +- **文件导入/导出**: 支持 `.scene` 格式文件的导入导出 + +### 2.2 编辑器状态控制 + +- **编辑模式切换**: 通过 `editable` 状态控制编辑器的启用/禁用 +- **权限管理**: 根据编辑状态显示不同的操作按钮和功能 +- **实时状态同步**: 编辑状态变化时自动更新编辑器服务状态 + +### 2.3 三大管理区域 + +- **机器人管理**: 显示和管理场景中的机器人组和单个机器人 +- **库区管理**: 管理各种类型的库区(仅显示库区类型的区域) +- **高级组管理**: 管理复杂的路径、点位、区域等元素 + +### 2.4 详情卡片系统 + +- **动态卡片显示**: 根据选中元素类型显示对应的详情卡片 +- **编辑/查看模式**: 根据编辑状态显示编辑卡片或查看卡片 +- **悬浮定位**: 卡片固定在右侧悬浮显示 + +## 3. 技术架构分析 + +### 3.1 核心依赖关系 + +```typescript +// 主要导入依赖 +import { getSceneById, pushSceneById } from '@api/scene'; // 场景API +import { EditorService } from '@core/editor.service'; // 编辑器服务 +import { decodeTextFile, downloadFile, selectFile, textToBlob } from '@core/utils'; // 工具函数 +``` + +### 3.2 组件架构设计 + +#### 3.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(); // 编辑器服务实例 +``` + +#### 3.2.2 依赖注入系统 + +```typescript +const EDITOR_KEY = Symbol('editor-key'); +provide(EDITOR_KEY, editor); +``` + +使用 Vue 3 的依赖注入机制,将编辑器服务注入到子组件中。 + +### 3.3 EditorService 核心服务分析 + +#### 3.3.1 服务基础 + +```typescript +export class EditorService extends Meta2d { + // 继承自 Meta2d 图形引擎 + // 提供场景编辑的核心功能 +} +``` + +#### 3.3.2 核心方法 + +- **load()**: 加载场景数据到编辑器 +- **save()**: 保存当前场景数据 +- **setState()**: 设置编辑器状态(可编辑/只读) +- **updateRobots()**: 更新机器人数据 +- **addArea()**: 添加区域 +- **deleteById()**: 删除指定元素 + +### 3.4 API 接口设计 + +#### 3.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; +``` + +#### 3.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; +``` + +## 4. 从零开发实现过程 + +### 4.1 第一步:创建基础组件结构 + +```vue + + + +``` + +### 4.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); +``` + +### 4.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' }, +); +``` + +### 4.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); +}; +``` + +### 4.5 第五步:集成管理组件 + +```vue + +``` + +### 4.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(); +}; +``` + +### 4.7 第七步:添加工具栏和详情卡片 + +```vue + +``` + +## 5. 子组件详细分析 + +### 5.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(); + } +}; +``` + +### 5.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))); +``` + +### 5.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; +}); +``` + +## 6. 样式设计分析 + +### 6.1 布局结构 + +- **头部**: 固定高度64px,包含标题和操作按钮 +- **主体**: 左侧面板320px宽度,右侧编辑器自适应 +- **工具栏**: 固定在底部中央,悬浮显示 +- **详情卡片**: 固定在右侧,320px宽度,悬浮显示 + +### 6.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; + } +} +``` + +## 7. 维护和调试指南 + +### 7.1 常见问题排查 + +#### 问题1: 场景数据加载失败 + +**排查步骤**: + +1. 检查 `props.id` 是否正确传入 +2. 检查 `getSceneById` API 是否正常响应 +3. 检查编辑器服务是否正确初始化 + +#### 问题2: 编辑器功能异常 + +**排查步骤**: + +1. 检查 `container` 元素是否正确获取 +2. 检查 `EditorService` 是否正确实例化 +3. 检查依赖注入是否正常工作 + +#### 问题3: 文件导入导出失败 + +**排查步骤**: + +1. 检查工具函数是否正确导入 +2. 检查文件格式是否正确 +3. 检查浏览器兼容性 + +### 7.2 性能优化建议 + +1. **使用 shallowRef**: 对于大对象使用 `shallowRef` 避免深度响应式 +2. **组件懒加载**: 使用 `v-if` 控制组件渲染时机 +3. **事件防抖**: 对于频繁触发的事件(如搜索)使用防抖 +4. **内存管理**: 及时清理事件监听器和定时器 + +### 7.3 扩展开发指南 + +#### 添加新的元素类型 + +1. 在 `EditorService` 中添加对应的管理方法 +2. 在 `PenGroups` 组件中添加新的分组 +3. 创建对应的详情卡片组件 +4. 在主组件中添加类型判断逻辑 + +#### 添加新的工具 + +1. 在 `EditorToolbar` 组件中添加工具按钮 +2. 在 `EditorService` 中实现对应功能 +3. 处理工具状态管理和交互逻辑 + +## 8. 总结 + +这个场景编辑器组件是一个功能完整、架构清晰的复杂组件,主要特点: + +1. **模块化设计**: 通过子组件分离不同功能模块 +2. **服务化架构**: 核心逻辑封装在 EditorService 中 +3. **响应式状态管理**: 使用 Vue 3 的响应式系统管理复杂状态 +4. **依赖注入**: 通过 provide/inject 实现服务共享 +5. **文件操作**: 完整的文件导入导出功能 +6. **用户体验**: 良好的交互设计和视觉反馈 + +对于维护和扩展这个组件,需要重点关注: + +- EditorService 的 API 设计和实现 +- 各子组件之间的通信机制 +- 状态管理的一致性 +- 性能优化和内存管理 + diff --git a/编辑器服务核心架构分析.md b/编辑器服务核心架构分析.md new file mode 100644 index 0000000..82386c0 --- /dev/null +++ b/编辑器服务核心架构分析.md @@ -0,0 +1,677 @@ +# 编辑器服务核心架构分析 + +## 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 提供完整的类型检查 + +这个编辑器服务是一个设计精良的复杂系统,通过合理的架构设计实现了强大的场景编辑功能。