# 命令系统与用户输入

自定义命令的创建与注册、用户输入交互、事件系统。

---

## 基础命令定义

```javascript
const {
    CommandDefinition, CommandRegistry, CommandOptions,
    writeMessage, Engine
} = vjcad;

class HelloCommand {
    async main() {
        writeMessage("<br/>Hello WebCAD!");
        Engine.zoomExtents();
    }
}

const options = new CommandOptions();
options.useAutoComplete = true;

const cmdDef = new CommandDefinition(
    'HELLO',             // 命令名（大写）
    '简单示例命令',       // 描述
    HelloCommand,        // 命令类
    options              // 选项（可选）
);

CommandRegistry.regist(cmdDef);

// 执行命令
await Engine.editor.executerWithOp('HELLO');
```

---

## 命令类标准模板

```javascript
class MyCommand {
    constructor() {
        // 初始化状态
    }

    async main() {
        Engine.undoManager.start_undoMark();
        try {
            await this.doWork();
        } catch (error) {
            writeMessage(`<br/>错误: ${error.message}`);
        } finally {
            Engine.undoManager.end_undoMark();
            Engine.clearPreview();
        }
    }

    async doWork() {
        // 实际命令逻辑
    }
}
```

---

## 命令注册与管理

```javascript
// 批量注册
const commands = [
    { name: 'CMD1', desc: '命令1', cls: Cmd1Class },
    { name: 'CMD2', desc: '命令2', cls: Cmd2Class },
];
commands.forEach(cmd => {
    CommandRegistry.regist(
        new CommandDefinition(cmd.name, cmd.desc, cmd.cls)
    );
});

// 查询
const cmdInfo = CommandRegistry.item('CMD1');

// 注销
CommandRegistry.unregist('CMD1');
```

---

## 用户输入函数

### getPoint — 获取点

```javascript
const { PointInputOptions, InputStatusEnum, getPoint } = vjcad;

const options = new PointInputOptions("指定点位置:");
const result = await getPoint(options);

if (result.status === InputStatusEnum.OK) {
    const point = result.value;  // Point2D { x, y }
}
```

**带橡皮筋线**：

```javascript
const opt = new PointInputOptions("指定终点:");
opt.useBasePoint = true;
opt.basePoint = startPoint;
const result = await getPoint(opt);
```

**带关键字选项**：

```javascript
const opt = new PointInputOptions("指定点 [直线(L)/圆(C)/退出(E)]:");
opt.keywords = ["L", "C", "E"];

const result = await getPoint(opt);
switch (result.status) {
    case InputStatusEnum.OK:
        const point = result.value;     // 用户点击了坐标点
        break;
    case InputStatusEnum.Keyword:
        const keyword = result.stringResult.toUpperCase();
        break;
    case InputStatusEnum.Cancel:        // ESC（不抛异常）
        break;
    case InputStatusEnum.None:          // 空输入回车/空格（getPoint 不返回 EnterOrSpace）
        break;
}
```

**自定义预览**：

```javascript
const opt = new PointInputOptions("指定终点:");
opt.useBasePoint = true;
opt.basePoint = startPoint;
opt.hideRubberBand = true;

opt.callback = (canvasPt) => {
    const worldPt = Engine.canvasToWcs(canvasPt);
    const preview = new LineEnt([startPoint.x, startPoint.y], [worldPt.x, worldPt.y]);
    preview.setDefaults();
    Engine.clearPreview();
    Engine.drawPreviewEntities([preview]);
};

const result = await getPoint(opt);
Engine.clearPreview();
```

### getSelections — 获取选择集

```javascript
const { SelectionInputOptions, InputStatusEnum, getSelections } = vjcad;

const options = new SelectionInputOptions();   // 无参构造：该类只有 useOsnap 等字段，没有提示语参数
const result = await getSelections(options);

if (result.status === InputStatusEnum.OK) {
    const entities = result.value;  // 实体数组
}
```

### getReal — 获取实数

```javascript
const { RealInputOptions, InputStatusEnum, getReal } = vjcad;

const options = new RealInputOptions("输入半径:");
options.defaultValue = 30;
const result = await getReal(options);

if (result.status === InputStatusEnum.OK) {
    const radius = result.value;
}
```

### getInteger — 获取整数

```javascript
const { IntegerInputOptions, InputStatusEnum, getInteger } = vjcad;

const options = new IntegerInputOptions("输入边数:");
options.defaultValue = 6;
const result = await getInteger(options);
```

### getString — 获取字符串

```javascript
const { StringInputOptions, InputStatusEnum, getString } = vjcad;

const options = new StringInputOptions("输入图层名称:");
// StringInputOptions 无 defaultValue（字段只有 message/keywords/allowEmpty/defaultKeyWord）
const result = await getString(options);
// 空输入回车返回 status === InputStatusEnum.None，默认值自己兜底；注意空格也会结束输入
const layerName = result.status === InputStatusEnum.OK ? result.value : "新图层";
```

### getCorner — 获取矩形对角点

```javascript
const { CornerInputOptions, getCorner } = vjcad;

const options = new CornerInputOptions("指定对角点:");
options.basePoint = startPoint;
const result = await getCorner(options);
```

---

## InputStatusEnum 状态

| 状态 | 说明 |
|------|------|
| `OK` | 成功获取输入 |
| `Cancel` | 用户按 ESC（输入函数返回该状态，不抛异常） |
| `None` | 空输入时按回车/空格（`getPoint`/`getString`/无默认值的 `getReal` 等；`getPoint` 设了 `defaultKeyword` 时改返回 `Keyword`） |
| `EnterOrSpace` | 回车/空格——**只由 `getEntity` 返回**，`getPoint` 判"完成"要用 `None` |
| `Keyword` | 用户输入关键字 |

---

## 状态机命令模式

处理复杂的多步骤命令：

```javascript
const {
    PointInputOptions, InputStatusEnum,
    getPoint, PolylineEnt, Engine, writeMessage
} = vjcad;

class DrawPolylineCommand {
    constructor() {
        this.step = 1;
        this.points = [];
        this.isClosed = false;
    }

    async main() {
        Engine.undoManager.start_undoMark();
        try {
            while (this.step > 0) {
                switch (this.step) {
                    case 1: await this.getFirstPoint(); break;
                    case 2: await this.getNextPoints(); break;
                    case 3: this.finish(); break;
                }
            }
        } finally {
            Engine.undoManager.end_undoMark();
            Engine.clearPreview();
        }
    }

    async getFirstPoint() {
        const opt = new PointInputOptions("指定起点:");
        const result = await getPoint(opt);
        if (result.status === InputStatusEnum.OK) {
            this.points.push(result.value);
            this.step = 2;
        } else {
            this.step = 0;
        }
    }

    async getNextPoints() {
        const opt = new PointInputOptions("指定下一点 [闭合(C)/撤销(U)] <完成>:");
        opt.keywords = ["C", "U"];
        if (this.points.length > 0) {
            opt.useBasePoint = true;
            opt.basePoint = this.points[this.points.length - 1];
        }
        const result = await getPoint(opt);
        if (result.status === InputStatusEnum.OK) {
            this.points.push(result.value);
        } else if (result.status === InputStatusEnum.Keyword) {
            const kw = result.stringResult.toUpperCase();
            if (kw === "C") { this.isClosed = true; this.step = 3; }
            else if (kw === "U" && this.points.length > 1) { this.points.pop(); }
        } else if (result.status === InputStatusEnum.None) {   // 空输入回车/空格 = 完成
            this.step = 3;
        } else {
            this.step = 0;
        }
    }

    finish() {
        if (this.points.length >= 2) {
            const pline = new PolylineEnt();
            pline.setPoints(this.points.map(p => [p.x, p.y]));
            pline.isClosed = this.isClosed;
            pline.setDefaults();
            Engine.addEntities(pline);
        }
        this.step = 0;
    }
}
```

---

## 事件系统

### 注册事件监听

```javascript
const { CadEventManager, CadEvents, Engine } = vjcad;

const events = Engine.eventManager;

events.on(CadEvents.EntityAdded, (args) => {
    console.log('实体已添加:', args.entity.type);
});

events.once(CadEvents.DocumentSwitched, (args) => {   // DocumentOpened 当前版本不派发
    console.log('切换到文档:', args.currentDocument.name);
});
```

### 取消订阅

```javascript
const handler = (args) => { console.log(args); };
events.on(CadEvents.EntityAdded, handler);
events.off(CadEvents.EntityAdded, handler);    // 移除指定监听器
events.offAll(CadEvents.EntityAdded);          // 移除该事件所有监听器
events.offAll();                               // 移除所有监听器
```

### 事件管理器方法

| 方法 | 说明 |
|------|------|
| `on(event, handler)` | 注册监听器 |
| `once(event, handler)` | 一次性监听器 |
| `off(event, handler)` | 移除指定监听器 |
| `offAll(event?)` | 移除所有监听器 |
| `hasListeners(event)` | 检查是否有监听器 |
| `suspendEvents()` | 暂停事件触发 |
| `resumeEvents()` | 恢复事件触发 |

### 常用事件

| 事件 | 说明 |
|------|------|
| `EntityAdded` / `EntitiesAdded` | 实体添加 |
| `EntityModified` | 实体修改 |
| `EntitiesErased` | 实体删除 |
| `SelectionChanged` | 选择集变化 |
| `CommandStarted` / `CommandEnded` | 命令开始/结束（ESC 取消也只发 `CommandEnded`） |
| `DocumentCreated` / `DocumentSwitched` / `DocumentModified` | 文档新建 / 切换 / 首次变脏 |
| `LayerAdded` / `CurrentLayerChanged` | 图层变化 |
| `ViewZoomed` / `ViewRegenerated` | 视图变化 |

> ⚠️ `DocumentOpening/Opened/Closing/Closed/Saving/Saved`、`EntityErasing/EntityErased`、`CommandCancelled`、`LayerModified`、`ViewPanned`、`Block*` 以及协作/缓存/分支/Patch/冲突类事件在当前版本**没有任何派发点**，订阅永不触发。全表见 [event/overview](../event/overview.md)。

### 可取消事件（以 -ing 结尾）

```javascript
// 删除只派发批量版 EntitiesErasing（单数 EntityErasing 不派发）
events.on(CadEvents.EntitiesErasing, (args) => {
    if (args.entities.some(e => e.layer === "保护图层")) {
        args.cancel = true;
        args.cancelReason = "保护图层上的实体不允许删除";
    }
});

// 阻止保存：DocumentSaving 不派发，改在命令层拦截
events.on(CadEvents.CommandStarting, (args) => {
    if (args.commandName === "QSAVE") args.cancel = true;
});
```

实际会派发的可取消事件：`DocumentCreating`、`EntityAdding`、`EntitiesAdding`、`EntitiesErasing`、`CommandStarting`、`LayerAdding`、`LayerDeleting`、`ContextMenuOpening`（`Plugin*ing` 的参数类型不带 `cancel`，不可取消）。（`DocumentOpening/Closing/Saving`、`EntityErasing`、`BlockAdding/Deleting` 虽在枚举中，但不派发。）
