# 输入与事件系统

用户输入交互和事件监听。

## 输入函数

### getPoint - 获取点

```javascript
const { PointInputOptions, InputStatusEnum, getPoint, Point2D } = vjcad;

// 基本用法
const options = new PointInputOptions("指定点位置:");
const result = await getPoint(options);

if (result.status === InputStatusEnum.OK) {
    const point = result.value;  // Point2D {x, y}
    console.log(`点: (${point.x}, ${point.y})`);
}
```

**带橡皮筋线**：

```javascript
const opt = new PointInputOptions("指定终点:");
opt.useBasePoint = true;      // 启用橡皮筋线
opt.basePoint = startPoint;   // 设置基点
const result = await getPoint(opt);
```

**隐藏默认橡皮筋线，使用自定义预览**：

```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();
    preview.color = 1;
    Engine.clearPreview();
    Engine.drawPreviewEntities([preview]);
};

const result = await getPoint(opt);
Engine.clearPreview();
```

**带关键字选项**：

```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();
        if (keyword === "L") { /* 处理直线 */ }
        else if (keyword === "C") { /* 处理圆 */ }
        else if (keyword === "E") { /* 退出 */ }
        break;
    case InputStatusEnum.Cancel:
        // 用户取消（ESC）
        break;
}
```

---

### getSelections - 获取选择集

```javascript
const { SelectionInputOptions, InputStatusEnum, getSelections } = vjcad;

const options = new SelectionInputOptions();   // 无参构造：提示语不在这里传
const result = await getSelections(options);

if (result.status === InputStatusEnum.OK) {
    const entities = result.value;  // 实体数组
    console.log(`选中了 ${entities.length} 个实体`);
    
    entities.forEach((ent, i) => {
        console.log(`${i + 1}. ${ent.type} (ID: ${ent.id}, 颜色: ${ent.color})`);
    });
}
```

**特点**：
- 支持框选和点选
- 用户按回车确认选择
- 返回实体数组

---

### getReal - 获取实数

```javascript
const { RealInputOptions, InputStatusEnum, getReal } = vjcad;

const options = new RealInputOptions("输入圆的半径:");
options.defaultValue = 30;  // 默认值（构造时默认就是 0）

const result = await getReal(options);

if (result.status === InputStatusEnum.OK) {
    // 直接回车/空格时 getReal 返回 OK + defaultValue（getPoint/getInteger/getString 则返回 None）
    const radius = result.value;
} else if (result.status === InputStatusEnum.None) {
    // 仅当 defaultValue 为 null/undefined 时，直接回车才返回 None
}
```

---

### getInteger - 获取整数

```javascript
const { IntegerInputOptions, InputStatusEnum, getInteger } = vjcad;

const options = new IntegerInputOptions("输入多边形边数:");
options.defaultValue = 6;   // getInteger 不会自动套用 defaultValue

const result = await getInteger(options);

if (result.status === InputStatusEnum.OK) {
    const sides = result.value;
} else if (result.status === InputStatusEnum.None) {
    // 直接回车/空格（或输入非数字）返回 None，需自己回退到默认值
    const sides = options.defaultValue;
}
```

---

### getString - 获取字符串

```javascript
const { StringInputOptions, InputStatusEnum, getString } = vjcad;

const options = new StringInputOptions("输入图层名称:");
// StringInputOptions 没有 defaultValue 字段（只有 keywords/allowEmpty/defaultKeyWord）

const result = await getString(options);

if (result.status === InputStatusEnum.OK) {
    const layerName = result.value;
} else if (result.status === InputStatusEnum.None) {
    // 直接回车/空格（空输入），默认值自己兜底
    const layerName = "新图层";
}
```

**注意**：getString 中空格也会结束输入（与回车相同），因此输入值不能含空格。

---

## InputStatusEnum 状态

| 状态 | 说明 | 常见场景 |
|------|------|----------|
| `OK` | 成功获取输入 | 用户点击/输入有效值；直接回车时 getReal 返回 OK + defaultValue、getKeyword（设了 defaultKeyword）返回 OK + 默认关键字 |
| `Cancel` | 用户取消 | 按 ESC |
| `None` | 直接回车/空格（空输入） | getPoint / getInteger / getString（getInteger 输入非数字也返回 None） |
| `EnterOrSpace` | 回车/空格 | **仅 getEntity** 返回 |
| `Keyword` | 用户输入关键字 | 带 keywords 选项时（getPoint 设了 defaultKeyword 时空回车返回该关键字） |

---

## 事件系统

### CadEventManager 基本用法

```javascript
const { CadEventManager, CadEvents, Engine } = vjcad;

// 获取事件管理器
const events = Engine.eventManager;
// 或: const events = CadEventManager.getInstance();

// 注册监听器
events.on(CadEvents.EntityAdded, (args) => {
    console.log('实体已添加:', args.entity.type);
});

// 链式调用
events
    .on(CadEvents.EntityAdded, handler1)
    .on(CadEvents.EntityModified, handler2);

// 一次性监听器（DocumentOpened 在 vjcad@1.0.16 从不派发，打开/切换文档用 DocumentSwitched）
events.once(CadEvents.DocumentSwitched, (args) => {
    console.log('当前文档:', args.currentDocument.name);   // 另有 args.previousDocument（可能为 null）
});
```

### 取消订阅

```javascript
// 保存处理函数引用，用 off 精确移除
const handler = (args) => { console.log(args); };
events.on(CadEvents.EntityAdded, handler);
events.off(CadEvents.EntityAdded, handler);
```

⚠️ **不要用 `offAll()` / `offAll(event)` 做清理**：`Engine.eventManager` 是全局单例，内核自己也在上面订阅（标注关联反应器桥 `ReactorEventBridge` 订阅 EntityAdded/EntityModified、RibbonBar 订阅 Plugin*/RibbonConfigChanged、MainView 订阅 DocumentCreated），且只订阅一次——`offAll` 会把它们一并移除，关联标注、Ribbon 刷新等从此失效。只用 `off(event, handler)`。

### 事件管理器方法

| 方法 | 说明 |
|------|------|
| `on(event, handler)` | 注册监听器 |
| `once(event, handler)` | 注册一次性监听器 |
| `off(event, handler)` | 移除指定监听器（清理只用它） |
| `offAll(event?)` | 移除所有监听器——含内核内部监听，**勿用** |
| `hasListeners(event)` | 检查是否有监听器 |
| `listenerCount(event)` | 获取监听器数量 |
| `suspendEvents(queueEvents = false)` | 暂停事件触发（默认丢弃期间的事件；传 true 则排队，resume 时补发） |
| `resumeEvents()` | 恢复事件触发 |

---

## 常用事件类型

### 实体事件

```javascript
// 实体添加
events.on(CadEvents.EntityAdded, (args) => {
    console.log('实体:', args.entity);
    console.log('文档:', args.document);
});

// 批量添加——只由 Engine.currentSpace.addEntities([...]) 派发（期间逐个 EntityAdded 被抑制）；
// Engine.addEntities 走逐个 pcanvas.addEntity，只派发逐个 EntityAdding/EntityAdded
events.on(CadEvents.EntitiesAdded, (args) => {
    console.log('添加数量:', args.entities.length);
});

// 实体修改
events.on(CadEvents.EntityModified, (args) => {
    console.log('修改的实体:', args.entity);
});

// 实体删除
events.on(CadEvents.EntitiesErased, (args) => {
    args.entities.forEach(ent => {
        console.log('已删除:', ent.type);
    });
});
```

### 选择事件

```javascript
events.on(CadEvents.SelectionChanged, (args) => {
    const selected = Engine.ssGetFirst();
    console.log('当前选中数量:', selected.length);
});
```

### 命令事件

```javascript
events.on(CadEvents.CommandStarted, (args) => {
    console.log('命令开始:', args.commandName);
});

// Esc 取消的命令同样触发 CommandEnded（CommandCancelled 在 vjcad@1.0.16 从不派发）；
// success 只表示 main() 没抛异常，duration 为耗时毫秒
events.on(CadEvents.CommandEnded, (args) => {
    console.log('命令结束:', args.commandName, args.success, args.duration);
});
```

### 文档事件

```javascript
// 打开/新建/切换文档后都会触发（DocumentOpened/DocumentSaved 等在 vjcad@1.0.16 从不派发）
events.on(CadEvents.DocumentSwitched, (args) => {
    console.log('当前文档:', args.currentDocument.name, '之前:', args.previousDocument?.name);
});

// 只在文档从"未修改"变为"已修改"时触发一次；保存清除修改标记前不会再触发
events.on(CadEvents.DocumentModified, (args) => {
    console.log('文档已修改:', args.document.name);
});
```

### 图层事件

```javascript
events.on(CadEvents.LayerAdded, (args) => {
    console.log('新图层:', args.layer.name);
});

events.on(CadEvents.CurrentLayerChanged, (args) => {
    // 参数是图层**名**字符串，不是 Layer 对象：previousLayerName / currentLayerName
    console.log('当前图层:', args.currentLayerName);
});
```

---

## 可取消事件

设 `args.cancel = true` 即取消（只对下方列表里真正派发且检查 cancel 的事件有效）。

```javascript
// 阻止删除保护图层上的实体（EntityErasing 从不派发，用批量的 EntitiesErasing）
events.on(CadEvents.EntitiesErasing, (args) => {
    const protectedEnts = args.entities.filter(ent => ent.layer === "保护图层");
    if (protectedEnts.length === args.entities.length) {
        args.cancel = true;                       // 整批取消
        args.cancelReason = "保护图层上的实体不允许删除";
    } else if (protectedEnts.length > 0) {
        args.excludeEntities = protectedEnts;     // 只把这些排除，其余照删
    }
});

// 阻止命令执行
events.on(CadEvents.CommandStarting, (args) => {
    if (args.commandName === 'ERASE' && !hasPermission()) {
        args.cancel = true;
        args.cancelReason = "没有删除权限";
    }
});
```

**可取消事件列表**（vjcad@1.0.16 实际派发并检查 `cancel` 的）：
- `DocumentCreating`（取消后 `Engine.view.newDocument()` / `openDbDoc` 不判空会直接抛错，慎用）
- `EntityAdding` / `EntitiesAdding` / `EntitiesErasing`（后者另支持 `args.excludeEntities` 部分排除）
- `CommandStarting`
- `LayerAdding` / `LayerDeleting`
- `ContextMenuOpening`

`DocumentOpening` / `DocumentClosing` / `DocumentSaving` / `EntityErasing` / `BlockAdding` / `BlockDeleting` 虽在 `CadEvents` 枚举里，但**从不派发**，监听它们取消不了任何操作。

---

## 事件类型完整列表

⚠️ 标 **✗** 的事件只存在于 `CadEvents` 枚举，vjcad@1.0.16 **从不派发**（监听永远不触发）。

### 文档事件
| 事件 | 说明 |
|------|------|
| `DocumentCreating` / `DocumentCreated` | 新建文档 |
| `DocumentOpening` / `DocumentOpened` | ✗ 打开文档——改用 `DocumentSwitched` |
| `DocumentClosing` / `DocumentClosed` | ✗ 关闭文档 |
| `DocumentSaving` / `DocumentSaved` | ✗ 保存文档 |
| `DocumentModified` | 文档由未修改变为已修改（仅首次） |
| `DocumentSwitched` | 打开/新建/切换文档（`previousDocument` / `currentDocument`） |

### 实体事件
| 事件 | 说明 |
|------|------|
| `EntityAdding` / `EntityAdded` | 添加实体（`Engine.addEntities` 逐个派发） |
| `EntitiesAdding` / `EntitiesAdded` | 批量添加（仅 `currentSpace.addEntities`） |
| `EntityModified` | 实体修改 |
| `EntityErasing` / `EntityErased` | ✗ 删除实体——删除只派发批量版 |
| `EntitiesErasing` / `EntitiesErased` | 删除实体（单个删除也走这对） |

### 命令事件
| 事件 | 说明 |
|------|------|
| `CommandStarting` / `CommandStarted` | 命令开始 |
| `CommandEnded` | 命令结束（Esc 取消也触发） |
| `CommandCancelled` | ✗ 命令取消——看 `CommandEnded` |

### 选择事件
| 事件 | 说明 |
|------|------|
| `SelectionChanged` | 选择集变化 |
| `SelectionCleared` | 选择集清空 |

### 图层事件
| 事件 | 说明 |
|------|------|
| `LayerAdding` / `LayerAdded` | 添加图层 |
| `LayerDeleting` / `LayerDeleted` | 删除图层 |
| `LayerModified` | ✗ 图层修改 |
| `CurrentLayerChanged` | 当前图层切换 |

### 视图事件
| 事件 | 说明 |
|------|------|
| `ViewZoomed` | 视图缩放 |
| `ViewPanned` | ✗ 视图平移 |
| `ViewRegenerated` | 视图重绘 |

### 块事件
| 事件 | 说明 |
|------|------|
| `BlockAdding` / `BlockAdded` | ✗ 添加块定义 |
| `BlockDeleting` / `BlockDeleted` | ✗ 删除块定义 |
| `BlockModified` | ✗ 块定义修改 |
