web-map/docs/编辑器服务核心架构分析.md

678 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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