> **本文回答**：`CommandRegistry.register` —— 写一条完整自定义命令：输入 / 预览 / 事务 / 选项。

## 一、命令类最小骨架

```js
class MyCmd {
  // ⚠️ 必须是实例方法：registry 内部 `new cmdClass()` 后调 `cmd.main(ctx, opts)`，
  // 写成 static 会得到 "cmd.main is not a function"（被 execute 的 try/catch 吞掉）
  async main(ctx, opts) {
    // ctx: CommandContext（含 store / inputManager / previewManager / ...）
    // opts: executeCommand(name, opts) 的第二个参数
  }
}
```

## 二、注册

```js
mapcadLayer.commandRegistry.register(
  'MY_CMD',         // 建议大写
  MyCmd,
  '我的命令'        // 可选描述
);

mapcadLayer.executeCommand('MY_CMD');
mapcadLayer.executeCommand('MY_CMD', { color: 0xff0000 });
```

## 三、CommandContext 速查

| 字段 | 用途 |
|---|---|
| `store` | `add(ent)` / `delete(id)` / `get(id)` / `getAll()` |
| `inputManager` | `getPoint` / `getEntity` / `getSelections` / `getReal` / ... |
| `previewManager` | `setPreviewEntities` / `clear` |
| `undoManager` | `beginTransaction` / `endTransaction` |
| `gripManager` | 编辑夹点 |
| `drawingDefaults` | 当前默认颜色 / 填充色 / 图层（无文字高字段，见 serialize.md） |
| `blocks` | 块定义集合 |
| `coordMapper` | 坐标转换（像素 / CAD） |
| `eventBus` | fire 业务事件 |
| `map` | vjmap.Map 实例 |

## 四、完整示例：画星形

```js
class DrawStarCmd {
  async main(ctx, opts = {}) {
    const { inputManager: im, previewManager: pm, store, undoManager: um, drawingDefaults: defaults } = ctx;

    const ptsCount = opts.points ?? 5;

    const cr = await im.getPoint({ message: '指定星形中心' });
    if (cr.status !== 'ok') return;
    const center = cr.value;   // 点结果在 .value，不是 .point

    const rr = await im.getPoint({
      message: '指定外径点',
      useBasePoint: true,      // 橡皮筋开关是 useBasePoint，不是 rubberBand
      basePoint: center,
      callback: p => {          // 光标移动回调字段名是 callback，不是 onMouseMove
        const ent = buildStar(center, center.distanceTo(p), ptsCount, defaults);
        pm.setPreviewEntities([ent]);
      }
    });
    pm.clear();                // PreviewManager 上是 clear()，不是 clearPreview()
    if (rr.status !== 'ok') return;

    um.beginTransaction();  // 画星形
    const star = buildStar(center, center.distanceTo(rr.value), ptsCount, defaults);
    store.add(star);
    um.endTransaction();
  }
}

function buildStar(center, r, n, defaults) {
  const pts = [];
  for (let i = 0; i < n * 2; i++) {
    const a = (Math.PI * i) / n - Math.PI / 2;
    const rr = i % 2 === 0 ? r : r * 0.5;
    pts.push([                              // Point2DLike：用 [x,y] 数组
      center.x + rr * Math.cos(a),
      center.y + rr * Math.sin(a)
    ]);
  }
  const poly = new vjmapext.PolylineEnt(pts, true);
  poly.color = defaults.color;
  poly.lineWidth = defaults.lineWidth;
  poly.layerId = defaults.layerId;
  return poly;
}

mapcadLayer.commandRegistry.register('STAR', DrawStarCmd, '画星形');

// 启动
mapcadLayer.executeCommand('STAR', { points: 6 });
```

要点：

- **预览**：`PointInputOptions.callback`（光标移动回调）+ `setPreviewEntities`
- **事务**：`beginTransaction` / `endTransaction` 让一次 Ctrl+Z 撤掉整个星形——注：`execute` 已对每条命令自动包事务，手动再包是无害嵌套；真正要注意的是**修改类**操作（scale/move 等）需先 `recordModify` 才入栈（`store.add/delete` 自动入栈，见 §六）
- **默认值**：读 `drawingDefaults` 填充颜色 / 线宽 / 图层
- **选项**：`opts.points` 让外部可配置边数

## 五、用户关键字支持

```js
const r = await im.getPoint({
  message: '指定终点 [闭合(C)/撤销(U)]',
  keywords: ['C', 'U']
});

// 关键字命中：status === 'keyword'，关键字文本在 r.stringResult
if (r.status === 'keyword') {
  const kw = (r.stringResult || '').toUpperCase();
  if (kw === 'C') closeShape();
  else if (kw === 'U') undoLast();
}
```

## 六、选集 + 变换

```js
class DoubleScaleCmd {
  async main(ctx) {
    const { inputManager: im, undoManager: um } = ctx;

    // 多选是 getSelections（复数）；EntityInputOptions 才有 filter
    const sel = await im.getSelections({ message: '选择要缩放为 2 倍的实体' });
    if (sel.status !== 'ok' || !sel.entities?.length) return;

    const base = await im.getPoint({ message: '指定基点' });
    if (base.status !== 'ok') return;

    um.beginTransaction();  // 2x 缩放；scale(center, factor)
    sel.entities.forEach(e => { um.recordModify(e); e.scale(base.value, 2); });  // 先快照
    um.endTransaction();
  }
}
mapcadLayer.commandRegistry.register('DBLSCALE', DoubleScaleCmd);
```

## 七、异步副作用（远程查询）

命令里可以 `await` 后端：

```js
class FetchAndPlaceCmd {
  async main(ctx, opts) {
    const data = await fetch(`/api/symbol/${opts.id}`).then(r => r.json());
    const pt = await ctx.inputManager.getPoint({ message: '放置点' });
    if (pt.status !== 'ok') return;

    // 构造：new BlockRefEnt(blockId?, insertionPoint?, scaleFactor?, rotation?)——第 1 参是 blockId 不是块名
    const def = ctx.blocks.itemByName(data.blockname);
    if (!def) return;
    const block = new vjmapext.BlockRefEnt(def.blockId, pt.value);
    block.setBlocksRef(ctx.blocks);   // store.add 不会自动注入 blocks 引用；也可改用 mapcadLayer.addEntity（见坑 3）
    ctx.store.add(block);
  }
}
```

内置 UI（`createUI()` 后）没有"执行中"状态提示：`command:started`→`command:ended` 期间只会高亮工具栏上同名命令按钮，命令行显示 `getPoint` 等的 `message`。`await fetch` 这类长耗时步骤要自己显示 loading。

## 八、命令选项（opts）

传进来任意对象：

```js
mapcadLayer.executeCommand('MY_CMD', {
  color: 0xff0000,
  width: 2,
  skipPrompt: true
});
```

`main(ctx, opts)` 里 `opts.skipPrompt` 可用于"跳过前几步直接开始"。

## 九、注销命令

```js
mapcadLayer.commandRegistry.unregister('MY_CMD');
// 或 register 同名会覆盖
```

## 十、常见坑

1. **忘 return on cancel**：用户 Esc 后下一步仍执行 → 崩溃。
2. **main 写成 `static` 或忘 `async`**：registry 是 `new cmdClass()` 后调实例 `cmd.main(ctx, opts)`——`static main` 会抛 `cmd.main is not a function` 且被 try/catch 吞掉（命令看似"没反应"）；忘 `async` 则 `await` 写不上去。
3. **store.add vs mapcadLayer.addEntity**：两者都只 emit 一次 `entity:added`（事件在 store.add 里发），不存在重复触发。区别是 `addEntity` 会先给 `type==="INSERT"` 的实体 `setBlocksRef(this.blocks)` 再委托 store.add——**BlockRefEnt 建议走 `addEntity`**，普通实体两者等价。
4. **捕获异常吃掉**：try/catch 内抛错不让用户看；用 eventBus 广播或 console。
5. **命令里直接操作 DOM**：耦合；建议 fire 事件让外部处理。

## 相关

- 命令概览 → [overview](overview.md)
- 执行入口 → [execute-command](execute-command.md)
- 预览 → [preview](preview.md)
- 快捷键 → [shortcuts](shortcuts.md)
