# 基于事件的反应器

> 适用：业务自定义联动、统计监控、临时约束、操作日志。
> 关键示例：
> - `vjcad/example/reactor/01basic.js` — 圆 → 自动外接框
> - `vjcad/example/reactor/03multitype.js` — 实体统计 / 同类型颜色同步
> - `vjcad/example/reactor/05lifecycle.js` — 全生命周期日志

## 1. 最小骨架

事件型反应器的核心就是订阅 `CadEvents.EntityAdded / EntityModified / EntitiesErased`：

```js
class CircleBoundingBoxReactor {
    constructor() {
        this.boundingLines = new Map(); // ownerId -> Lines[]
    }
    install() {
        Engine.eventManager.on(CadEvents.EntityAdded, args => {
            if (args.entity.type === 'CIRCLE') this.create(args.entity);
        });
        Engine.eventManager.on(CadEvents.EntityModified, args => {
            if (args.entity.type === 'CIRCLE') this.update(args.entity);
        });
        Engine.eventManager.on(CadEvents.EntitiesErased, args => {
            args.entities.forEach(e => e.type === 'CIRCLE' && this.remove(e));
        });
    }
    create(c) { /* new LineEnt(...) -> Engine.addEntities */ }
    update(c) { /* 修改已存在的 line.startPoint/endPoint */ }
    remove(c) { /* Engine.eraseEntities(lines) */ }
}
new CircleBoundingBoxReactor().install();
```

> 完整代码：`vjcad/example/reactor/01basic.js`

> ⚠️ **`EntityModified` 的派发时机**：实体 setter / `setModified()` 只置 `isDirty`，**不发事件**；事件是在下一次 `Engine.regen()` 的 bucket 脏扫描里**批量**派发的（每个脏实体一次）。所以：① 编程式改完不 `regen` 就收不到；② 同一实体在两次 regen 之间改多次只收到一次；③ `Engine.eraseEntities` 是软删除（置 `isAlive=false` 并 `setModified()`），**刚删除的实体也会在下次 regen 收到一条 `EntityModified`**——回调开头先 `if (!args.entity.isAlive) return;`。

## 2. 防止递归触发

`EntityModified` 事件里再去改实体会再次触发 `EntityModified`，必须做闭锁。例如几何约束反应器：

```js
class EndpointConstraintReactor {
    constructor() {
        this.constraints = []; // {line1Id, point1, line2Id, point2}
        this.updating = false;
    }
    install() {
        Engine.eventManager.on(CadEvents.EntityModified, args => {
            if (this.updating || !args.entity.isAlive) return;
            this.updating = true;
            try {
                this.applyConstraints(args.entity);
            } finally {
                this.updating = false;
            }
        });
    }
}
```

> 完整代码：`vjcad/example/reactor/04constraint.js`

## 3. 多类型监控（统计 / 颜色同步）

订阅多个事件，按 `entity.type` 分类计数；颜色同步则用 `Engine.getEntities().filter(...)` 批量改：

```js
Engine.eventManager.on(CadEvents.EntityAdded, ({ entity }) => {
    if (entity.type in stats) {
        stats[entity.type]++;
        stats.total++;
        updateStatsText();
    }
});

Engine.eventManager.on(CadEvents.EntityModified, ({ entity }) => {
    const prev = typeColors.get(entity.type);
    if (prev !== undefined && entity.color !== prev) {
        Engine.getEntities()
            .filter(e => e.type === entity.type && e.color !== entity.color)
            .forEach(e => { e.color = entity.color; });
        Engine.regen();
    }
    typeColors.set(entity.type, entity.color);
});
```

> 完整代码：`vjcad/example/reactor/03multitype.js`

## 4. 生命周期日志（追踪某个实体的所有变化）

```js
Engine.eventManager.on(CadEvents.EntityAdded,    a => log('创建', a.entity));
Engine.eventManager.on(CadEvents.EntityModified, a => log('修改', a.entity));
Engine.eventManager.on(CadEvents.EntitiesErased, a => a.entities.forEach(e => log('删除', e)));
Engine.eventManager.on(CadEvents.SelectionChanged, a => {
    const n = a.selections?.length || 0;
    if (n > 0) log(`选择 ${n} 个实体`);
});
```

> 完整代码：`vjcad/example/reactor/05lifecycle.js`

## 5. 与 PLINE 等交互命令

`PLINE` / `SPLINE` 这类多点交互命令的事件时机比较特殊（首点不触发 `EntityAdded`、第二点起开始触发、命令结束才有最终实体），如果反应器要响应折线绘制完成，应当：

- 监听 `CadEvents.CommandEnded` + `args.commandName === 'PLINE'`，
- 然后取 `Engine.getEntities(e => e.type === 'PLINE').slice(-1)[0]` 拿到最终实体。

详见 `event/pline-timing.md`。

## 红旗

- 事件派发**不 await** 回调（`emit` 同步调用 handler、忽略返回值）：`async` 回调不会阻塞命令循环，但 `await` 之后再设 `args.cancel = true` 已经无效（`emit` 早已返回），回调返回的 rejected Promise 也不会被 `emit` 的 try/catch 捕获（变成 unhandledrejection）——可取消事件要同步判断、同步设 `cancel`，异步回调自己 try/catch
- 每次集中修改完一定要 `Engine.regen()`
- `off` 必须传入与 `on` 时**完全相同的 handler 引用**，否则解绑失败
- 文档关闭、视图重建后，事件监听不会自动清理；`DocumentClosing` 在当前版本**从不派发**，清理要放在自己的销毁流程里（如调 `cadView.destroy()` 之前、插件 `onDeactivate`）手动 `Engine.eventManager.off(...)`

## 参考（官方文档 MCP）

- 文档：`vjcad/doc/reactor/index.md`、`vjcad/doc/event/event-types.md`
- 示例：
  - `vjcad/example/reactor/01basic.js` — 圆 → 自动外接框
  - `vjcad/example/reactor/03multitype.js` — 实体统计 / 同类型颜色同步
  - `vjcad/example/reactor/04constraint.js` — 端点约束（防递归）
  - `vjcad/example/reactor/05lifecycle.js` — 全生命周期日志
