# 编辑器服务核心架构分析

## 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 提供完整的类型检查

这个编辑器服务是一个设计精良的复杂系统，通过合理的架构设计实现了强大的场景编辑功能。
