> **本文回答**：实体事件 9 个：`EntityAdding/EntityAdded/EntitiesAdding/EntitiesAdded/EntityModified/EntityErasing/EntityErased/EntitiesErasing/EntitiesErased`。`-ing` 可取消。`Adding/Erasing` 由 `args.entity` 携带；`-Modified` 表示已修改；批量版传 `entities[]`。**注意**：交互式命令（如 PLINE）只在第 2 个点时触发 `EntityAdded`，后续点都走 `EntityModified`，详见 [pline-timing](pline-timing.md)。

## 一、事件清单

| 事件 | 说明 | 可取消 |
|---|---|---|
| `EntityAdding` | 单个添加前 | 是 |
| `EntityAdded` | 单个添加后 | 否 |
| `EntitiesAdding` | 批量添加前 | 是 |
| `EntitiesAdded` | 批量添加后 | 否 |
| `EntityModified` | 实体已修改 | 否 |
| `EntityErasing` | ~~单个删除前~~ ⚠️ 死事件 | — |
| `EntityErased` | ~~单个删除后~~ ⚠️ 死事件 | — |
| `EntitiesErasing` | 批量删除前 | 是 |
| `EntitiesErased` | 批量删除后 | 否 |

> ⚠️ `EntityErasing`/`EntityErased` **单数版在发布包里没有任何派发点**（`emit` 调用为 0）——`Engine.eraseEntities` 只发 `EntitiesErasing`/`EntitiesErased` 复数版。订阅单数版永远不会触发；"阻止删除"要监听 **`EntitiesErasing`** 并检查 `args.entities`。

MCP 参考：`vjcad/doc/event/event-types.md`。

## 二、监听添加 / 修改 / 删除

```js
import { Engine, CadEvents } from 'vjcad';

Engine.eventManager.on(CadEvents.EntityAdded, (args) => {
  console.log('已添加:', args.entity.type, 'id=', args.entity.id);
});

Engine.eventManager.on(CadEvents.EntityModified, (args) => {
  console.log('已修改:', args.entity.type, 'id=', args.entity.id);
});

Engine.eventManager.on(CadEvents.EntitiesErased, (args) => {
  // ⚠️ 勿用 EntityErased（单数从不派发）；复数版 args.entities / args.entityIds
  args.entities.forEach(e => console.log('已删除:', e.type, 'id=', e.id));
});
```

MCP 参考：`vjcad/example/event/01entityadded.js`、`02entitymodified.js`。

## 三、阻止删除（可取消）

```js
// ⚠️ 只有复数 EntitiesErasing 派发且可取消；单数 EntityErasing 是死事件
Engine.eventManager.on(CadEvents.EntitiesErasing, (args) => {
  if (args.entities.some(e => e.layer === '保护图层')) {
    args.cancel = true;
    Engine.writeMessage?.('该图层禁止删除');
  }
});

// 部分排除：只保护其中一部分，其余照删
Engine.eventManager.on(CadEvents.EntitiesErasing, (args) => {
  args.excludeEntities = args.entities.filter(e => e.layer === '保护图层');
});
```

> ⚠️ **取消/排除后撤销栈仍会记录原数组**：`Engine.eraseEntities(ents)` 与内置 `ERASE` 命令都是先 `currentSpace.erase(ents)` 再无条件 `erased_undoMark(ents)`，不看取消/排除结果。于是撤销栈多出一条"删除"记录：Ctrl+Z 无可见变化，随后 Ctrl+Y（`pushRedo` 把记录里所有实体 `isAlive=false`）会**把受保护实体也删掉**。自己写删除逻辑时，用 `const erased = Engine.currentSpace.erase(ents)` 拿实际删除的数组，再 `if (erased.length) Engine.undoManager.erased_undoMark(erased)`。

MCP 参考：`vjcad/doc/event/event-usage.md`；撤销行为见 `Engine.eraseEntities`。

## 四、批量版事件

```js
Engine.eventManager.on(CadEvents.EntitiesAdded, (args) => {
  console.log('批量添加:', args.entities.length);
});

Engine.eventManager.on(CadEvents.EntitiesErasing, (args) => {
  if (args.entities.some(e => e.layer === '保护图层')) {
    args.cancel = true;
  }
});
```

> **注意**：`Engine.addEntities` 无论传单个还是数组，都逐个走 `pcanvas.addEntity → pushEntity`，对**每个实体**触发 `EntityAdding/EntityAdded`，**不会**触发批量版。`EntitiesAdding/EntitiesAdded` 只由 `Engine.currentSpace.addEntities(arr)`（`BlockDefinition.addEntities`）派发——期间单实体事件被 `suspendEvents()` 暂停并丢弃；但这条底层路径不记撤销、不刷新可选实体列表（见 [core/document-space](../core/document-space.md)）。删除则只有批量版：`Engine.eraseEntities` / `currentSpace.erase` 都发 `EntitiesErasing/EntitiesErased`。

## 五、`EntityModified` 触发时机

- 直接修改属性后（`entity.color = ...`）调用 `entity.setModified()` **只置 `_modified` 旗标，不直接派发**——`EntityModified` 在**下一次 `Engine.regen()` 的 bucket 脏扫描**时派发（emit 点全部位于 bucket/regen 内部，且经 `suspendEvents` 队列延迟到 `resumeEvents` 才送达）。所以"改属性 + `setModified()` + `Engine.regen()`"才是完整链路。
- `move/rotate/scale/mirror/clone` 等方法内部（同样经 regen 通道派发）
- 交互式命令（PLINE 等）的中间步骤
- ⚠️ `Engine.updateEntities`/`Engine.pcanvas.updateEntities` **均不存在**（d.ts 亦无）——不要按 IDE 的习惯去调。

MCP 参考：`vjcad/example/event/02entitymodified.js`、`vjcad/example/event/06plinepoints.js`。

## 六、典型场景

```js
// 自动保存 hook
let dirty = false;
Engine.eventManager.on(CadEvents.EntityAdded,    () => dirty = true);
Engine.eventManager.on(CadEvents.EntityModified, () => dirty = true);
Engine.eventManager.on(CadEvents.EntitiesErased, () => dirty = true); // ⚠️ 单数 EntityErased 是死事件

// 实体添加前验证
Engine.eventManager.on(CadEvents.EntityAdding, (args) => {
  if (args.entity.type === 'CIRCLE' && args.entity.radius < 0) {
    args.cancel = true;
  }
});
```

MCP 参考：`vjcad/doc/event/event-usage.md`。

## 反例

- 在 `EntityAdded` 里读 PLINE 全部点位 → 只有最初 2 个点（详见 [pline-timing](pline-timing.md)）。
- 在 `EntityModified` 里再 `setModified()` → 触发新一轮事件，可能死循环。
- 订阅单数 `EntityErasing` 想阻止删除 → 永不触发；用 `EntitiesErasing`。即便用对了，取消后撤销栈仍留下一条删除记录（见第三节），建议在更早阶段（命令 `CommandStarting` 或 UI 层）拦截。

## 参考（官方文档 MCP）

- 文档：`vjcad/doc/event/event-types.md`、`vjcad/doc/event/event-usage.md`
- 示例：`vjcad/example/event/01entityadded.js`、`02entitymodified.js`
