feat: 添加场景编辑器核心架构和组件详细分析文档
This commit is contained in:
parent
96def3b411
commit
b874ca43f0
508
场景编辑器组件详细分析.md
Normal file
508
场景编辑器组件详细分析.md
Normal file
@ -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<string>(''); // 场景标题
|
||||
const editable = ref<boolean>(false); // 编辑状态
|
||||
const show = ref<boolean>(true); // 卡片显示状态
|
||||
const current = ref<{ type: string; id: string }>(); // 当前选中元素
|
||||
const container = shallowRef<HTMLDivElement>(); // 编辑器容器
|
||||
const editor = shallowRef<EditorService>(); // 编辑器服务实例
|
||||
```
|
||||
|
||||
#### 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<SceneDetail | null>;
|
||||
|
||||
// 推送场景到数据库
|
||||
export async function pushSceneById(id: string): Promise<boolean>;
|
||||
|
||||
// 保存场景数据
|
||||
export async function saveSceneById(id: string, json: string, png?: string): Promise<boolean>;
|
||||
```
|
||||
|
||||
#### 3.4.2 文件操作工具
|
||||
|
||||
```typescript
|
||||
// 文件选择
|
||||
export async function selectFile(accept?: string, limit?: number): Promise<File | void>;
|
||||
|
||||
// 文件解码
|
||||
export async function decodeTextFile(file: File): Promise<string | undefined>;
|
||||
|
||||
// 文本转二进制
|
||||
export function textToBlob(text: string): Blob | undefined;
|
||||
|
||||
// 文件下载
|
||||
export function downloadFile(url: string, name?: string): void;
|
||||
```
|
||||
|
||||
## 4. 从零开发实现过程
|
||||
|
||||
### 4.1 第一步:创建基础组件结构
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
// 1. 定义组件属性
|
||||
type Props = {
|
||||
id: string; // 场景ID
|
||||
};
|
||||
const props = defineProps<Props>();
|
||||
|
||||
// 2. 基础状态管理
|
||||
const title = ref<string>('');
|
||||
const editable = ref<boolean>(false);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-layout class="full">
|
||||
<!-- 头部区域 -->
|
||||
<a-layout-header class="p-16" style="height: 64px">
|
||||
<!-- 标题和操作按钮 -->
|
||||
</a-layout-header>
|
||||
|
||||
<!-- 主体区域 -->
|
||||
<a-layout class="p-16">
|
||||
<!-- 左侧面板 -->
|
||||
<a-layout-sider :width="320">
|
||||
<!-- 选项卡内容 -->
|
||||
</a-layout-sider>
|
||||
|
||||
<!-- 编辑器区域 -->
|
||||
<a-layout-content>
|
||||
<div ref="container" class="editor-container full"></div>
|
||||
</a-layout-content>
|
||||
</a-layout>
|
||||
</a-layout>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 4.2 第二步:集成编辑器服务
|
||||
|
||||
```typescript
|
||||
// 1. 导入编辑器服务
|
||||
import { EditorService } from '@core/editor.service';
|
||||
|
||||
// 2. 创建编辑器实例
|
||||
const container = shallowRef<HTMLDivElement>();
|
||||
const editor = shallowRef<EditorService>();
|
||||
|
||||
// 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
|
||||
<template>
|
||||
<a-layout-sider :width="320">
|
||||
<a-tabs type="card">
|
||||
<!-- 机器人管理 -->
|
||||
<a-tab-pane key="1" :tab="$t('机器人')">
|
||||
<RobotGroups
|
||||
v-if="editor"
|
||||
:token="EDITOR_KEY"
|
||||
:sid="id"
|
||||
:editable="editable"
|
||||
:current="current?.id"
|
||||
@change="selectRobot"
|
||||
show-group-edit
|
||||
/>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 库区管理 -->
|
||||
<a-tab-pane key="2" :tab="$t('库区')">
|
||||
<PenGroups v-if="editor" :token="EDITOR_KEY" :current="current?.id" only-area1 />
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 高级组管理 -->
|
||||
<a-tab-pane key="3" :tab="$t('高级组')">
|
||||
<PenGroups v-if="editor" :token="EDITOR_KEY" :current="current?.id" />
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-layout-sider>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 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
|
||||
<template>
|
||||
<!-- 工具栏 -->
|
||||
<div v-if="editable" class="toolbar-container">
|
||||
<EditorToolbar v-if="editor" :token="EDITOR_KEY" :id="id" />
|
||||
</div>
|
||||
|
||||
<!-- 详情卡片 -->
|
||||
<template v-if="current?.id">
|
||||
<a-float-button style="top: 80px; right: 16px" shape="square" @click="show = !show">
|
||||
<template #icon><i class="icon detail" /></template>
|
||||
</a-float-button>
|
||||
|
||||
<div v-if="show" class="card-container">
|
||||
<RobotDetailCard v-if="isRobot" :token="EDITOR_KEY" :current="current.id" />
|
||||
|
||||
<template v-if="isPoint">
|
||||
<PointEditCard v-if="editable" :token="EDITOR_KEY" :id="current.id" />
|
||||
<PointDetailCard v-else :token="EDITOR_KEY" :current="current.id" />
|
||||
</template>
|
||||
|
||||
<!-- 其他卡片类型... -->
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
```
|
||||
|
||||
## 5. 子组件详细分析
|
||||
|
||||
### 5.1 RobotGroups 组件
|
||||
|
||||
**功能**: 管理机器人组和单个机器人
|
||||
**核心特性**:
|
||||
|
||||
- 机器人组的增删改查
|
||||
- 机器人的添加、注册、移除
|
||||
- 批量操作支持(全选、批量移除)
|
||||
- 搜索过滤功能
|
||||
|
||||
**关键实现**:
|
||||
|
||||
```typescript
|
||||
// 机器人列表获取
|
||||
const robots = computed<RobotInfo[]>(() => editor.value.robots.filter(({ label }) => label.includes(keyword.value)));
|
||||
|
||||
// 批量选择管理
|
||||
const selected = reactive<Set<RobotInfo['id']>>(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<MapPen[]>(() =>
|
||||
editor.value.points.value.filter(({ label }) => label?.includes(keyword.value)),
|
||||
);
|
||||
|
||||
// 区域列表(按类型分组)
|
||||
const areas = computed<MapPen[]>(() => editor.value.areas.value.filter(({ label }) => label?.includes(keyword.value)));
|
||||
```
|
||||
|
||||
### 5.3 EditorToolbar 组件
|
||||
|
||||
**功能**: 提供编辑工具栏
|
||||
**核心特性**:
|
||||
|
||||
- 区域添加工具(库区、互斥区、非互斥区)
|
||||
- 场景保存功能
|
||||
- 撤销/重做操作
|
||||
- 删除操作
|
||||
|
||||
**关键实现**:
|
||||
|
||||
```typescript
|
||||
// 区域添加模式
|
||||
const mode = ref<MapAreaType>();
|
||||
watch(editor.value.mouseBrush, (v) => {
|
||||
if (!mode.value) return;
|
||||
const [p1, p2] = v ?? [];
|
||||
if (isEmpty(p1) || isEmpty(p2)) return;
|
||||
editor.value.addArea(p1, p2, mode.value);
|
||||
mode.value = undefined;
|
||||
});
|
||||
```
|
||||
|
||||
## 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 设计和实现
|
||||
- 各子组件之间的通信机制
|
||||
- 状态管理的一致性
|
||||
- 性能优化和内存管理
|
||||
</rewritten_file>
|
677
编辑器服务核心架构分析.md
Normal file
677
编辑器服务核心架构分析.md
Normal file
@ -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<GroupSceneDetail>): Promise<void> {
|
||||
// 1. 解析 JSON 字符串为场景对象
|
||||
const scene: StandardScene = map ? JSON.parse(map) : {};
|
||||
|
||||
// 2. 如果有组详情,优先使用组数据
|
||||
if (!isEmpty(detail?.group)) {
|
||||
scene.robotGroups = [detail.group];
|
||||
scene.robots = detail.robots;
|
||||
}
|
||||
|
||||
// 3. 提取各类数据
|
||||
const { robotGroups, robots, points, routes, areas } = scene;
|
||||
|
||||
// 4. 初始化编辑器
|
||||
this.open(); // 打开 Meta2d 画布
|
||||
this.setState(editable); // 设置编辑状态
|
||||
|
||||
// 5. 按顺序加载各类元素
|
||||
this.#loadRobots(robotGroups, robots); // 加载机器人
|
||||
await this.#loadScenePoints(points); // 加载点位
|
||||
this.#loadSceneRoutes(routes); // 加载路线
|
||||
await this.#loadSceneAreas(areas); // 加载区域 ⭐
|
||||
|
||||
// 6. 清空历史记录
|
||||
this.store.historyIndex = undefined;
|
||||
this.store.histories = [];
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.2.2 区域加载详细过程
|
||||
|
||||
```typescript
|
||||
async #loadSceneAreas(areas?: StandardSceneArea[]): Promise<void> {
|
||||
if (!areas?.length) return;
|
||||
|
||||
// 并行处理所有区域
|
||||
await Promise.all(
|
||||
areas.map(async (v) => {
|
||||
// 1. 从场景数据中提取区域信息
|
||||
const { id, name, desc, x, y, w, h, type, points, routes, properties } = v;
|
||||
|
||||
// 2. 调用 addArea 方法在画布上创建实际的图形对象
|
||||
await this.addArea(
|
||||
{ x, y }, // 左上角坐标
|
||||
{ x: x + w, y: y + h }, // 右下角坐标
|
||||
type, // 区域类型
|
||||
id // 区域ID
|
||||
);
|
||||
|
||||
// 3. 设置区域的详细属性
|
||||
this.setValue(
|
||||
{
|
||||
id,
|
||||
label: name, // 显示名称
|
||||
desc, // 描述
|
||||
properties, // 自定义属性
|
||||
area: { type, points, routes } // 区域特定数据
|
||||
},
|
||||
{ render: false, history: false, doEvent: false }
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**关键理解点**:
|
||||
|
||||
- 场景文件中的 `areas` 数组包含了所有区域的完整信息
|
||||
- 每个区域包含位置 `(x, y, w, h)`、类型 `type`、关联的点位和路线
|
||||
- `addArea` 方法负责在 Canvas 上创建实际的可视化图形
|
||||
- `setValue` 方法设置图形对象的业务属性
|
||||
|
||||
## 4. 区域绘制原理详解(为什么可以在页面画一个区域)
|
||||
|
||||
### 4.1 鼠标事件监听系统
|
||||
|
||||
```typescript
|
||||
// 鼠标事件主题
|
||||
readonly #mouse$$ = new Subject<{ type: 'click' | 'mousedown' | 'mouseup'; value: Point }>();
|
||||
|
||||
// 点击事件流
|
||||
public readonly mouseClick = useObservable<Point>(
|
||||
this.#mouse$$.pipe(
|
||||
filter(({ type }) => type === 'click'),
|
||||
debounceTime(100),
|
||||
map(({ value }) => value),
|
||||
),
|
||||
);
|
||||
|
||||
// 拖拽事件流 ⭐ 关键!这是画区域的核心
|
||||
public readonly mouseBrush = useObservable<[Point, Point]>(
|
||||
this.#mouse$$.pipe(
|
||||
filter(({ type }) => type === 'mousedown'), // 监听鼠标按下
|
||||
switchMap(({ value: s }) =>
|
||||
this.#mouse$$.pipe(
|
||||
filter(({ type }) => type === 'mouseup'), // 监听鼠标抬起
|
||||
map(({ value: e }) => <[Point, Point]>[s, e]), // 返回起始和结束点
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
### 4.2 工具栏组件中的区域创建监听
|
||||
|
||||
```typescript
|
||||
// 在 EditorToolbar 组件中
|
||||
const mode = ref<MapAreaType>();
|
||||
|
||||
// 监听鼠标拖拽事件
|
||||
watch(editor.value.mouseBrush, (v) => {
|
||||
if (!mode.value) return; // 如果没有选择区域工具,不处理
|
||||
const [p1, p2] = v ?? []; // 获取起始点和结束点
|
||||
if (isEmpty(p1) || isEmpty(p2)) return; // 验证点位有效性
|
||||
|
||||
// 调用编辑器服务创建区域 ⭐
|
||||
editor.value.addArea(p1, p2, mode.value);
|
||||
mode.value = undefined; // 重置工具状态
|
||||
});
|
||||
```
|
||||
|
||||
### 4.3 addArea 方法详细实现
|
||||
|
||||
```typescript
|
||||
public async addArea(p1: Point, p2: Point, type = MapAreaType.库区, id?: string) {
|
||||
// 1. 获取当前缩放比例
|
||||
const scale = this.data().scale ?? 1;
|
||||
|
||||
// 2. 计算区域宽高
|
||||
const w = Math.abs(p1.x - p2.x);
|
||||
const h = Math.abs(p1.y - p2.y);
|
||||
|
||||
// 3. 最小尺寸检查(防止创建过小的区域)
|
||||
if (w * scale < 50 || h * scale < 60) return;
|
||||
|
||||
// 4. 准备关联数据
|
||||
const points = new Array<string>();
|
||||
const routes = new Array<string>();
|
||||
|
||||
if (!id) {
|
||||
id = s8(); // 生成唯一ID
|
||||
const selected = <MapPen[]>this.store.active; // 获取当前选中的元素
|
||||
|
||||
// 5. 根据区域类型自动关联相关元素
|
||||
switch (type) {
|
||||
case MapAreaType.库区:
|
||||
// 库区只关联动作点
|
||||
selected?.filter(({ point }) => point?.type === MapPointType.动作点)
|
||||
.forEach(({ id }) => points.push(id!));
|
||||
break;
|
||||
case MapAreaType.互斥区:
|
||||
// 互斥区关联所有点位和路线
|
||||
selected?.filter(({ point }) => point?.type)
|
||||
.forEach(({ id }) => points.push(id!));
|
||||
selected?.filter(({ route }) => route?.type)
|
||||
.forEach(({ id }) => routes.push(id!));
|
||||
break;
|
||||
case MapAreaType.非互斥区:
|
||||
// 非互斥区只关联点位
|
||||
selected?.filter(({ point }) => point?.type)
|
||||
.forEach(({ id }) => points.push(id!));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 6. 创建区域图形对象
|
||||
const pen: MapPen = {
|
||||
id,
|
||||
name: 'area', // 图形类型标识
|
||||
tags: ['area', `area-${type}`], // 标签用于查找和分类
|
||||
label: `A${id}`, // 显示标签
|
||||
x: Math.min(p1.x, p2.x), // 左上角 X
|
||||
y: Math.min(p1.y, p2.y), // 左上角 Y
|
||||
width: w, // 宽度
|
||||
height: h, // 高度
|
||||
lineWidth: 1, // 边框宽度
|
||||
area: { type, points, routes }, // 区域业务数据
|
||||
locked: LockState.DisableMoveScale, // 锁定状态(禁止移动缩放)
|
||||
};
|
||||
|
||||
// 7. 添加到画布并设置层级
|
||||
const area = await this.addPen(pen, true, true, true);
|
||||
this.bottom(area); // 将区域放到最底层
|
||||
}
|
||||
```
|
||||
|
||||
**关键理解点**:
|
||||
|
||||
1. **事件流处理**:通过 RxJS 的事件流来处理鼠标拖拽
|
||||
2. **坐标计算**:将鼠标坐标转换为画布坐标系中的区域
|
||||
3. **图形对象创建**:创建符合 Meta2d 要求的图形对象
|
||||
4. **层级管理**:区域作为背景层,放在最底层
|
||||
5. **状态管理**:自动关联当前选中的相关元素
|
||||
|
||||
## 5. 自定义绘制系统
|
||||
|
||||
### 5.1 绘制函数注册
|
||||
|
||||
```typescript
|
||||
#register() {
|
||||
// 注册基础图形
|
||||
this.register({ line: () => new Path2D() });
|
||||
|
||||
// 注册自定义绘制函数 ⭐
|
||||
this.registerCanvasDraw({
|
||||
point: drawPoint, // 点位绘制
|
||||
line: drawLine, // 路线绘制
|
||||
area: drawArea, // 区域绘制 ⭐
|
||||
robot: drawRobot // 机器人绘制
|
||||
});
|
||||
|
||||
// 注册锚点
|
||||
this.registerAnchors({ point: anchorPoint });
|
||||
|
||||
// 注册线条绘制函数
|
||||
this.addDrawLineFn('bezier2', lineBezier2);
|
||||
this.addDrawLineFn('bezier3', lineBezier3);
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 区域绘制函数详解
|
||||
|
||||
```typescript
|
||||
function drawArea(ctx: CanvasRenderingContext2D, pen: MapPen): void {
|
||||
// 1. 获取主题配置
|
||||
const theme = sTheme.editor;
|
||||
|
||||
// 2. 获取绘制参数
|
||||
const { active, fontSize = 14, lineHeight = 1.5, fontFamily } = pen.calculative ?? {};
|
||||
const { x = 0, y = 0, width: w = 0, height: h = 0 } = pen.calculative?.worldRect ?? {};
|
||||
const { type } = pen.area ?? {};
|
||||
const { label = '' } = pen ?? {};
|
||||
|
||||
// 3. 开始绘制
|
||||
ctx.save();
|
||||
|
||||
// 4. 绘制矩形区域
|
||||
ctx.rect(x, y, w, h);
|
||||
|
||||
// 5. 填充颜色(根据区域类型)
|
||||
ctx.fillStyle = get(theme, `area.fill-${type}`) ?? '';
|
||||
ctx.fill();
|
||||
|
||||
// 6. 绘制边框(根据激活状态)
|
||||
ctx.strokeStyle = get(theme, active ? 'area.strokeActive' : `area.stroke-${type}`) ?? '';
|
||||
ctx.stroke();
|
||||
|
||||
// 7. 绘制标签文字
|
||||
ctx.fillStyle = get(theme, 'color') ?? '';
|
||||
ctx.font = `${fontSize}px/${lineHeight} ${fontFamily}`;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'top';
|
||||
ctx.fillText(label, x + w / 2, y - fontSize * lineHeight);
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
```
|
||||
|
||||
**关键理解点**:
|
||||
|
||||
- Canvas 2D API 直接绘制矩形和文字
|
||||
- 主题系统提供颜色配置
|
||||
- 根据区域类型和激活状态使用不同的样式
|
||||
- 文字标签显示在区域上方
|
||||
|
||||
## 6. 响应式状态管理
|
||||
|
||||
### 6.1 数据流设计
|
||||
|
||||
```typescript
|
||||
// 变化事件主题
|
||||
readonly #change$$ = new Subject<boolean>();
|
||||
|
||||
// 区域列表响应式数据
|
||||
public readonly areas = useObservable<MapPen[], MapPen[]>(
|
||||
this.#change$$.pipe(
|
||||
filter((v) => v), // 只响应数据变化事件
|
||||
debounceTime(100), // 防抖处理
|
||||
map(() => this.find('area')), // 查找所有区域
|
||||
),
|
||||
{ initialValue: new Array<MapPen>() },
|
||||
);
|
||||
|
||||
// 当前选中元素
|
||||
public readonly current = useObservable<MapPen>(
|
||||
this.#change$$.pipe(
|
||||
debounceTime(100),
|
||||
map(() => <MapPen>clone(this.store.active?.[0])),
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
### 6.2 事件监听系统
|
||||
|
||||
```typescript
|
||||
#listen(e: unknown, v: any) {
|
||||
switch (e) {
|
||||
case 'opened':
|
||||
this.#load(sTheme.theme);
|
||||
this.#change$$.next(true); // 触发数据更新
|
||||
break;
|
||||
|
||||
case 'add':
|
||||
this.#change$$.next(true); // 元素添加后更新
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
this.#onDelete(v);
|
||||
this.#change$$.next(true); // 元素删除后更新
|
||||
break;
|
||||
|
||||
case 'update':
|
||||
case 'valueUpdate':
|
||||
this.#change$$.next(true); // 元素更新后更新
|
||||
break;
|
||||
|
||||
case 'active':
|
||||
case 'inactive':
|
||||
this.#change$$.next(false); // 选择状态变化
|
||||
break;
|
||||
|
||||
case 'click':
|
||||
case 'mousedown':
|
||||
case 'mouseup':
|
||||
// 将鼠标事件传递给事件流
|
||||
this.#mouse$$.next({ type: e, value: pick(v, 'x', 'y') });
|
||||
break;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 7. 场景保存原理
|
||||
|
||||
### 7.1 保存入口函数
|
||||
|
||||
```typescript
|
||||
public save(): string {
|
||||
// 1. 构建标准场景对象
|
||||
const scene: StandardScene = {
|
||||
robotGroups: this.robotGroups.value,
|
||||
robots: this.robots,
|
||||
// 2. 将画布上的图形对象转换为标准格式
|
||||
points: this.points.value.map((v) => this.#mapScenePoint(v)).filter((v) => !isNil(v)),
|
||||
routes: this.routes.value.map((v) => this.#mapSceneRoute(v)).filter((v) => !isNil(v)),
|
||||
areas: this.areas.value.map((v) => this.#mapSceneArea(v)).filter((v) => !isNil(v)), // ⭐
|
||||
blocks: [],
|
||||
};
|
||||
|
||||
// 3. 序列化为 JSON 字符串
|
||||
return JSON.stringify(scene);
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 区域数据映射
|
||||
|
||||
```typescript
|
||||
#mapSceneArea(pen: MapPen): StandardSceneArea | null {
|
||||
if (!pen.id || isEmpty(pen.area)) return null;
|
||||
|
||||
// 1. 提取基础信息
|
||||
const { id, label, desc, properties } = pen;
|
||||
const { type, points, routes } = pen.area;
|
||||
|
||||
// 2. 获取区域的实际位置和尺寸
|
||||
const { x, y, width, height } = this.getPenRect(pen);
|
||||
|
||||
// 3. 构建标准区域对象
|
||||
const area: StandardSceneArea = {
|
||||
id,
|
||||
name: label || id,
|
||||
desc,
|
||||
x, // 左上角 X 坐标
|
||||
y, // 左上角 Y 坐标
|
||||
w: width, // 宽度
|
||||
h: height, // 高度
|
||||
type, // 区域类型
|
||||
config: {},
|
||||
properties,
|
||||
};
|
||||
|
||||
// 4. 根据区域类型设置关联数据
|
||||
if (MapAreaType.库区 === type) {
|
||||
// 库区只保存动作点
|
||||
area.points = points?.filter((v) =>
|
||||
this.getPenById(v)?.point?.type === MapPointType.动作点
|
||||
);
|
||||
}
|
||||
|
||||
if ([MapAreaType.互斥区, MapAreaType.非互斥区].includes(type)) {
|
||||
// 互斥区和非互斥区保存所有非禁行点
|
||||
area.points = points?.filter((v) => {
|
||||
const { point } = this.getPenById(v) ?? {};
|
||||
if (isNil(point)) return false;
|
||||
if (point.type === MapPointType.禁行点) return false;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
if (MapAreaType.互斥区 === type) {
|
||||
// 互斥区还要保存关联的路线
|
||||
area.routes = routes?.filter((v) => !isEmpty(this.getPenById(v)?.area));
|
||||
}
|
||||
|
||||
return area;
|
||||
}
|
||||
```
|
||||
|
||||
## 8. 机器人管理系统
|
||||
|
||||
### 8.1 机器人数据结构
|
||||
|
||||
```typescript
|
||||
// 机器人映射表(响应式)
|
||||
readonly #robotMap = reactive<Map<RobotInfo['id'], RobotInfo>>(new Map());
|
||||
|
||||
// 机器人组流
|
||||
readonly #robotGroups$$ = new BehaviorSubject<RobotGroup[]>([]);
|
||||
public readonly robotGroups = useObservable<RobotGroup[]>(
|
||||
this.#robotGroups$$.pipe(debounceTime(300))
|
||||
);
|
||||
```
|
||||
|
||||
### 8.2 实时机器人更新
|
||||
|
||||
```typescript
|
||||
public refreshRobot(id: RobotInfo['id'], info: Partial<RobotRealtimeInfo>): void {
|
||||
const pen = this.getPenById(id);
|
||||
const { rotate: or, robot } = pen ?? {};
|
||||
if (!robot?.type) return;
|
||||
|
||||
// 1. 获取当前位置
|
||||
const { x: ox, y: oy } = this.getPenRect(pen!);
|
||||
|
||||
// 2. 解析实时数据
|
||||
const { x: cx = 37, y: cy = 37, active, angle, path: points } = info;
|
||||
|
||||
// 3. 计算新位置(机器人中心点偏移)
|
||||
const x = cx - 37;
|
||||
const y = cy - 37;
|
||||
const rotate = angle ?? or;
|
||||
|
||||
// 4. 处理路径数据
|
||||
const path = points?.map((p) => ({ x: p.x - cx, y: p.y - cy })) ??
|
||||
robot.path?.map((p) => ({ x: p.x + ox! - x, y: p.y + oy! - y }));
|
||||
|
||||
// 5. 更新机器人状态
|
||||
const o = { ...robot, ...omitBy({ active, path }, isNil) };
|
||||
|
||||
// 6. 根据激活状态使用不同的更新策略
|
||||
if (isNil(active)) {
|
||||
// 只更新位置和路径
|
||||
this.setValue(
|
||||
{ id, x, y, rotate, robot: o, visible: true },
|
||||
{ render: true, history: false, doEvent: false }
|
||||
);
|
||||
} else {
|
||||
// 同时更新图片资源
|
||||
this.setValue(
|
||||
{
|
||||
id,
|
||||
...this.#mapRobotImage(robot.type, active),
|
||||
x, y, rotate, robot: o, visible: true
|
||||
},
|
||||
{ render: true, history: false, doEvent: false }
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 9. 点位和路线管理
|
||||
|
||||
### 9.1 点位创建
|
||||
|
||||
```typescript
|
||||
public async addPoint(p: Point, type = MapPointType.普通点, id?: string): Promise<void> {
|
||||
id ||= s8();
|
||||
|
||||
// 1. 创建点位图形对象
|
||||
const pen: MapPen = {
|
||||
...p, // 坐标
|
||||
...this.#mapPoint(type), // 尺寸配置
|
||||
...this.#mapPointImage(type), // 图片配置
|
||||
id,
|
||||
name: 'point',
|
||||
tags: ['point'],
|
||||
label: `P${id}`,
|
||||
point: { type },
|
||||
};
|
||||
|
||||
// 2. 调整坐标到中心点
|
||||
pen.x! -= pen.width! / 2;
|
||||
pen.y! -= pen.height! / 2;
|
||||
|
||||
// 3. 添加到画布
|
||||
await this.addPen(pen, false, true, true);
|
||||
}
|
||||
```
|
||||
|
||||
### 9.2 路线创建
|
||||
|
||||
```typescript
|
||||
public addRoute(p: [MapPen, MapPen], type = MapRouteType.直线, id?: string): void {
|
||||
const [p1, p2] = p;
|
||||
if (!p1?.anchors?.length || !p2?.anchors?.length) return;
|
||||
|
||||
// 1. 连接两个点位
|
||||
const line = this.connectLine(p1, p2, undefined, undefined, false);
|
||||
|
||||
// 2. 设置ID
|
||||
id ||= line.id!;
|
||||
this.changePenId(line.id!, id);
|
||||
|
||||
// 3. 设置路线属性
|
||||
const pen: MapPen = { tags: ['route'], route: { type }, lineWidth: 1 };
|
||||
this.setValue({ id, ...pen }, { render: false, history: false, doEvent: false });
|
||||
|
||||
// 4. 更新线条类型
|
||||
this.updateLineType(line, type);
|
||||
|
||||
// 5. 选中并渲染
|
||||
this.active(id);
|
||||
this.render();
|
||||
}
|
||||
```
|
||||
|
||||
## 10. 主题系统集成
|
||||
|
||||
### 10.1 主题响应
|
||||
|
||||
```typescript
|
||||
// 监听主题变化
|
||||
watch(
|
||||
() => sTheme.theme,
|
||||
(v) => this.#load(v),
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
#load(theme: string): void {
|
||||
// 1. 设置 Meta2d 主题
|
||||
this.setTheme(theme);
|
||||
|
||||
// 2. 更新编辑器配置
|
||||
this.setOptions({ color: get(sTheme.editor, 'color') });
|
||||
|
||||
// 3. 更新所有点位图片
|
||||
this.find('point').forEach((pen) => {
|
||||
if (!pen.point?.type) return;
|
||||
if (pen.point.type < 10) return;
|
||||
this.canvas.updateValue(pen, this.#mapPointImage(pen.point.type));
|
||||
});
|
||||
|
||||
// 4. 更新所有机器人图片
|
||||
this.find('robot').forEach((pen) => {
|
||||
if (!pen.robot?.type) return;
|
||||
this.canvas.updateValue(pen, this.#mapRobotImage(pen.robot.type, pen.robot.active));
|
||||
});
|
||||
|
||||
// 5. 重新渲染
|
||||
this.render();
|
||||
}
|
||||
```
|
||||
|
||||
## 11. 性能优化策略
|
||||
|
||||
### 11.1 防抖处理
|
||||
|
||||
```typescript
|
||||
// 所有响应式数据都使用防抖
|
||||
debounceTime(100); // 100ms 防抖
|
||||
debounceTime(300); // 300ms 防抖(机器人组)
|
||||
```
|
||||
|
||||
### 11.2 浅层响应式
|
||||
|
||||
```typescript
|
||||
// 使用 shallowRef 避免深度响应式
|
||||
const editor = shallowRef<EditorService>();
|
||||
```
|
||||
|
||||
### 11.3 并行处理
|
||||
|
||||
```typescript
|
||||
// 场景加载时并行处理
|
||||
await Promise.all(
|
||||
areas.map(async (v) => {
|
||||
await this.addArea(/* ... */);
|
||||
}),
|
||||
);
|
||||
```
|
||||
|
||||
## 12. 总结
|
||||
|
||||
### 12.1 画区域的完整流程
|
||||
|
||||
1. **工具选择**:用户点击工具栏的区域工具,设置 `mode`
|
||||
2. **鼠标监听**:`mouseBrush` 流监听鼠标拖拽事件
|
||||
3. **坐标获取**:获取拖拽的起始点和结束点
|
||||
4. **区域创建**:调用 `addArea` 方法创建区域对象
|
||||
5. **画布绘制**:`drawArea` 函数在 Canvas 上绘制实际图形
|
||||
6. **状态更新**:触发响应式数据更新,通知 Vue 组件
|
||||
|
||||
### 12.2 场景文件生成区域的完整流程
|
||||
|
||||
1. **文件解析**:将 JSON 字符串解析为 `StandardScene` 对象
|
||||
2. **数据提取**:从 `areas` 数组中提取每个区域的信息
|
||||
3. **图形创建**:调用 `addArea` 方法在画布上创建图形对象
|
||||
4. **属性设置**:通过 `setValue` 设置业务属性
|
||||
5. **绘制渲染**:自定义绘制函数在 Canvas 上渲染图形
|
||||
|
||||
### 12.3 架构优势
|
||||
|
||||
1. **分层设计**:业务逻辑与图形引擎分离
|
||||
2. **响应式驱动**:状态变化自动更新 UI
|
||||
3. **事件流处理**:RxJS 提供强大的异步事件处理
|
||||
4. **自定义绘制**:完全控制图形的渲染效果
|
||||
5. **类型安全**:TypeScript 提供完整的类型检查
|
||||
|
||||
这个编辑器服务是一个设计精良的复杂系统,通过合理的架构设计实现了强大的场景编辑功能。
|
Loading…
x
Reference in New Issue
Block a user