# 基础控件（NumberInput / ToolBarButton / 线型预览）

> 文档：`vjcad/doc/ui/controls.md`
> 示例：
> - `vjcad/example/ui/12modaldialogbase.js` — 在对话框中使用 NumberInput
> - `vjcad/example/ui/13modelesspanelbase.js` — 工具栏 / 自定义按钮
> - `vjcad/example/ui/04yesnodialog.js` — 对话框基础

vjcad 把"和 CAD 习惯一致的小控件"做成 Web Components，可在自定义对话框 / 面板中复用。

## 1. NumberInputComponent (`<input-number>`)

```html
<input-number
    .value=${this.x}
    .luprec=${4}             precision: 小数位
    .allowMinus=${true}      允许负数
    .allowZero=${true}       允许 0
    @change=${this.onXChange}>
</input-number>
```

属性：

| 属性 | 类型 | 说明 |
|---|---|---|
| `value` | number | **仅初始值**：只在 `connectedCallback` 时转成显示文本，之后再改 `.value` 不会刷新显示，也不同步 `numValue` |
| `luprec` | number | 显示精度（小数位，默认 6） |
| `allowMinus` | boolean | 是否允许负数（**默认 false**） |
| `allowZero` | boolean | 是否允许 0（**默认 false**；坐标类输入两个都要开） |
| `disabled` | boolean | |
| `theme` | string | `'dark' \| 'light'` |

> ⚠️ 组件**没有** `min` / `max` / `step` / `placeholder` 属性——范围校验只能自己在 change 里做。
> 另有 `isAny` / `numValue` / `numStr` / `ANYSTR` 内部态和 `setValue`/`updateValue`/`setNumbers`/`setDisabled` 方法。
> **程序化改值用 `updateValue(v)`**（`setValue` 只改内部态不刷新输入框）；读当前数值用 `numValue`（初始为 0，`value` 不会写进它，直到用户提交或 `setValue/updateValue`）。

事件：

```js
host.addEventListener('change', e => {
    const v = e.detail.value;       // ⚠️ string（输入原文），数值化用 Number(v) 或读 e.target.numValue
});
// 每次 Enter、每次失焦（值合法时）都会触发——按 Enter 后再点别处会收到第二次 change，处理要幂等
// 没有向外抛 'input' 事件——input_callback 只做内部校验
```

实战：在属性面板里编辑端点：

```js
class PointEditor extends LitElement {
    @property({ type: Object }) point = { x: 0, y: 0 };

    onXChange(e) { this.point = { ...this.point, x: Number(e.detail.value) }; this._emit(); }
    onYChange(e) { this.point = { ...this.point, y: Number(e.detail.value) }; this._emit(); }
    _emit() { this.dispatchEvent(new CustomEvent('change', { detail: this.point })); }

    render() {
        return html`
            <div>X: <input-number .value=${this.point.x} .luprec=${4} .allowMinus=${true} .allowZero=${true}
                                  @change=${this.onXChange}></input-number></div>
            <div>Y: <input-number .value=${this.point.y} .luprec=${4} .allowMinus=${true} .allowZero=${true}
                                  @change=${this.onYChange}></input-number></div>`;
        // 外部改了 point 想刷新显示：this.renderRoot.querySelector('input-number').updateValue(newX)
    }
}
```

## 2. ToolbarButton (`<tool-bar-button>`)

```html
<tool-bar-button
    title="直线"
    src="images/commands/line.svg"
    @click=${() => Engine.editor.executerWithOp('LINE')}>
</tool-bar-button>
```

属性（实现类 `ButtonEle`）：

| 属性 | 说明 |
|---|---|
| `title` | tooltip + a11y label |
| `execute` | 命令名**字符串而已**：按钮自身不绑定点击，只有内置工具条（如命令行工具区）读取它；自己用要 `@click` 里 `executerWithOp` |
| `src` | **不是任意 URL**：只认 `[./]images/<分类>/<名>.svg` 形式，从中解析出分类（`commands`/`actbar`/`func-buttons`/…）和名字去 IconRegistry 查；不匹配则**不显示图标**（不会发请求加载图片） |
| `svgContent` | 直接给 SVG 字符串（优先于 `src`）；自定义图标最稳的写法 |
| `theme` | `'dark' \| 'light'` |
| `isOn` | 声明了但 `ButtonEle` 渲染里没用到——**无任何视觉效果** |
| `state` | 状态字符串（配合 `preSelectMode` 用） |
| `preSelectMode` | 预选模式标记 |

> ⚠️ **没有** `disabled` / `selected` 属性；`isOn` 也不产生高亮——按下/选中态请自己用外层 class 或 `svgContent` 换图标表现。

事件：

```js
btn.addEventListener('click', () => Engine.editor.executerWithOp('LINE'));   // execute 属性不会自动执行
// 没有 'select' 事件——按钮态切换自己处理
```

实战：自定义浮动工具栏（无需 createFloatingToolbar 的简化版）：

```js
class MyToolbar extends LitElement {
    render() {
        return html`
            <div style="display:flex; gap:4px; padding:4px;">
                <tool-bar-button title="直线" src="images/commands/line.svg"
                    @click=${() => Engine.editor.executerWithOp('LINE')}></tool-bar-button>
                <tool-bar-button title="圆"   src="images/commands/circle.svg"
                    @click=${() => Engine.editor.executerWithOp('CIRCLE')}></tool-bar-button>
                <tool-bar-button title="弧"   src="images/commands/arc.svg"
                    @click=${() => Engine.editor.executerWithOp('ARC')}></tool-bar-button>
            </div>`;
    }
}
customElements.define('my-toolbar', MyToolbar);
```

## 3. 线型预览（没有独立 `<linetype-preview>` 元素）

vjcad **没有导出**独立的线型预览控件。可用的真实路径：

- **`LinetypeViewerDialog`**（`<linetype-viewer-dialog>`）：只读对话框，内部 `loadLinetypeContent()` 预览全部已加载线型
- **`layer-linetype-cell`**：图层管理表里渲染"线型"列的内部单元格组件（`LayerLinetypeCell`，`name` 属性），绑定在图层表格流程里，不适合单独复用
- 自定义下拉里的预览，用 `Engine.linetypeManager.getLinetype(name)` 拿 `LinetypeDefinition`（`elements`/`patternLength`）自己画 `<canvas>`，十几行即可

```js
// 只读查看全部已加载线型：
const viewer = new LinetypeViewerDialog();
await viewer.startDialog();

// 自己画缩略：取线型定义（注意是 Engine.linetypeManager，不是 currentDoc.lineTypes）
const lt = Engine.linetypeManager.getLinetype('Hidden');   // LinetypeDefinition | null
console.log(lt.elements, lt.patternLength);
```

## 4. 启动撤销标记

vjcad 在自定义按钮 / 面板中改 model 时，要把"一次操作"打成 undo 单元：

```js
Engine.startUndoMark('修改坐标');
selected.forEach(e => {
    Engine.markModified(e);          // = undoManager.modEntity_undoMark(e)：先记录修改前状态
    e.startPoint = newPoint;         // 再改
});
Engine.endUndoMark();
Engine.regen();
```

`startUndoMark`/`endUndoMark` 是 `Engine` 上的 static（也作为顶层函数导出），参数收 `string | UndoDescriptor`；`Engine.undoManager`（UndoManager）上的同名功能内部叫 `start_undoMark`，不要直接调。它们**只负责把中间的撤销记录合成一组**，本身不记录任何修改——直接赋值属性不会进撤销栈，必须在改之前逐个 `Engine.markModified(e)`（或 `Engine.markModifiedBatch(arr)`）。

## 5. message / writeMessage 速记

```js
import { message, writeMessage } from 'vjcad';

message.info({ content: '保存中…', duration: 0, key: 'save' });
await save();
message.success({ content: '保存成功', duration: 3, key: 'save' });

writeMessage('<br/>已添加 ' + n + ' 个实体');
```

详见 [dialogs](dialogs.md) 第 1 节。

## 6. 主题相关 CSS 变量

没有 `--input-bg` / `--button-bg` 这类变量。控件直接读全局主题变量（`THEME_COLOR_CSS_VARS` 里的 `--theme-color-*`，组件内部不重定义，所以在祖先元素上设置即可继承）：

| 控件 | 读取的变量 |
|---|---|
| `<input-number>` | `--theme-color-input-bg`（底色）、`--theme-color-font`（字色）、`--theme-color-border`（边框）、`--theme-color-accent`（valid 态边框） |
| `<tool-bar-button>` | `--theme-color-toolbar-button-bg`（回退 `--theme-color-input-bg`）、`--theme-color-border`、`--theme-hover`（悬停）、`--pre-select-mode`（预选态边框）、light 主题用 `--theme-color-font` 作底 |

```js
// 推荐：全局改（写到 document.documentElement，内置 UI 一起变）
Engine.setThemeColors({ inputBg: '#1e1e1e', font: '#f0f0f0', border: '#3d3d3d',
                        accent: '#4589ee', toolbarButtonBg: '#3a3a3a', hover: '#4a4a4a' });
```

```css
/* 只影响自己面板里的控件：在自己组件的 static styles 中给宿主设变量 */
:host { --theme-color-input-bg: #1e1e1e; --theme-color-toolbar-button-bg: #3a3a3a; }
```

## 红旗

- `<input-number>` 的 `change` 在**每次 Enter、每次失焦**（值合法时）都触发，Enter 后再失焦会触发两次；`e.detail.value` 是**字符串**原文——用 `Number(e.detail.value)` 取值；输入中途没有 `input` 事件可监听；`.value` 只是初始值，程序化改值用 `updateValue(v)`；默认 `allowZero/allowMinus` 都是 false
- `<tool-bar-button>` 的 `execute` 不会自动执行、`isOn` 没有视觉效果、`src` 只认 `images/<分类>/<名>.svg` 注册表路径——点击自己绑 `@click`，自定义图标用 `svgContent`（没有 `disabled`/`selected` 属性）
- 没有独立 `<linetype-preview>` 元素——预览线型用 `LinetypeViewerDialog` 或自己画
- vjcad 控件都基于 Lit；自定义对话框 `useShadowDOM=false` 时这些控件依旧用 ShadowDOM，外层 CSS 不能穿透
- 异步操作期间记得 `message.key` 复用同一 toast，避免叠出多条

## 参考（官方文档 MCP）

- 文档：`vjcad/doc/ui/controls.md`
- 示例：
- `vjcad/example/ui/12modaldialogbase.js` — 在对话框中使用 NumberInput
- `vjcad/example/ui/13modelesspanelbase.js` — 工具栏 / 自定义按钮
- `vjcad/example/ui/04yesnodialog.js` — 对话框基础
