# 图标注册（IconRegistry）

> 文档：`vjcad/doc/ui/icons.md`
> 示例：`vjcad/example/ui/01ribbon.js`、命令 / 插件示例中频繁使用

## 1. 模型

vjcad 把所有图标做成"按 **类别 + 名字** 索引"的单例字典：

```ts
class IconRegistry {                        // 全部静态方法，无 getInstance
    static getIcon(key: string, category?: IconCategory): string | undefined;
    static registerIcon(key: string, svg: string, options?: IconRegistryOptions): boolean;
    static registerIcons(icons: Record<string, string>, category: IconCategory): void;
    static registerCommandIcon(commandName: string, svg: string, override?: boolean): boolean;
    static getCommandIcon(commandName: string): string | undefined;
    static hasIcon(key: string, category?: IconCategory): boolean;
    static hasCommandIcon(commandName: string): boolean;
    static getIconsByCategory(category: IconCategory): Record<string, string>;
    static getIconInfo(key: string): IconInfo | undefined;
    static removeIcon(key: string, category: IconCategory): boolean;
    static createIconElement(key: string, category?: IconCategory, className?: string): HTMLElement | null;
}

interface IconRegistryOptions {
    category?: IconCategory;   // 默认 Custom
    override?: boolean;        // 是否覆盖已存在图标
}

enum IconCategory {
    Commands = 'commands', Actbar = 'actbar', FuncButtons = 'func-buttons',
    LayerDialog = 'layer-dialog', Menubar = 'menubar', Osnap = 'osnap',
    Palette = 'palette', PaletteLayer = 'palette-layer', PalettePlot = 'palette-plot',
    Misc = 'misc', Custom = 'custom',   // 无 Toolbar/Sidebar/General；默认类别是 Custom
}
```

注册后**并不是**所有 UI 都能找到——各 UI 查找的分类不同：

| UI | 查找方式 |
|---|---|
| Ribbon 按钮 `icon`、右键菜单 `icon`、菜单默认的命令图标、命令面板 | **只查 `Commands` 分类** |
| 菜单项显式 `icon` 字段 | 全分类搜索 |
| 侧栏 ActivityBar 图标、`<tool-bar-button src>` | 解析 `images/<分类>/<名>.svg` 路径，到对应分类查 |
| 浮动工具栏 `icon` | **不查注册表**，只认内联 SVG 串或图片 URL |

所以给命令 / Ribbon / 右键用的图标要注册到 `IconCategory.Commands`（`registerCommandIcon` 或插件 `context.registerIcon`）。**所有 key 统一小写化存储与查询**——`getIcon('MYCMD')` 和 `getIcon('mycmd')` 等价。移除用 `removeIcon(key, category)`；重复注册默认 `override: true`（即静默覆盖）。

> ⚠️ 不对称：`getIcon`/`hasIcon` 的 `category` 省略时**全分类搜索**，但 `removeIcon` 的 `category` 是**必填**——`removeIcon('my-icon')` 不带分类返回 `false` 删不掉，必须 `removeIcon('my-icon', IconCategory.Custom)`（注册时默认入 Custom 类）。

## 2. 注册示例

```js
const ICON_LINE = `
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
     stroke-linecap="round" stroke-linejoin="round">
    <line x1="4" y1="20" x2="20" y2="4"/>
</svg>`;

IconRegistry.registerIcon('my-line', ICON_LINE, { category: IconCategory.Commands });

// 之后菜单 / Ribbon 用名字引用
{ command: 'MYLINE', label: '我的线', icon: 'my-line' }
```

不显式指定时 `options.category` 默认 `Custom`，**不是** `General`——而 Ribbon / 右键菜单只查 `Commands`，注册到 `Custom` 的图标在那里**显示不出来**。

## 3. 命令图标的命名约定

图标 key 在注册和查询时**都会被 `.toLowerCase()` 归一化**——只有一个 key 空间，大小写不敏感：

```js
context.registerCommand('DRAWCROSS', '绘制十字', DrawCrossCommand, opt);
context.registerIcon('DRAWCROSS', SVG);
// PluginContext.registerIcon(name, svg) → IconRegistry.registerCommandIcon(name.toUpperCase(), svg)
// 最终存进 commands 类别的 key 是 'drawcross'（自动小写化）
```

一次注册即可——菜单、Ribbon、命令面板查 `commands` 类别时同样走小写化查询，`'DRAWCROSS'` 和 `'drawcross'` 命中同一个图标。

## 4. 常用获取 / 检查

```js
const svg = IconRegistry.getIcon('line', IconCategory.Commands);
if (!svg) console.warn('未注册');

if (!IconRegistry.hasIcon('my-line', IconCategory.Commands)) {
    IconRegistry.registerIcon('my-line', SVG, { category: IconCategory.Commands });
}

// 覆盖已注册图标（override 默认就是 true，可省）：
IconRegistry.registerIcon('my-line', SVG2, { category: IconCategory.Commands });

// 真要移除：
IconRegistry.removeIcon('my-line', IconCategory.Commands);
```

## 5. SVG 设计规范

```html
<svg viewBox="0 0 24 24"
     fill="none"
     stroke="currentColor"
     stroke-width="2"
     stroke-linecap="round"
     stroke-linejoin="round">
    <!-- 路径 -->
</svg>
```

要点：

- **`viewBox="0 0 24 24"`** 与系统其他图标一致；不一致会显示比例错乱
- **`stroke="currentColor"`** 让按钮主题色（深 / 浅）自动套用；写死 `#fff` 在浅色主题下会"消失"
- 单色图标用线性描边，**`stroke-width="2"`** 视觉密度统一
- 主体填色推荐 `fill="none"` 或 `fill="currentColor"`，避免硬编码
- 多色 / 渐变可以使用，但要在两种主题下都好看

## 6. 在 MenuBar / Ribbon / Toolbar 中使用

```js
// 菜单项
{ type: 'command', command: 'MYCMD' }   // 自动用 IconRegistry.getIcon('MYCMD', Commands)

// Ribbon 按钮（icon 是 Commands 分类里的图标名；提示字段叫 prompt，没有 tooltip）
{ cmd: 'MYCMD', icon: 'mycmd', label: '我的命令', prompt: '...' }

// 浮动工具栏
{ id: 'mycmd', icon: IconRegistry.getIcon('mycmd', IconCategory.Commands),
  tooltip: '我的命令', command: 'MYCMD' }
```

## 7. 内置图标速查（节选）

| name（小写） | 含义 |
|---|---|
| `line` `pline` `circle` `arc` `ellipse` `polygon` `rectang` | 绘制 |
| `move` `copy` `rotate` `scale` `mirror` `offset` `trim` `extend` `fillet` `explode` `erase` | 编辑（**没有** `chamfer`） |
| `undo` `redo` `regen` `zoom` | 系统 |
| `hatch` `text` `mtext` `dimlinear` `dimdiameter` `dimradius` `dimaligned` `angledim` | 标注 / 填充（角度标注是 `angledim`，**没有** `dimangular`） |

完整名以注册表为准：`Object.keys(IconRegistry.getAllCommandIcons())`（或 `IconRegistry.getIconsByCategory(IconCategory.Commands)`）列出全部内置命令图标名。

## 8. 跨插件共享 / 优先级

- 第二次注册同名图标 **会覆盖** 之前的（无警告）
- 这意味着插件可以"皮肤化"内置图标；也意味着插件互相有可能踩
- 建议：插件自己的图标加前缀 `myplugin-foo`，避免冲突

## 红旗

- 没有正确写 `currentColor` 时，主题切换图标会"消失"
- `viewBox` 不为 24×24 → 在 Ribbon 里被裁切或留白
- 注册重复名导致 **谁后注册谁生效**（`override` 默认 true，无警告）；多插件场景务必用前缀
- 图标 key 大小写不敏感（统一小写化）——注册 `'DRAWCROSS'` 还是 `'drawcross'` 结果一样，一次即可
- SVG 字符串末尾 / 开头不要带 `<?xml ... ?>` 声明 —— vjcad 内部直接 `innerHTML` 拼接

## 参考（官方文档 MCP）

- 文档：`vjcad/doc/ui/icons.md`
- 示例：`vjcad/example/ui/01ribbon.js`、命令 / 插件示例中频繁使用
