命令 / 事件时序问题(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 终态
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);
});
或更可靠地通过实体事件:
events.on(CadEvents.EntityAdded, args => { // EntityAddedEventArgs: {document, entity, targetBlock}
if (args.entity.type !== 'PLINE') return;
// 实体级 .on('modified') 不存在——后续修改走全局 EntityModified,按 entity 过滤
});
想要只在"命令结束时"通知
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
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。
events.on(CadEvents.CommandEnded, args => {
// 无需让出一帧:命令的 undo 组此时已提交
Engine.undoManager.start_undoMark('补充修改'); // 方法名带下划线:start_undoMark / end_undoMark
// ...
Engine.undoManager.end_undoMark();
});
需要"在命令完成后立即追加修改"时直接开新 undo 组即可(注意它是独立的一步,用户要按两次 Ctrl+Z)。
5. 修改事件的链式响应
// 圆移动时同步移动文字:实体自身没有 .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 → 链式触发
});
两种应对:
// 方式 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. 指针事件与命令冲突
events.on(CadEvents.PointerDown, args => { /* 你想做的事 */ }); // 没有 LeftClick——用 PointerDown
当 某个命令在运行时(如 LINE),PointerDown 照常外发(画布 onPointerDown 先 emit 再走默认处理),你的监听会和命令拾取同时生效;且事件可取消——args.cancel = true 会拦截画布的默认处理。
只在空闲时处理:
events.on(CadEvents.PointerDown, args => {
if (Engine.editor.currentCommandName) return; // 有命令在跑就别处理
handleClick(args);
});
想在 vjcad 之前拿到原始指针事件,绑 DOM 的 pointerdown(捕获阶段),不要用 mousedown——画布在 pointerdown 里调了 preventDefault(),浏览器不再派发兼容的 mousedown:
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. 调试时序的"录像机"
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 之前处理用 DOMpointerdown捕获监听,mousedown会被 preventDefault 吞掉 - 子事件触发顺序未文档化的,靠"录像"调试比看 ts 类型靠谱
参考(官方文档 MCP)
- 文档:
vjcad/doc/event/event-types.md、vjcad/doc/event/event-usage.md - 相关:
event/command-events.md、event/pline-timing.md