# 命令 / 事件时序问题（PLINE / 命令钩子 / 事件顺序）

> 文档：`vjcad/doc/event/event-types.md`、`vjcad/doc/event/event-usage.md`
> 相关：`event/command-events.md`、`event/pline-timing.md`
> ⚠️ 已按 d.ts/运行时核对：事件名是 **`CommandEnded`**（不是 `CommandFinished`）；指针事件是 **`PointerDown/PointerMove/PointerUp`**（没有 `LeftClick`）；没有 `AnyEvent` 通配。

## 1. 经典症状

| 症状 | 可能根因 | 跳到 |
|---|---|---|
| 画 PLINE 时 `EntityModified` 触发了 N 次（每加一个点都触发） | PLINE 的中间状态也在 fire 事件 | §2 |
| `CommandEnded` 里读 `Engine.editor.currentCommandName`，是 `undefined` | 派发 `CommandEnded` 前已清空；用 `args.commandName` | §3 |
| `CommandEnded` 里读 `Engine.currentSpace.items` 数和预期对不上 | Esc 取消同样走 `CommandEnded`（无 `CommandCancelled`）；PLINE 第 2 点就已入图 | §2 §3 §4 |
| 在 `EntityAdded` 里改另一实体立即又触发 `EntityModified` | 链式响应 | §5 |
| 全局点击与命令系统冲突 | 命令运行时 `PointerDown` 照常外发，你的监听与命令拾取同时生效 | §6 |

## 2. PLINE 多点事件

`PolylineEnt` 是"在命令运行期间逐点构建的"实体：**拾取第 2 点时即加入文档**（触发一次 `EntityAdded`，此时仅 2 个顶点），之后每加一点（`addVertex` + `regenPartial`）触发 `EntityModified`；命令结束时**不再**触发 `EntityAdded`。所以 `EntityAdded` 里拿到的顶点数不是最终值，过程中反复 fire 的 `EntityModified` 也容易让人误以为有多个实体。

### 正确监听 PLINE 终态

```js
events.on(CadEvents.CommandEnded, args => {          // CommandEndedEventArgs: {commandName, success, duration}
    if (args.commandName !== 'PLINE' || !args.success) return;
    const ents = Engine.getEntitiesByType('PLINE');   // PolylineEnt.type === 'PLINE'（勿用 'LWPOLYLINE'，那是 EntityTypeFilter 词表/历史序列化值）
    console.log('当前 polyline 数:', ents.length);
});
```

或更可靠地通过实体事件：

```js
events.on(CadEvents.EntityAdded, args => {           // EntityAddedEventArgs: {document, entity, targetBlock}
    if (args.entity.type !== 'PLINE') return;
    // 实体级 .on('modified') 不存在——后续修改走全局 EntityModified，按 entity 过滤
});
```

### 想要只在"命令结束时"通知

```js
let pendingPline = null;
events.on(CadEvents.EntityAdded, args => {
    if (args.entity.type === 'PLINE') pendingPline = args.entity;
});
events.on(CadEvents.CommandEnded, args => {
    if (args.commandName === 'PLINE' && pendingPline) {
        console.log('PLINE 完成, 顶点数:', pendingPline.bulgePoints.length);   // length 是 getter（BulgePoints 还有 items: BulgePoint[]），无 .points 字段
        pendingPline = null;
    }
});
```

## 3. 命令事件的 args

```js
events.on(CadEvents.CommandStarted, args => {
    console.log(args.commandName);   // ✅ 'LINE'
    // ✅ 此时 Engine.editor.currentCommandName 已 = 'LINE'（先赋值再 emit）
});
events.on(CadEvents.CommandEnded, args => {
    console.log(args.commandName, args.success, args.duration);   // 字段是 success/duration，不是 canceled
    // ⚠️ 此时 currentCommandName 已清空为 undefined，只能用 args.commandName
});
// ⚠️ CadEvents.CommandCancelled 只是枚举值，当前版本（vjcad@1.0.16）从不派发；
// Esc 取消同样触发 CommandEnded，success 通常仍为 true（只有 main() 抛异常才为 false）
```

事件 args 是 vjcad 给你的"那一刻的快照"；`CommandStarted` 时 `currentCommandName` 已就位，`CommandEnded` 时已清空——统一用 `args.commandName` 最稳。

## 4. `CommandEnded` 时 undo 组已闭合

vjcad 的 undo 体系是 **一段命令一个 mark**。命令的撤销记录在 `main()` 内部就已完成（如 LINE 在 `main()` 末尾 `end_undoMark()` 闭合撤销组，PLINE 在第 2 点时 `added_undoMark`）；`CommandEnded` 要等 `await cmd.main()` 返回、再 `delay(15)` 之后才派发。顺序：`CommandStarting`（可 `cancel`）→ `CommandStarted` → `EntityAdded/Modified…` → `end_undoMark`（微任务派发 `UndoStackChanged`）→ 约 15ms 后 `CommandEnded`。

```js
events.on(CadEvents.CommandEnded, args => {
    // 无需让出一帧：命令的 undo 组此时已提交
    Engine.undoManager.start_undoMark('补充修改');      // 方法名带下划线：start_undoMark / end_undoMark
    // ...
    Engine.undoManager.end_undoMark();
});
```

需要"在命令完成后立即追加修改"时直接开新 undo 组即可（注意它是独立的一步，用户要按两次 Ctrl+Z）。

## 5. 修改事件的链式响应

```js
// 圆移动时同步移动文字：实体自身没有 .on()，用全局 EntityModified 按对象过滤
events.on(CadEvents.EntityModified, args => {
    const e = args.entity;
    if (e === circle) { text.insertionPoint = e.center; text.setModified(); }   // TextEnt 用 insertionPoint，无 center 字段
    // ⚠️ setModified() 会再次 fire EntityModified → 链式触发
});
```

两种应对：

```js
// 方式 1：标志位防递归
let inSync = false;
events.on(CadEvents.EntityModified, args => {
    if (inSync) return;
    inSync = true;
    try { /* update */ } finally { inSync = false; }
});

// 方式 2：用 Reactor 系统的 owner 关联（registerReactor 吃 IEntityReactor 实体，不是 {owner,target,onChange} 配置对象）
// 见 reactor/ireactor-interface.md —— 需要写一个继承 CustomEntityBase 的 reactor 实体
```

`EntityReactorManager` 内部已有"防循环"逻辑；用专用 API 比手写监听省心。

## 6. 指针事件与命令冲突

```js
events.on(CadEvents.PointerDown, args => { /* 你想做的事 */ });   // 没有 LeftClick——用 PointerDown
```

当 **某个命令在运行时**（如 LINE），`PointerDown` **照常外发**（画布 `onPointerDown` 先 emit 再走默认处理），你的监听会和命令拾取同时生效；且事件可取消——`args.cancel = true` 会拦截画布的默认处理。

只在空闲时处理：

```js
events.on(CadEvents.PointerDown, args => {
    if (Engine.editor.currentCommandName) return;   // 有命令在跑就别处理
    handleClick(args);
});
```

想在 vjcad 之前拿到原始指针事件，绑 DOM 的 `pointerdown`（捕获阶段），**不要用 `mousedown`**——画布在 pointerdown 里调了 `preventDefault()`，浏览器不再派发兼容的 `mousedown`：

```js
const container = document.getElementById('map');
container.addEventListener('pointerdown', e => {
    // 捕获阶段先于画布处理
}, true);
```

## 7. 事件触发顺序（典型）

```
0. CommandStarting          ← 可 args.cancel = true 阻止命令启动
1. CommandStarted           ← LINE 命令启动（currentCommandName 已赋值）
2. (命令内 await getPoint)  ← 用户拾取点
3. EntityAdded             ← 实体加入文档
4. SelectionChanged        ← 选择集变化（可能）
5. UndoStackChanged        ← main() 内 end_undoMark 后微任务派发
6. CommandEnded            ← main() 返回 + 约 15ms 后（Esc 取消也是它；CommandCancelled 从不派发）
```

把上面理顺后大部分时序问题都不再是问题。

## 8. 调试时序的"录像机"

```js
const events = CadEventManager.getInstance();
const log = [];
const start = performance.now();
const types = [                                            // 全部用真实事件名，无 AnyEvent 通配
    'CommandStarted','CommandEnded','EntityAdded','EntitiesErasing', // CommandCancelled / 单数 EntityErasing 从不派发，不必录
    'EntityModified','SelectionChanged','PointerDown'
];
const handlers = {};
types.forEach(t => {
    handlers[t] = args => log.push([
        Math.round(performance.now() - start), t,
        args.commandName ?? args.entity?.type ?? args.entities?.length
    ]);
    events.on(CadEvents[t], handlers[t]);
});
window.dumpLog = () => console.table(log);
window.stopLog = () => types.forEach(t => events.off(CadEvents[t], handlers[t]));
```

之后任何时候 `dumpLog()` 看时序 / 调用次数，`stopLog()` 解除监听。

## 红旗

- 在 `EntityAdded / EntityModified` 里 **不要再触发会改实体的操作**，除非有防递归
- 用 `args.commandName` / `args.entity.type` 判断而不是从全局 Engine 读
- `EntityModified` 触发 N 次 → 通常是 PLINE 中间态 / 多次 `addEntities` / 你重复绑了监听
- `CommandEnded` 时命令的 undo 组已闭合，读数据、追加新 undo 都无需让出一帧
- 全局 `events.on(PointerDown)` 在命令运行时照常触发（可 `args.cancel = true` 拦截默认处理）；要抢在 vjcad 之前处理用 DOM `pointerdown` 捕获监听，`mousedown` 会被 preventDefault 吞掉
- 子事件触发顺序未文档化的，靠"录像"调试比看 ts 类型靠谱

## 参考（官方文档 MCP）

- 文档：`vjcad/doc/event/event-types.md`、`vjcad/doc/event/event-usage.md`
- 相关：`event/command-events.md`、`event/pline-timing.md`
