# 右键菜单

> 文档：`vjcad/doc/ui/contextmenu.md`
> 示例：
> - `vjcad/example/ui/06contextmenu.js` — 加自定义项
> - `vjcad/example/ui/06contextmenudisable.js` — 完全禁用
> - `vjcad/example/ui/06contextmenucommand.js` — 按命令状态定制
> - `vjcad/example/ui/06contextmenucustom.js` — 完全替换默认项

## 1. 入口：监听 ContextMenuOpening

vjcad 不提供"硬注册"右键项的 API，而是统一通过事件 `ContextMenuOpening` 在打开前修改菜单。

```js
const events = CadEventManager.getInstance();

events.on(CadEvents.ContextMenuOpening, args => {
    // args.menuItems     默认菜单项数组（直接修改也可以）
    // args.prependItems  追加在最前
    // args.appendItems   追加在最后
    // args.useDefaultItems  是否显示默认项（默认 true）
    // args.cancel        是否取消显示菜单
});
```

事件参数（关键字段）：

| 字段 | 含义 |
|---|---|
| `screenX / screenY` | 屏幕坐标 |
| `canvasX / canvasY` | 画布坐标 |
| `isCommandActive` | 当前是否有命令在执行 |
| `activeCommandName` | 当前命令名（如 `LINE`） |
| `useDefaultItems` | 控制是否显示默认菜单项 |
| `cancel` | 设为 `true` 完全不弹 |

## 2. 菜单项配置

```ts
interface ContextMenuItemInfo {
    label?: string;                       // 文本（分隔线时可省）
    icon?: string;                        // 图标名，不带 .svg；只在 IconRegistry 的 Commands 分类里查
    shortcut?: string;                    // ⚠️ 类型里有，但渲染时被忽略
    command?: string;                     // 直接执行 vjcad 命令
    callback?: () => void;                // 自定义点击回调
    submenu?: ContextMenuItemInfo[];      // 二级菜单
    disabled?: boolean;                   // ⚠️ 被忽略（不会置灰，照样可点）
    checked?: boolean;                    // ⚠️ 被忽略（不显示勾选）
    isSeparator?: boolean;                // 不是真分隔线：渲染成一行文字 "---"（可点击，点了等于执行不存在的命令 "---"，静默无效）
}
```

`command` 与 `callback` 二选一：填了 `command`，点击会自动 `Engine.editor.executerWithOp(command)`；有 `callback` 时优先调 `callback`。转换成菜单行时只读取 `label / command / icon / submenu / callback / isSeparator`——要"禁用"某项请干脆不 push，或在 `callback` 里自己判断后 return。

`icon` 会被拼成 `<icon>.svg` 再剥掉扩展名，按小写在 `IconCategory.Commands` 里查找：写 `'tool.svg'` 会查 `'tool.svg'` 而落空；自定义图标要用 `IconRegistry.registerCommandIcon(name, svg)`（或插件 `context.registerIcon`）注册到 Commands 分类。

## 3. 四种典型用法

### 3.1 在默认菜单前后追加

```js
events.on(CadEvents.ContextMenuOpening, args => {
    // 前置：含子菜单
    args.prependItems.push({
        label: '我的工具', icon: 'tool',   // 内置 Commands 分类没有 'tool'，不先注册就不显示图标
        submenu: [
            { label: '画圆', callback: () => {
                const c = new CircleEnt([50, 50], 15); c.setDefaults();
                Engine.addEntities(c);
            } },
            { label: '画线', callback: () => {
                const l = new LineEnt([0, 0], [100, 100]); l.setDefaults();
                Engine.addEntities(l);
            } }
        ]
    });
    args.prependItems.push({ isSeparator: true });

    // 后置：分隔 + 缩放全图
    args.appendItems.push({ isSeparator: true });
    args.appendItems.push({
        label: '缩放全图  Z+E',             // shortcut 字段不渲染，要提示就写进 label
        callback: () => Engine.zoomExtents()
    });
});
```

### 3.2 完全禁用系统菜单

```js
events.on(CadEvents.ContextMenuOpening, args => {
    args.cancel = true;
});
```

### 3.3 按命令状态定制

```js
events.on(CadEvents.ContextMenuOpening, args => {
    if (args.isCommandActive) {
        if (args.activeCommandName === 'LINE') {
            args.appendItems.push({ label: '闭合', callback: () => { /* close */ } });
        }
    } else {
        args.prependItems.push({
            label: '快捷命令',
            submenu: [
                { label: '画线', command: 'LINE' },
                { label: '画圆', command: 'CIRCLE' }
            ]
        });
    }
});
```

`isCommandActive` 让你区分"空闲右键"和"命令进行中右键"。在命令中右键通常用于补充选项（闭合 / 撤销最后一步 / 取消）。

### 3.4 完全替换默认项

```js
events.on(CadEvents.ContextMenuOpening, args => {
    args.useDefaultItems = false;
    args.prependItems.push({ label: '自定义项 1', callback: () => {} });
    args.prependItems.push({ label: '自定义项 2', callback: () => {} });
    args.prependItems.push({ isSeparator: true });
    args.prependItems.push({ label: '缩放全图',  callback: () => Engine.zoomExtents() });
});
```

## 4. 相关事件

| 事件 | 说明 |
|---|---|
| `ContextMenuOpening` | 打开前（可改、可取消） |
| `ContextMenuOpened` | 已打开（仅通知） |
| ~~`ContextMenuItemClicked`~~ | 枚举里有，但**从不派发**（源码无 emit 点）；要感知点击请在各项 `callback` 里处理 |

## 5. 实战模板：基于选中实体显示

```js
events.on(CadEvents.ContextMenuOpening, args => {
    const sel = ssGetFirst() ?? [];
    if (sel.length === 0) return;            // 没选中就不动菜单

    args.prependItems.push({ isSeparator: true });
    args.prependItems.push({
        label: `属性 (${sel.length})`,
        callback: () => Engine.editor.executerWithOp('PROPERTIES')
    });
    if (sel.every(e => e.type === 'LINE')) {
        args.prependItems.push({
            label: '直线长度求和',
            callback: () => {
                const total = sel.reduce((s, e) => s + e.Length, 0);
                message.info(`总长 ${total.toFixed(2)}`);
            }
        });
    }
});
```

## 红旗

- 监听 `ContextMenuOpening` 不会自动解绑；不需要时记得 `events.off`，否则插件多次激活会重复添加
- `args.menuItems` 是 **默认菜单项**，直接 `splice` / `push` 也能改但不推荐；优先用 `prependItems / appendItems / useDefaultItems`
- `command` 字段走 `executerWithOp`，它会**先自动 ESC 掉当前命令**（`awaitDispatchEscapeEvent()`：派发 Escape 并等命令结束）再执行新命令，不会冲突；`callback` 里要手动取消当前命令可 `await Engine.editor.awaitDispatchEscapeEvent()`。默认菜单里的"取消"项即 `ContextMenuAction.CANCEL_INPUT`（等效 ESC）
- 子菜单 `submenu` 内的项不能再嵌套 `submenu`（仅支持 1 级）
- 画布上的浏览器原生右键菜单**始终被屏蔽**（`contextmenu` 回调第一步就 `preventDefault()`），`cancel = true` 只是不弹 vjcad 菜单，不存在"fallback 到浏览器原生菜单"
- `MainView` 配置 `enableContextMenu: false`（或 viewer 模式未显式开启）时右键菜单被整体卸掉，**`ContextMenuOpening` 根本不会触发**，监听写了也不生效

## 参考（官方文档 MCP）

- 文档：`vjcad/doc/ui/contextmenu.md`
- 示例：
- `vjcad/example/ui/06contextmenu.js` — 加自定义项
- `vjcad/example/ui/06contextmenudisable.js` — 完全禁用
- `vjcad/example/ui/06contextmenucommand.js` — 按命令状态定制
- `vjcad/example/ui/06contextmenucustom.js` — 完全替换默认项
