增加测量窗口
This commit is contained in:
@@ -322,6 +322,15 @@ interface IBimComponent {
|
||||
- **`index.type.ts`**: 标签页类型定义
|
||||
- **`index.css`**: 标签页样式
|
||||
|
||||
#### `measure-panel/`
|
||||
- **`index.ts`**: `MeasurePanel` 类 - 测量面板组件(仅 UI,不包含真实测量算法)
|
||||
- 顶部:8 种测量方式按钮(默认显示前 4 种,可展开/收起)
|
||||
- 中部:显示“当前测量方式”与“测量结果”(结果由外部注入)
|
||||
- 底部:删除全部/设置入口(本期仅预留方法/回调)
|
||||
- 实现 `IBimComponent` 接口,支持主题与国际化
|
||||
- **`types.ts`**: 测量面板类型定义(`MeasureMode`、`MeasureResult`、`MeasurePanelOptions`)
|
||||
- **`index.css`**: 测量面板样式
|
||||
|
||||
### 3.4 管理器目录 (`src/managers/`)
|
||||
|
||||
#### `dialog-manager.ts`
|
||||
@@ -512,6 +521,7 @@ const dialog = engine.dialog.create({
|
||||
| `RightKeyManager` | `src/managers/right-key-manager.ts` | 管理右键菜单 (Context Menu) | `BimComponent` |
|
||||
| `ModelTreeManager` | `src/managers/model-tree-manager.ts` | 模型树业务管理器 | `BimComponent` |
|
||||
| `PropertyPanelManager` | `src/managers/property-panel-manager.ts` | 属性面板业务管理器 (演示 Collapse) | `BimComponent` |
|
||||
| `MeasureDialogManager` | `src/managers/measure-dialog-manager.ts` | 测量弹窗管理器 | `BimComponent` |
|
||||
|
||||
### 4.2 组件类清单
|
||||
|
||||
@@ -528,6 +538,7 @@ const dialog = engine.dialog.create({
|
||||
| `BimTab` | `src/components/tab/index.ts` | 固定标签页组件 | `IBimComponent` |
|
||||
| `BimCollapse` | `src/components/collapse/index.ts` | 折叠面板组件 | `IBimComponent` |
|
||||
| `BimDescription` | `src/components/description/index.ts` | 描述列表组件 (Key-Value) | `IBimComponent` |
|
||||
| `MeasurePanel` | `src/components/measure-panel/index.ts` | 测量面板组件(仅 UI) | `IBimComponent` |
|
||||
|
||||
### 4.3 服务类清单
|
||||
|
||||
|
||||
4781
dist/bim-engine-sdk.es.js
vendored
4781
dist/bim-engine-sdk.es.js
vendored
File diff suppressed because one or more lines are too long
2
dist/bim-engine-sdk.es.js.map
vendored
2
dist/bim-engine-sdk.es.js.map
vendored
File diff suppressed because one or more lines are too long
350
dist/bim-engine-sdk.umd.js
vendored
350
dist/bim-engine-sdk.umd.js
vendored
File diff suppressed because one or more lines are too long
2
dist/bim-engine-sdk.umd.js.map
vendored
2
dist/bim-engine-sdk.umd.js.map
vendored
File diff suppressed because one or more lines are too long
120
dist/index.d.ts
vendored
120
dist/index.d.ts
vendored
@@ -19,7 +19,7 @@ declare class BimButtonGroup implements IBimComponent {
|
||||
protected emit<K extends keyof EngineEvents>(event: K, payload: EngineEvents[K]): void;
|
||||
private initContainer;
|
||||
/**
|
||||
* 设置事件拦截,防止事件<EFBFBD><EFBFBD>泡到下层元素(如 3D 引擎)
|
||||
* 设置事件拦截,防止事件冒泡到下层元素(如 3D 引擎)
|
||||
*/
|
||||
private setupEventInterception;
|
||||
private updatePosition;
|
||||
@@ -45,6 +45,12 @@ declare class BimButtonGroup implements IBimComponent {
|
||||
render(): void;
|
||||
private renderGroup;
|
||||
private renderButton;
|
||||
/**
|
||||
* 设置按钮的激活状态
|
||||
* @param id 按钮 ID
|
||||
* @param active 可选,如果不传则切换(toggle)当前状态
|
||||
*/
|
||||
setBtnActive(id: string, active?: boolean): void;
|
||||
private handleClick;
|
||||
private handleMouseEnter;
|
||||
private handleMouseLeave;
|
||||
@@ -120,6 +126,21 @@ declare class BimDialog implements IBimComponent {
|
||||
* @param recenter 是否重新计算定位(例如保持居中),默认 true
|
||||
*/
|
||||
fitWidth(recenter?: boolean): void;
|
||||
/**
|
||||
* 根据内容自动调整弹窗高度
|
||||
*
|
||||
* 设计说明:
|
||||
* - 主要用于“内容展开/收起”场景(比如测量面板展开后,Dialog 高度跟随变化)
|
||||
* - 默认不改变用户拖拽后的当前位置,只做边界夹紧,避免弹窗超出容器
|
||||
*
|
||||
* @param recenter 是否根据 options.position 重新定位(默认 false)
|
||||
*/
|
||||
fitHeight(recenter?: boolean): void;
|
||||
/**
|
||||
* 边界夹紧:保持当前 left/top 不变的前提下,确保弹窗不超出容器
|
||||
* 说明:用于 fitHeight / fitWidth 后的“尺寸变化”场景,避免弹窗被裁切。
|
||||
*/
|
||||
private clampToContainer;
|
||||
/**
|
||||
* 初始化弹窗位置
|
||||
*/
|
||||
@@ -148,7 +169,7 @@ declare class BimDialog implements IBimComponent {
|
||||
}
|
||||
|
||||
export declare class BimEngine extends EventEmitter {
|
||||
private container;
|
||||
container: HTMLElement;
|
||||
private wrapper;
|
||||
toolbar: ToolbarManager | null;
|
||||
constructTreeBtn: ConstructTreeManagerBtn | null;
|
||||
@@ -157,6 +178,7 @@ export declare class BimEngine extends EventEmitter {
|
||||
engine: EngineManager | null;
|
||||
rightKey: RightKeyManager | null;
|
||||
propertyPanel: PropertyPanelManager | null;
|
||||
measure: MeasureDialogManager | null;
|
||||
constructor(container: HTMLElement | string, options?: {
|
||||
locale?: LocaleType;
|
||||
theme?: ThemeType;
|
||||
@@ -247,6 +269,7 @@ export declare interface ButtonConfig {
|
||||
label: string;
|
||||
icon?: string;
|
||||
keepActive?: boolean;
|
||||
isActive?: boolean;
|
||||
disabled?: boolean;
|
||||
onClick?: (button: OptButton) => void;
|
||||
children?: ButtonConfig[];
|
||||
@@ -657,6 +680,98 @@ declare type Listener<T = any> = (payload: T) => void;
|
||||
*/
|
||||
declare type LocaleType = 'zh-CN' | 'en-US';
|
||||
|
||||
/**
|
||||
* 测量弹窗管理器
|
||||
*/
|
||||
declare class MeasureDialogManager extends BimComponent {
|
||||
private dialogId;
|
||||
private dialog;
|
||||
private panel;
|
||||
constructor(engine: BimEngine);
|
||||
init(): void;
|
||||
/**
|
||||
* 显示测量弹窗
|
||||
*/
|
||||
show(): void;
|
||||
/**
|
||||
* 获取当前测量方式
|
||||
* 说明:如果面板未创建,则返回 null
|
||||
*/
|
||||
getActiveMode(): MeasureMode | null;
|
||||
/**
|
||||
* 切换测量方式(你要求的“切换类型的方法”)
|
||||
* @param mode 测量方式
|
||||
*/
|
||||
switchMode(mode: MeasureMode): void;
|
||||
/**
|
||||
* 设置测量结果(由外部注入,仅用于显示)
|
||||
* @param result 测量结果;传 null 表示清空
|
||||
*/
|
||||
setResult(result: MeasureResult | null): void;
|
||||
/**
|
||||
* 删除全部(仅清空 UI;真实测量清理逻辑后续再接)
|
||||
*/
|
||||
clearAll(): void;
|
||||
/**
|
||||
* 打开设置(仅预留方法/回调)
|
||||
*/
|
||||
openSettings(): void;
|
||||
destroy(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 测量面板 - 类型定义
|
||||
*
|
||||
* 注意:
|
||||
* - 本次只实现 UI,不实现真实测量逻辑(拾取、画线、计算等)。
|
||||
* - 这里的类型以“可读性优先”为原则,尽量直观、易扩展。
|
||||
*/
|
||||
/**
|
||||
* 测量方式(8 种)
|
||||
*
|
||||
* 说明:
|
||||
* - id 采用英文驼峰/小写,便于程序内部使用;
|
||||
* - 显示名称必须通过国际化 key 获取(见 locales)。
|
||||
*/
|
||||
declare type MeasureMode = 'distance' | 'minDistance' | 'angle' | 'elevation' | 'volume' | 'laserDistance' | 'slope' | 'spaceVolume';
|
||||
|
||||
/**
|
||||
* 测量结果数据
|
||||
*
|
||||
* 说明:
|
||||
* - 真实测量未实现,因此结果由外部通过 setResult 传入。
|
||||
* - 不同测量方式对应不同字段;未传入则 UI 显示 “--”。
|
||||
*/
|
||||
declare interface MeasureResult {
|
||||
/** 距离(单位:mm) */
|
||||
distanceMm?: number;
|
||||
/** 最小距离(单位:mm) */
|
||||
minDistanceMm?: number;
|
||||
/** 角度(单位:deg) */
|
||||
angleDeg?: number;
|
||||
/** 标高(单位:mm) */
|
||||
elevationMm?: number;
|
||||
/** 体积(单位:m³) */
|
||||
volumeM3?: number;
|
||||
/** 激光测距(单位:mm) */
|
||||
laserDistanceMm?: number;
|
||||
/** 坡度(单位:%) */
|
||||
slopePercent?: number;
|
||||
/** 空间体积(单位:m³) */
|
||||
spaceVolumeM3?: number;
|
||||
/** 可选:展示测量点/结果点坐标(单位由引擎侧定义,这里只负责显示) */
|
||||
xyz?: MeasureXYZ;
|
||||
}
|
||||
|
||||
/**
|
||||
* 3D 坐标(可选展示)
|
||||
*/
|
||||
declare interface MeasureXYZ {
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 菜单项配置接口 (用于简化的对象配置)
|
||||
*/
|
||||
@@ -815,6 +930,7 @@ declare class ToolbarManager extends BimComponent {
|
||||
addButton(config: ButtonConfig): void;
|
||||
setButtonVisibility(id: string, v: boolean): void;
|
||||
setShowLabel(show: boolean): void;
|
||||
setBtnActive(id: string, active?: boolean): void;
|
||||
setVisible(visible: boolean): void;
|
||||
setBackgroundColor(color: string): void;
|
||||
setColors(colors: ButtonGroupColors): void;
|
||||
|
||||
@@ -1191,3 +1191,4 @@ type ExpandDirection = 'up' | 'down' | 'left' | 'right';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -105,6 +105,20 @@ constructor(options: DialogOptions)
|
||||
- 清空当前内容
|
||||
- 设置新内容(支持 HTML 字符串或 DOM 元素)
|
||||
|
||||
#### `fitHeight(recenter: boolean = false): void`
|
||||
根据内容自动调整弹窗高度
|
||||
|
||||
**使用场景**:
|
||||
- 弹窗内容存在“展开/收起”等高度变化的交互(例如测量面板展开后需要增高弹窗,避免遮挡底部操作按钮)
|
||||
|
||||
**参数**:
|
||||
- `recenter`: 是否根据 `options.position` 重新计算定位(默认 `false`)
|
||||
|
||||
**行为**:
|
||||
- 先将高度设置为 `auto`,获取自然高度
|
||||
- 再将高度夹紧到 `[minHeight, containerHeight]` 范围内
|
||||
- 默认不重置用户拖拽后的 `left/top`,仅做边界夹紧;若 `recenter=true` 则按 `position` 重新定位
|
||||
|
||||
#### `close(): void`
|
||||
关闭弹窗并销毁
|
||||
|
||||
@@ -890,3 +904,4 @@ interface DialogColors {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -609,3 +609,4 @@ interface ModelLoadOptions {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
252
docs/components/measure-panel.md
Normal file
252
docs/components/measure-panel.md
Normal file
@@ -0,0 +1,252 @@
|
||||
# MeasurePanel 组件详细文档
|
||||
|
||||
> 本文档详细描述 `MeasurePanel`(测量面板)组件的实现细节,包括 API、UI 结构、逻辑流程等,供后续维护/AI 重现。
|
||||
>
|
||||
> 重要说明:**本组件仅实现 UI,不实现真实测量算法**(不做拾取、画线、计算等)。测量结果通过对外方法注入,仅用于展示。
|
||||
|
||||
---
|
||||
|
||||
## 1. 组件概述
|
||||
|
||||
### 1.1 基本信息
|
||||
- **组件名称**:`MeasurePanel`
|
||||
- **文件路径**:`src/components/measure-panel/index.ts`
|
||||
- **类型定义**:`src/components/measure-panel/types.ts`
|
||||
- **样式文件**:`src/components/measure-panel/index.css`
|
||||
- **实现接口**:`IBimComponent`
|
||||
|
||||
### 1.2 在 SDK 中的位置
|
||||
- `MeasurePanel` 是内部 UI 组件
|
||||
- 由 `MeasureDialogManager` 创建并挂载到 `BimDialog` 中
|
||||
- 外部业务(SDK 使用者)不直接 import 组件类,统一通过 `engine.measure`(Manager)调用
|
||||
|
||||
---
|
||||
|
||||
## 2. 组件类 API 文档
|
||||
|
||||
### 2.1 构造函数
|
||||
|
||||
```typescript
|
||||
constructor(options?: MeasurePanelOptions)
|
||||
```
|
||||
|
||||
**参数**:
|
||||
- `options.defaultMode?`: 默认测量方式(不传默认 `distance`)
|
||||
- `options.defaultExpanded?`: 是否默认展开(不传默认 `false`,即只显示前 4 个)
|
||||
- `options.onModeChange?`: 用户切换测量方式时回调
|
||||
- `options.onClearAll?`: 用户点击“删除全部”时回调
|
||||
- `options.onSettings?`: 用户点击“设置”时回调
|
||||
|
||||
### 2.2 公共方法
|
||||
|
||||
#### `init(): void`
|
||||
- 初始化订阅(主题/语言),并刷新 UI 状态(展开/选中态/结果区)。
|
||||
|
||||
#### `setTheme(theme: ThemeConfig): void`
|
||||
- 将主题色映射到 CSS 变量(按钮背景、hover、active、文字、分割线等)。
|
||||
|
||||
#### `setLocales(): void`
|
||||
- 更新所有用户可见文本:
|
||||
- 8 个按钮的 tooltip(图标占位时 tooltip 是主要可读文本)
|
||||
- 展开/收起按钮 tooltip
|
||||
- “删除全部”文本
|
||||
- “设置”tooltip
|
||||
- “当前测量方式”显示文本
|
||||
- 主值 label(随模式变化)
|
||||
- X/Y/Z 标签
|
||||
|
||||
#### `destroy(): void`
|
||||
- 取消主题/语言订阅并移除 DOM。
|
||||
|
||||
#### `getActiveMode(): MeasureMode`
|
||||
- 获取当前选中的测量方式。
|
||||
|
||||
#### `switchMode(mode: MeasureMode): void`
|
||||
- **切换类型的方法**:切换当前测量方式(等价于 `setActiveMode`)。
|
||||
|
||||
#### `setActiveMode(mode: MeasureMode): void`
|
||||
- 设置当前测量方式,并触发 `onModeChange`(如果提供)。
|
||||
|
||||
#### `setResult(result: MeasureResult | null): void`
|
||||
- 外部注入测量结果:
|
||||
- `null`:清空显示(主值与 xyz 均显示 `--`)
|
||||
- 非 null:根据当前 mode 显示对应字段的值
|
||||
|
||||
#### `clearAll(): void`
|
||||
- 清空结果展示并触发 `onClearAll`(如果提供)。
|
||||
|
||||
#### `openSettings(): void`
|
||||
- 触发 `onSettings`(如果提供),否则输出中文警告日志(仅预留接口)。
|
||||
|
||||
#### `setExpanded(expanded: boolean): void`
|
||||
- 展开/收起按钮区(收起时只显示前 4 个)。
|
||||
|
||||
#### `getExpanded(): boolean`
|
||||
- 获取当前是否展开。
|
||||
|
||||
---
|
||||
|
||||
## 3. 分化组件说明
|
||||
|
||||
- 无
|
||||
|
||||
---
|
||||
|
||||
## 4. Manager API 文档(关联)
|
||||
|
||||
### 4.1 MeasureDialogManager(关联文件)
|
||||
- `src/managers/measure-dialog-manager.ts`
|
||||
|
||||
### 4.2 相关对外方法(本次新增/补齐)
|
||||
- `getActiveMode(): MeasureMode | null`
|
||||
- `switchMode(mode: MeasureMode): void`
|
||||
- `setResult(result: MeasureResult | null): void`
|
||||
- `clearAll(): void`
|
||||
- `openSettings(): void`
|
||||
|
||||
---
|
||||
|
||||
## 5. UI 详细描述
|
||||
|
||||
### 5.1 DOM 结构(核心)
|
||||
|
||||
```html
|
||||
<div class="bim-measure-panel">
|
||||
<div class="bim-measure-tools">
|
||||
<div class="bim-measure-tool-grid">
|
||||
<!-- 8 个按钮:收起时隐藏后 4 个 -->
|
||||
<button class="bim-measure-tool-btn is-active" data-mode="distance">
|
||||
<span class="bim-measure-tool-icon">(圆形占位 svg)</span>
|
||||
</button>
|
||||
<!-- ... -->
|
||||
</div>
|
||||
|
||||
<div class="bim-measure-toggle">
|
||||
<button class="bim-measure-toggle-btn">
|
||||
<!-- 箭头 svg,展开时旋转 180deg -->
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bim-measure-result">
|
||||
<div class="bim-measure-row">
|
||||
<span class="label">当前测量方式:</span>
|
||||
<span class="value">距离</span>
|
||||
</div>
|
||||
|
||||
<div class="bim-measure-row">
|
||||
<span class="label">距离:</span>
|
||||
<span class="value">--</span>
|
||||
</div>
|
||||
|
||||
<div class="bim-measure-xyz">
|
||||
<div class="bim-measure-row"><span class="label">X:</span><span class="value">--</span></div>
|
||||
<div class="bim-measure-row"><span class="label">Y:</span><span class="value">--</span></div>
|
||||
<div class="bim-measure-row"><span class="label">Z:</span><span class="value">--</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bim-measure-footer">
|
||||
<button class="bim-measure-clear-btn">删除全部</button>
|
||||
<button class="bim-measure-settings-btn">(齿轮 svg)</button>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 5.2 CSS 类名说明
|
||||
- `.bim-measure-tool-grid`: 4 列网格布局
|
||||
- `.bim-measure-tool-btn.is-active`: 当前选中态
|
||||
- `.bim-measure-toggle-btn.is-expanded`: 展开态(箭头旋转)
|
||||
- `.bim-measure-result`: 结果区,顶部与底部用分割线隔开
|
||||
|
||||
---
|
||||
|
||||
## 6. 逻辑流程详细描述
|
||||
|
||||
### 6.1 初始化
|
||||
- 构造函数创建 DOM,但不订阅
|
||||
- `init()`:
|
||||
- 订阅 `localeManager`、`themeManager`
|
||||
- 调用 `setLocales()`、`setTheme()`
|
||||
- 应用展开状态(隐藏/显示后 4 个按钮)
|
||||
- 应用选中态
|
||||
- 渲染结果(初始为 `--`)
|
||||
|
||||
### 6.2 切换测量方式
|
||||
- 点击按钮或调用 `switchMode/setActiveMode`
|
||||
- 更新内部 `activeMode`
|
||||
- 刷新按钮选中态
|
||||
- 更新“当前测量方式”文本
|
||||
- 更新主值 label
|
||||
- 触发 `onModeChange`(若提供)
|
||||
- 重新渲染结果
|
||||
|
||||
### 6.3 注入结果
|
||||
- 调用 `setResult(result)`
|
||||
- 保存 result 并刷新结果区:
|
||||
- 主值:根据 mode 显示对应字段
|
||||
- xyz:无则显示 `--`
|
||||
|
||||
---
|
||||
|
||||
## 7. 国际化支持
|
||||
|
||||
### 7.1 使用的翻译键(核心)
|
||||
- `measure.modes.*`:8 种模式名
|
||||
- `measure.actions.*`:展开/收起/删除全部/设置
|
||||
- `measure.labels.*`:当前方式、X/Y/Z、主值 label
|
||||
- `measure.units.*`:mm/°/m³/% 等单位
|
||||
|
||||
---
|
||||
|
||||
## 8. 主题支持
|
||||
|
||||
### 8.1 CSS 变量(核心)
|
||||
- `--bim-measure-border`
|
||||
- `--bim-measure-divider`
|
||||
- `--bim-measure-btn-bg` / `--bim-measure-btn-hover-bg` / `--bim-measure-btn-active-bg`
|
||||
- `--bim-measure-label-color` / `--bim-measure-value-color`
|
||||
- `--bim-measure-icon-color`
|
||||
|
||||
---
|
||||
|
||||
## 9. 使用示例(通过 Manager)
|
||||
|
||||
```typescript
|
||||
// 通过工具栏点击“测量”按钮打开弹窗后:外部可以这样注入展示数据
|
||||
engine.measure.switchMode('angle');
|
||||
engine.measure.setResult({ angleDeg: 12.34, xyz: { x: 1, y: 2, z: 3 } });
|
||||
|
||||
// 清空
|
||||
engine.measure.clearAll();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 实现细节(供 AI 重现)
|
||||
|
||||
### 10.1 关键点
|
||||
- 模式列表严格按需求顺序渲染
|
||||
- 收起时隐藏后 4 个按钮(通过 `style.display = 'none'`)
|
||||
- 图标占位统一用圆形 SVG
|
||||
- 主值显示使用 `formatWithUnit`(单位走国际化)
|
||||
|
||||
---
|
||||
|
||||
## 11. 类型定义
|
||||
|
||||
见 `src/components/measure-panel/types.ts`:
|
||||
- `MeasureMode`
|
||||
- `MeasureResult`
|
||||
- `MeasurePanelOptions`
|
||||
|
||||
---
|
||||
|
||||
## 12. 文件清单
|
||||
|
||||
- `src/components/measure-panel/index.ts`
|
||||
- `src/components/measure-panel/index.css`
|
||||
- `src/components/measure-panel/types.ts`
|
||||
- 关联:`src/managers/measure-dialog-manager.ts`
|
||||
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
import './bim-engine.css';
|
||||
import { ToolbarManager } from './managers/toolbar-manager';
|
||||
import { ButtonGroupManager } from './managers/button-group-manager';
|
||||
import { DialogManager } from './managers/dialog-manager';
|
||||
import { EngineManager } from './managers/engine-manager';
|
||||
import { RightKeyManager } from './managers/right-key-manager';
|
||||
import { ConstructTreeManagerBtn } from './managers/construct-tree-manager-btn';
|
||||
import { PropertyPanelManager } from './managers/property-panel-manager';
|
||||
import type { EngineOptions, ModelLoadOptions } from './components/engine';
|
||||
import { localeManager } from './services/locale';
|
||||
import { themeManager } from './services/theme';
|
||||
import type { LocaleType } from './locales/types';
|
||||
import type { ThemeType, ThemeConfig } from './themes/types';
|
||||
import { EventEmitter } from './core/event-emitter';
|
||||
import { EngineEvents } from './types/events';
|
||||
import {ToolbarManager} from './managers/toolbar-manager';
|
||||
import {ButtonGroupManager} from './managers/button-group-manager';
|
||||
import {DialogManager} from './managers/dialog-manager';
|
||||
import {EngineManager} from './managers/engine-manager';
|
||||
import {RightKeyManager} from './managers/right-key-manager';
|
||||
import {ConstructTreeManagerBtn} from './managers/construct-tree-manager-btn';
|
||||
import {PropertyPanelManager} from './managers/property-panel-manager';
|
||||
import {MeasureDialogManager} from './managers/measure-dialog-manager';
|
||||
import type {EngineOptions, ModelLoadOptions} from './components/engine';
|
||||
import {localeManager} from './services/locale';
|
||||
import {themeManager} from './services/theme';
|
||||
import type {LocaleType} from './locales/types';
|
||||
import type {ThemeType, ThemeConfig} from './themes/types';
|
||||
import {EventEmitter} from './core/event-emitter';
|
||||
import {EngineEvents} from './types/events';
|
||||
|
||||
export type { EngineOptions, ModelLoadOptions };
|
||||
export type {EngineOptions, ModelLoadOptions};
|
||||
|
||||
export class BimEngine extends EventEmitter {
|
||||
private container: HTMLElement;
|
||||
public container: HTMLElement;
|
||||
private wrapper: HTMLElement | null = null;
|
||||
|
||||
public toolbar: ToolbarManager | null = null; // 底部专用
|
||||
@@ -27,6 +28,7 @@ export class BimEngine extends EventEmitter {
|
||||
public engine: EngineManager | null = null; // 3D 引擎管理器
|
||||
public rightKey: RightKeyManager | null = null; // 右键菜单管理器
|
||||
public propertyPanel: PropertyPanelManager | null = null; // 属性面板 (演示 Collapse)
|
||||
public measure: MeasureDialogManager | null = null; // 测量面板
|
||||
|
||||
|
||||
constructor(
|
||||
@@ -62,10 +64,21 @@ export class BimEngine extends EventEmitter {
|
||||
return super.on(event, listener);
|
||||
}
|
||||
|
||||
public setLocale(locale: LocaleType) { localeManager.setLocale(locale); }
|
||||
public getLocale(): LocaleType { return localeManager.getLocale(); }
|
||||
public setTheme(theme: 'dark' | 'light') { themeManager.setTheme(theme); }
|
||||
public setCustomTheme(theme: ThemeConfig) { themeManager.setCustomTheme(theme); }
|
||||
public setLocale(locale: LocaleType) {
|
||||
localeManager.setLocale(locale);
|
||||
}
|
||||
|
||||
public getLocale(): LocaleType {
|
||||
return localeManager.getLocale();
|
||||
}
|
||||
|
||||
public setTheme(theme: 'dark' | 'light') {
|
||||
themeManager.setTheme(theme);
|
||||
}
|
||||
|
||||
public setCustomTheme(theme: ThemeConfig) {
|
||||
themeManager.setCustomTheme(theme);
|
||||
}
|
||||
|
||||
private init() {
|
||||
this.container.innerHTML = '';
|
||||
@@ -81,6 +94,7 @@ export class BimEngine extends EventEmitter {
|
||||
this.rightKey = new RightKeyManager(this, this.wrapper);
|
||||
this.constructTreeBtn = new ConstructTreeManagerBtn(this, this.wrapper);
|
||||
this.propertyPanel = new PropertyPanelManager(this);
|
||||
this.measure = new MeasureDialogManager(this);
|
||||
|
||||
// 初始主题
|
||||
this.updateTheme(themeManager.getTheme());
|
||||
@@ -105,6 +119,7 @@ export class BimEngine extends EventEmitter {
|
||||
this.dialog?.destroy();
|
||||
this.rightKey?.destroy();
|
||||
this.propertyPanel?.destroy();
|
||||
this.measure?.destroy();
|
||||
this.container.innerHTML = '';
|
||||
this.clear();
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ export class BimButtonGroup implements IBimComponent {
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置事件拦截,防止事件<EFBFBD><EFBFBD>泡到下层元素(如 3D 引擎)
|
||||
* 设置事件拦截,防止事件冒泡到下层元素(如 3D 引擎)
|
||||
*/
|
||||
private setupEventInterception(el: HTMLElement): void {
|
||||
const stopPropagation = (e: Event) => {
|
||||
@@ -319,6 +319,11 @@ export class BimButtonGroup implements IBimComponent {
|
||||
const btnEl = document.createElement('div');
|
||||
btnEl.className = 'opt-btn';
|
||||
|
||||
// 初始化时根据 button 自身的属性同步 active 状态
|
||||
if (button.isActive) {
|
||||
this.activeBtnIds.add(button.id);
|
||||
}
|
||||
|
||||
// 按钮优先使用自己的 align,否则使用全局配置,默认为 vertical
|
||||
const align = button.align || this.options.align || 'vertical';
|
||||
if (align === 'horizontal') {
|
||||
@@ -380,14 +385,34 @@ export class BimButtonGroup implements IBimComponent {
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置按钮的激活状态
|
||||
* @param id 按钮 ID
|
||||
* @param active 可选,如果不传则切换(toggle)当前状态
|
||||
*/
|
||||
public setBtnActive(id: string, active?: boolean): void {
|
||||
const button = this.findButtonById(id);
|
||||
if (!button) return;
|
||||
|
||||
// 确定最终状态
|
||||
const newState = active !== undefined ? active : !this.activeBtnIds.has(id);
|
||||
|
||||
if (newState) {
|
||||
this.activeBtnIds.add(id);
|
||||
} else {
|
||||
this.activeBtnIds.delete(id);
|
||||
}
|
||||
|
||||
// 同步对象状态并更新 DOM
|
||||
button.isActive = newState;
|
||||
this.updateButtonState(id);
|
||||
}
|
||||
|
||||
private handleClick(button: OptButton): void {
|
||||
if (button.disabled) return;
|
||||
if (!button.children || button.children.length === 0) {
|
||||
if (button.keepActive) {
|
||||
const wasActive = this.activeBtnIds.has(button.id);
|
||||
if (wasActive) this.activeBtnIds.delete(button.id);
|
||||
else this.activeBtnIds.add(button.id);
|
||||
this.updateButtonState(button.id);
|
||||
this.setBtnActive(button.id);
|
||||
}
|
||||
this.closeDropdown();
|
||||
if (button.onClick) button.onClick(button);
|
||||
@@ -518,16 +543,22 @@ export class BimButtonGroup implements IBimComponent {
|
||||
private updateButtonState(buttonId: string): void {
|
||||
const btnEl = this.btnRefs.get(buttonId);
|
||||
if (btnEl) {
|
||||
this.activeBtnIds.has(buttonId) ? btnEl.classList.add('active') : btnEl.classList.remove('active');
|
||||
if (this.activeBtnIds.has(buttonId)) {
|
||||
btnEl.classList.add('active');
|
||||
} else {
|
||||
btnEl.classList.remove('active');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getIcon(icon?: string): string { return icon || this.DEFAULT_ICON; }
|
||||
|
||||
public updateButtonVisibility(id: string, visible: boolean): void {
|
||||
if (!this.options.visibility) this.options.visibility = {};
|
||||
this.options.visibility[id] = visible;
|
||||
this.render();
|
||||
}
|
||||
|
||||
public setShowLabel(show: boolean): void {
|
||||
this.options.showLabel = show;
|
||||
this.updateLabelsVisibility();
|
||||
@@ -557,8 +588,10 @@ export class BimButtonGroup implements IBimComponent {
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public setBackgroundColor(color: string): void { this.setColors({ backgroundColor: color }); }
|
||||
private isVisible(id: string): boolean { return this.options.visibility?.[id] !== false; }
|
||||
|
||||
public destroy(): void {
|
||||
if (this.unsubscribeLocale) {
|
||||
this.unsubscribeLocale();
|
||||
|
||||
@@ -7,6 +7,7 @@ export interface ButtonConfig {
|
||||
label: string;
|
||||
icon?: string;
|
||||
keepActive?: boolean;
|
||||
isActive?:boolean;
|
||||
disabled?: boolean;
|
||||
onClick?: (button: OptButton) => void;
|
||||
children?: ButtonConfig[];
|
||||
|
||||
24
src/components/button-group/toolbar/buttons/measure/index.ts
Normal file
24
src/components/button-group/toolbar/buttons/measure/index.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import type {ButtonConfig} from '../../../index.type';
|
||||
import type {BimEngine} from '../../../../../bim-engine';
|
||||
|
||||
/**
|
||||
* 测量按钮配置
|
||||
* 使用工厂函数模式,注入 engine 实例
|
||||
*/
|
||||
export const createMeasureButton = (engine: BimEngine): ButtonConfig => {
|
||||
return {
|
||||
id: 'measure',
|
||||
groupId: 'group-1',
|
||||
type: 'button',
|
||||
label: 'toolbar.measure',
|
||||
icon: '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" fill-rule="evenodd" d="M3 6a3 3 0 0 0-3 3v7a3 3 0 0 0 3 3h18a3 3 0 0 0 3-3V9a3 3 0 0 0-3-3zm6 2H7v5a1 1 0 1 1-2 0V8H3a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h18a1 1 0 0 0 1-1V9a1 1 0 0 0-1-1h-2v3a1 1 0 1 1-2 0V8h-2v5a1 1 0 1 1-2 0V8h-2v3a1 1 0 1 1-2 0z" clip-rule="evenodd"/></svg>',
|
||||
keepActive: true,
|
||||
onClick: (button) => {
|
||||
if (button.isActive) {
|
||||
engine.measure?.show()
|
||||
} else {
|
||||
engine.measure?.destroy()
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -19,12 +19,14 @@ export class Toolbar extends BimButtonGroup {
|
||||
const { walkBirdButton } = await import('./buttons/walk/walk-bird');
|
||||
const { settingButton } = await import('./buttons/setting');
|
||||
const { infoButton } = await import('./buttons/info');
|
||||
const { createMeasureButton } = await import('./buttons/measure');
|
||||
|
||||
this.addGroup('group-1');
|
||||
|
||||
// 使用工厂函数创建按钮,并注入 engine
|
||||
if (this.engine) {
|
||||
this.addButton(createHomeButton(this.engine));
|
||||
this.addButton(createMeasureButton(this.engine));
|
||||
} else {
|
||||
console.warn('[Toolbar] Engine not available when creating buttons.');
|
||||
}
|
||||
|
||||
@@ -228,6 +228,66 @@ export class BimDialog implements IBimComponent {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据内容自动调整弹窗高度
|
||||
*
|
||||
* 设计说明:
|
||||
* - 主要用于“内容展开/收起”场景(比如测量面板展开后,Dialog 高度跟随变化)
|
||||
* - 默认不改变用户拖拽后的当前位置,只做边界夹紧,避免弹窗超出容器
|
||||
*
|
||||
* @param recenter 是否根据 options.position 重新定位(默认 false)
|
||||
*/
|
||||
public fitHeight(recenter: boolean = false) {
|
||||
// 1) 先让高度由内容自然撑开,便于测量真实高度
|
||||
this.element.style.height = 'auto';
|
||||
|
||||
// 2) 获取自然高度并做约束(最小高度 + 不超过容器)
|
||||
const naturalHeight = this.element.getBoundingClientRect().height;
|
||||
const minHeight = this.options.minHeight ?? 100;
|
||||
const containerHeight = this.container.clientHeight || 0;
|
||||
|
||||
// 如果容器高度不可用,至少保证最小高度
|
||||
let targetHeight = Math.max(minHeight, naturalHeight);
|
||||
|
||||
// 约束最大高度:不超过容器高度(避免完全溢出)
|
||||
if (containerHeight > 0) {
|
||||
targetHeight = Math.min(targetHeight, containerHeight);
|
||||
}
|
||||
|
||||
this.element.style.height = `${targetHeight}px`;
|
||||
|
||||
// 3) 定位修正:recenter 则重新按 position 计算,否则只做边界夹紧
|
||||
if (recenter) {
|
||||
this.initPosition();
|
||||
} else {
|
||||
this.clampToContainer();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 边界夹紧:保持当前 left/top 不变的前提下,确保弹窗不超出容器
|
||||
* 说明:用于 fitHeight / fitWidth 后的“尺寸变化”场景,避免弹窗被裁切。
|
||||
*/
|
||||
private clampToContainer(): void {
|
||||
const containerW = this.container.clientWidth;
|
||||
const containerH = this.container.clientHeight;
|
||||
const elW = this.element.offsetWidth;
|
||||
const elH = this.element.offsetHeight;
|
||||
|
||||
// 当前 left/top(优先从 style 读取,避免 NaN)
|
||||
const currentLeft = this.element.offsetLeft;
|
||||
const currentTop = this.element.offsetTop;
|
||||
|
||||
const maxLeft = Math.max(0, containerW - elW);
|
||||
const maxTop = Math.max(0, containerH - elH);
|
||||
|
||||
const nextLeft = Math.max(0, Math.min(currentLeft, maxLeft));
|
||||
const nextTop = Math.max(0, Math.min(currentTop, maxTop));
|
||||
|
||||
this.element.style.left = `${nextLeft}px`;
|
||||
this.element.style.top = `${nextTop}px`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化弹窗位置
|
||||
*/
|
||||
|
||||
222
src/components/measure-panel/index.css
Normal file
222
src/components/measure-panel/index.css
Normal file
@@ -0,0 +1,222 @@
|
||||
/**
|
||||
* 测量面板样式(只做 UI)
|
||||
*
|
||||
* 设计目标:
|
||||
* - 视觉尽量接近截图(深色半透明面板 + 图标按钮网格 + 结果区)
|
||||
* - 主题颜色尽量使用 CSS 变量,保证可被 ThemeManager / Dialog 主题覆盖
|
||||
*/
|
||||
|
||||
.bim-measure-panel {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
|
||||
/* 面板内部颜色尽量复用 Dialog 的变量,保证整体一致 */
|
||||
color: var(--bim-dialog-text-color, #ccc);
|
||||
}
|
||||
|
||||
/* 顶部:测量方式按钮区 */
|
||||
.bim-measure-tools {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.bim-measure-tool-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.bim-measure-tool-btn {
|
||||
width: 100%;
|
||||
height: 42px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--bim-measure-border, rgba(255, 255, 255, 0.12));
|
||||
background: var(--bim-measure-btn-bg, rgba(255, 255, 255, 0.06));
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background-color 0.15s ease, border-color 0.15s ease;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.bim-measure-tool-btn:hover {
|
||||
background: var(--bim-measure-btn-hover-bg, rgba(255, 255, 255, 0.10));
|
||||
}
|
||||
|
||||
.bim-measure-tool-btn.is-active {
|
||||
border-color: var(--bim-measure-active-border, rgba(255, 255, 255, 0.30));
|
||||
background: var(--bim-measure-btn-active-bg, rgba(255, 255, 255, 0.14));
|
||||
}
|
||||
|
||||
.bim-measure-tool-icon {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--bim-measure-icon-color, #ddd);
|
||||
}
|
||||
|
||||
.bim-measure-tool-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.bim-measure-toggle {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.bim-measure-toggle-btn {
|
||||
/* 你要求:更小,并带文字提示 */
|
||||
height: 22px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--bim-measure-border, rgba(255, 255, 255, 0.12));
|
||||
background: var(--bim-measure-btn-bg, rgba(255, 255, 255, 0.06));
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background-color 0.15s ease;
|
||||
padding: 0 6px;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.bim-measure-toggle-btn:hover {
|
||||
background: var(--bim-measure-btn-hover-bg, rgba(255, 255, 255, 0.10));
|
||||
}
|
||||
|
||||
.bim-measure-toggle-text {
|
||||
color: var(--bim-measure-label-color, rgba(255, 255, 255, 0.70));
|
||||
}
|
||||
|
||||
.bim-measure-toggle-icon svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
fill: currentColor;
|
||||
color: var(--bim-measure-icon-color, #ddd);
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
|
||||
.bim-measure-toggle-btn.is-expanded .bim-measure-toggle-icon svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* 中部:结果展示区 */
|
||||
.bim-measure-result {
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--bim-measure-divider, rgba(255, 255, 255, 0.10));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.bim-measure-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.bim-measure-row .label {
|
||||
color: var(--bim-measure-label-color, rgba(255, 255, 255, 0.70));
|
||||
min-width: 84px;
|
||||
}
|
||||
|
||||
.bim-measure-row .value {
|
||||
color: var(--bim-measure-value-color, rgba(255, 255, 255, 0.90));
|
||||
flex: 1;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.bim-measure-xyz {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.bim-measure-xyz .value {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* 底部:操作区(删除全部 / 设置) */
|
||||
.bim-measure-footer {
|
||||
margin-top: 12px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid var(--bim-measure-divider, rgba(255, 255, 255, 0.10));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
/* 你要求:底部不要“占满”交互区域,按钮按自身尺寸布局 */
|
||||
justify-content: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.bim-measure-clear-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--bim-measure-danger, white); /* 先用偏绿(接近截图),可由主题覆盖 */
|
||||
cursor: pointer;
|
||||
/* 缩小可点击区域:仅文字本身附近 */
|
||||
padding: 0;
|
||||
font-size: 13px;
|
||||
/* 防止外部环境(如 demo)给 button 设置 flex: 1 导致“各占一半” */
|
||||
flex: 0 0 auto !important;
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* 你要求:删除按钮不需要 hover 效果 */
|
||||
.bim-measure-clear-btn:hover,
|
||||
.bim-measure-clear-btn:active,
|
||||
.bim-measure-clear-btn:focus {
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.bim-measure-settings-btn {
|
||||
/* 你要求:管理(设置)按钮去掉边框与 hover;按钮按自身尺寸即可 */
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
/* 右侧对齐,但不扩大可点击区域 */
|
||||
margin-left: auto;
|
||||
/* 防止外部环境(如 demo)给 button 设置 flex: 1 导致“各占一半” */
|
||||
flex: 0 0 auto !important;
|
||||
}
|
||||
|
||||
/* 你要求:设置按钮不需要 hover 效果 */
|
||||
.bim-measure-settings-btn:hover,
|
||||
.bim-measure-settings-btn:active,
|
||||
.bim-measure-settings-btn:focus {
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.bim-measure-settings-btn svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
fill: currentColor;
|
||||
color: var(--bim-measure-icon-color, #ddd);
|
||||
}
|
||||
|
||||
|
||||
568
src/components/measure-panel/index.ts
Normal file
568
src/components/measure-panel/index.ts
Normal file
@@ -0,0 +1,568 @@
|
||||
import './index.css';
|
||||
import type { ThemeConfig } from '../../themes/types';
|
||||
import { IBimComponent } from '../../types/component';
|
||||
import { localeManager, t } from '../../services/locale';
|
||||
import { themeManager } from '../../services/theme';
|
||||
import type { MeasureMode, MeasurePanelOptions, MeasureResult } from './types';
|
||||
|
||||
/**
|
||||
* 测量面板组件(只做 UI,不实现真实测量)
|
||||
*
|
||||
* 组件职责:
|
||||
* - 展示 8 种测量方式按钮(默认 4 个,可展开/收起)
|
||||
* - 维护当前选中的测量方式(current mode)
|
||||
* - 展示测量结果(由外部 setResult 注入)
|
||||
* - 提供 “删除全部 / 设置” 的 UI 与对外方法(暂不实现真实逻辑,仅回调/占位)
|
||||
*
|
||||
* 注意:
|
||||
* - 所有用户可见文本必须通过 t(key) 获取(国际化强制要求)
|
||||
* - 组件需要订阅主题/语言变更,并在 destroy 时清理订阅
|
||||
*/
|
||||
export class MeasurePanel implements IBimComponent {
|
||||
public element: HTMLElement;
|
||||
|
||||
private options: MeasurePanelOptions;
|
||||
private activeMode: MeasureMode;
|
||||
private isExpanded: boolean;
|
||||
private result: MeasureResult | null = null;
|
||||
|
||||
// DOM 引用(便于局部更新,减少频繁 querySelector)
|
||||
private toolButtons: Map<MeasureMode, HTMLButtonElement> = new Map();
|
||||
private toggleBtn!: HTMLButtonElement;
|
||||
private toggleTextEl!: HTMLElement;
|
||||
private currentModeValueEl!: HTMLElement;
|
||||
private mainValueValueEl!: HTMLElement;
|
||||
private mainValueLabelEl!: HTMLElement;
|
||||
private xyzXEl!: HTMLElement;
|
||||
private xyzYEl!: HTMLElement;
|
||||
private xyzZEl!: HTMLElement;
|
||||
private clearBtn!: HTMLButtonElement;
|
||||
private settingsBtn!: HTMLButtonElement;
|
||||
|
||||
// 订阅清理
|
||||
private unsubscribeLocale: (() => void) | null = null;
|
||||
private unsubscribeTheme: (() => void) | null = null;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
* @param options 组件配置
|
||||
*/
|
||||
constructor(options: MeasurePanelOptions = {}) {
|
||||
this.options = options;
|
||||
this.activeMode = options.defaultMode ?? 'distance';
|
||||
this.isExpanded = options.defaultExpanded ?? false;
|
||||
|
||||
this.element = this.createDom();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化组件(实现 IBimComponent)
|
||||
*/
|
||||
public init(): void {
|
||||
// 订阅语言变更:更新所有文本/提示
|
||||
this.unsubscribeLocale = localeManager.subscribe(() => {
|
||||
this.setLocales();
|
||||
});
|
||||
|
||||
// 订阅主题变更:更新 CSS 变量(如需要)
|
||||
this.unsubscribeTheme = themeManager.subscribe((theme) => {
|
||||
this.setTheme(theme);
|
||||
});
|
||||
|
||||
// 初始应用
|
||||
this.setLocales();
|
||||
this.setTheme(themeManager.getTheme());
|
||||
|
||||
// 初始渲染状态(按钮显隐、选中态、结果区)
|
||||
this.applyExpandedState();
|
||||
this.applyActiveModeState();
|
||||
this.renderResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置主题(实现 IBimComponent)
|
||||
* @param theme 主题配置
|
||||
*/
|
||||
public setTheme(theme: ThemeConfig): void {
|
||||
// 为了可读性:这里显式写出映射,不做过度抽象
|
||||
const style = this.element.style;
|
||||
|
||||
// 这些变量不会强制覆盖外部(Dialog)已有变量,只做兜底
|
||||
style.setProperty('--bim-measure-border', theme.border ?? 'rgba(255, 255, 255, 0.12)');
|
||||
style.setProperty('--bim-measure-divider', theme.border ?? 'rgba(255, 255, 255, 0.10)');
|
||||
style.setProperty('--bim-measure-icon-color', theme.icon ?? '#ddd');
|
||||
style.setProperty('--bim-measure-label-color', theme.textSecondary ?? 'rgba(255, 255, 255, 0.70)');
|
||||
style.setProperty('--bim-measure-value-color', theme.textPrimary ?? 'rgba(255, 255, 255, 0.90)');
|
||||
|
||||
// “删除全部”颜色:截图中偏绿色,这里用 primary 做一个合理映射
|
||||
style.setProperty('--bim-measure-danger', theme.primary ?? '#46d369');
|
||||
style.setProperty('--bim-measure-btn-bg', theme.componentBackground ?? 'rgba(255, 255, 255, 0.06)');
|
||||
style.setProperty('--bim-measure-btn-hover-bg', theme.componentHover ?? 'rgba(255, 255, 255, 0.10)');
|
||||
style.setProperty('--bim-measure-btn-active-bg', theme.componentActive ?? 'rgba(255, 255, 255, 0.14)');
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置语言(实现 IBimComponent)
|
||||
*/
|
||||
public setLocales(): void {
|
||||
// 1) 更新按钮 tooltip(图标占位时,tooltip 是主要的可读文本)
|
||||
for (const [mode, btn] of this.toolButtons.entries()) {
|
||||
btn.title = t(this.getModeI18nKey(mode));
|
||||
btn.setAttribute('aria-label', btn.title);
|
||||
}
|
||||
|
||||
// 2) 更新展开/收起按钮 tooltip
|
||||
this.toggleBtn.title = this.isExpanded ? t('measure.actions.collapse') : t('measure.actions.expand');
|
||||
this.toggleBtn.setAttribute('aria-label', this.toggleBtn.title);
|
||||
|
||||
// 2.1) 更新展开/收起按钮可见文本(你要求的“文字提示”)
|
||||
if (this.toggleTextEl) {
|
||||
this.toggleTextEl.textContent = this.toggleBtn.title;
|
||||
}
|
||||
|
||||
// 3) 更新底部按钮文本/tooltip
|
||||
this.clearBtn.textContent = t('measure.actions.clearAll');
|
||||
this.settingsBtn.title = t('measure.actions.settings');
|
||||
this.settingsBtn.setAttribute('aria-label', this.settingsBtn.title);
|
||||
|
||||
// 4) 更新“当前方式”显示(value)
|
||||
this.currentModeValueEl.textContent = t(this.getModeI18nKey(this.activeMode));
|
||||
|
||||
// 5) 主值 label(随模式变化)
|
||||
this.mainValueLabelEl.textContent = t(this.getModeValueLabelI18nKey(this.activeMode));
|
||||
|
||||
// 6) XYZ label(使用 key)
|
||||
// 这里 label 在 createDom 已经是固定文本节点,直接用 setText 更新更直观
|
||||
// 但为了减少 DOM 结构复杂度,我们把 label 写在 createDom 里,通过 data-key 更新
|
||||
const labelNodes = this.element.querySelectorAll<HTMLElement>('[data-i18n-key]');
|
||||
labelNodes.forEach((node) => {
|
||||
const key = node.dataset.i18nKey;
|
||||
if (key) node.textContent = t(key);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁组件(实现 IBimComponent)
|
||||
*/
|
||||
public destroy(): void {
|
||||
// 清理订阅
|
||||
if (this.unsubscribeLocale) {
|
||||
this.unsubscribeLocale();
|
||||
this.unsubscribeLocale = null;
|
||||
}
|
||||
if (this.unsubscribeTheme) {
|
||||
this.unsubscribeTheme();
|
||||
this.unsubscribeTheme = null;
|
||||
}
|
||||
|
||||
// 清理事件监听:由于本组件的监听都绑定在创建时的具体按钮上,
|
||||
// 且按钮会随 element 一起被 GC,这里不做逐个 removeEventListener(可读性优先)
|
||||
|
||||
// 移除 DOM
|
||||
this.element.remove();
|
||||
}
|
||||
|
||||
// ==========================
|
||||
// 对外 API(给 Manager / 外部业务调用)
|
||||
// ==========================
|
||||
|
||||
/**
|
||||
* 获取当前测量方式
|
||||
*/
|
||||
public getActiveMode(): MeasureMode {
|
||||
return this.activeMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换测量方式(你要求的“切换类型的方法”)
|
||||
* @param mode 目标测量方式
|
||||
*/
|
||||
public switchMode(mode: MeasureMode): void {
|
||||
this.setActiveMode(mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前测量方式
|
||||
* @param mode 目标测量方式
|
||||
*/
|
||||
public setActiveMode(mode: MeasureMode): void {
|
||||
if (this.activeMode === mode) return;
|
||||
this.activeMode = mode;
|
||||
this.applyActiveModeState();
|
||||
|
||||
// 切换方式后,主值 label 也需要更新
|
||||
this.mainValueLabelEl.textContent = t(this.getModeValueLabelI18nKey(this.activeMode));
|
||||
this.currentModeValueEl.textContent = t(this.getModeI18nKey(this.activeMode));
|
||||
|
||||
// 通知外部(如果需要)
|
||||
if (this.options.onModeChange) {
|
||||
this.options.onModeChange(mode);
|
||||
}
|
||||
|
||||
// 模式切换后,结果展示也应刷新(例如某些字段显示为 --)
|
||||
this.renderResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置测量结果(由外部注入)
|
||||
* @param result 测量结果;传 null 表示清空
|
||||
*/
|
||||
public setResult(result: MeasureResult | null): void {
|
||||
this.result = result;
|
||||
this.renderResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除全部(只做 UI 状态清空 + 回调)
|
||||
*/
|
||||
public clearAll(): void {
|
||||
// 先清空结果显示
|
||||
this.result = null;
|
||||
this.renderResult();
|
||||
|
||||
// 通知外部
|
||||
if (this.options.onClearAll) {
|
||||
this.options.onClearAll();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开设置(本次只预留方法/回调)
|
||||
*/
|
||||
public openSettings(): void {
|
||||
if (this.options.onSettings) {
|
||||
this.options.onSettings();
|
||||
return;
|
||||
}
|
||||
|
||||
// 兜底:避免无声失败,打印中文日志(符合项目规范)
|
||||
console.warn('[MeasurePanel] 未提供设置回调 onSettings,当前仅预留接口。');
|
||||
}
|
||||
|
||||
/**
|
||||
* 展开 / 收起(可选对外调用)
|
||||
* @param expanded 是否展开
|
||||
*/
|
||||
public setExpanded(expanded: boolean): void {
|
||||
if (this.isExpanded === expanded) return;
|
||||
this.isExpanded = expanded;
|
||||
this.applyExpandedState();
|
||||
this.setLocales(); // 更新 tooltip(展开/收起)
|
||||
|
||||
// 通知外部:用于重新计算 Dialog 高度
|
||||
if (this.options.onExpandedChange) {
|
||||
this.options.onExpandedChange(this.isExpanded);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取是否展开
|
||||
*/
|
||||
public getExpanded(): boolean {
|
||||
return this.isExpanded;
|
||||
}
|
||||
|
||||
// ==========================
|
||||
// 内部实现
|
||||
// ==========================
|
||||
|
||||
private createDom(): HTMLElement {
|
||||
const root = document.createElement('div');
|
||||
root.className = 'bim-measure-panel';
|
||||
|
||||
// 顶部:工具按钮区
|
||||
const toolsBox = document.createElement('div');
|
||||
toolsBox.className = 'bim-measure-tools';
|
||||
|
||||
const grid = document.createElement('div');
|
||||
grid.className = 'bim-measure-tool-grid';
|
||||
|
||||
// 8 种测量方式(顺序严格按你给的)
|
||||
const modes: MeasureMode[] = [
|
||||
'distance',
|
||||
'minDistance',
|
||||
'angle',
|
||||
'elevation',
|
||||
'volume',
|
||||
'laserDistance',
|
||||
'slope',
|
||||
'spaceVolume'
|
||||
];
|
||||
|
||||
// 图标占位:统一用圆形(你要求的“圆形占位”)
|
||||
const circleIconSvg = `
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="9"></circle>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
// 逐个创建按钮
|
||||
for (let i = 0; i < modes.length; i++) {
|
||||
const mode = modes[i];
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'bim-measure-tool-btn';
|
||||
btn.dataset.mode = mode;
|
||||
|
||||
// icon
|
||||
const icon = document.createElement('span');
|
||||
icon.className = 'bim-measure-tool-icon';
|
||||
icon.innerHTML = circleIconSvg;
|
||||
btn.appendChild(icon);
|
||||
|
||||
// 点击切换模式
|
||||
btn.addEventListener('click', () => {
|
||||
this.setActiveMode(mode);
|
||||
});
|
||||
|
||||
// 先不在这里设置 title/text(统一交给 setLocales)
|
||||
this.toolButtons.set(mode, btn);
|
||||
grid.appendChild(btn);
|
||||
}
|
||||
|
||||
toolsBox.appendChild(grid);
|
||||
|
||||
// 展开/收起按钮(箭头)
|
||||
const toggleBox = document.createElement('div');
|
||||
toggleBox.className = 'bim-measure-toggle';
|
||||
|
||||
this.toggleBtn = document.createElement('button');
|
||||
this.toggleBtn.type = 'button';
|
||||
this.toggleBtn.className = 'bim-measure-toggle-btn';
|
||||
// 展开/收起按钮:更小,并带文字提示(展开/收起)
|
||||
// 注意:文本内容由 setLocales() 统一更新,这里先放一个占位容器
|
||||
this.toggleTextEl = document.createElement('span');
|
||||
this.toggleTextEl.className = 'bim-measure-toggle-text';
|
||||
const toggleIconEl = document.createElement('span');
|
||||
toggleIconEl.className = 'bim-measure-toggle-icon';
|
||||
toggleIconEl.innerHTML = `
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M7 10l5 5 5-5z"></path>
|
||||
</svg>
|
||||
`;
|
||||
this.toggleBtn.appendChild(this.toggleTextEl);
|
||||
this.toggleBtn.appendChild(toggleIconEl);
|
||||
this.toggleBtn.addEventListener('click', () => {
|
||||
this.isExpanded = !this.isExpanded;
|
||||
this.applyExpandedState();
|
||||
this.setLocales(); // 更新 tooltip(展开/收起)
|
||||
|
||||
// 通知外部:用于重新计算 Dialog 高度
|
||||
if (this.options.onExpandedChange) {
|
||||
this.options.onExpandedChange(this.isExpanded);
|
||||
}
|
||||
});
|
||||
|
||||
toggleBox.appendChild(this.toggleBtn);
|
||||
toolsBox.appendChild(toggleBox);
|
||||
root.appendChild(toolsBox);
|
||||
|
||||
// 中部:结果区
|
||||
const resultBox = document.createElement('div');
|
||||
resultBox.className = 'bim-measure-result';
|
||||
|
||||
// 当前方式
|
||||
const currentModeRow = document.createElement('div');
|
||||
currentModeRow.className = 'bim-measure-row';
|
||||
const currentModeLabel = document.createElement('span');
|
||||
currentModeLabel.className = 'label';
|
||||
currentModeLabel.dataset.i18nKey = 'measure.labels.currentMode';
|
||||
const currentModeValue = document.createElement('span');
|
||||
currentModeValue.className = 'value';
|
||||
this.currentModeValueEl = currentModeValue;
|
||||
currentModeRow.appendChild(currentModeLabel);
|
||||
currentModeRow.appendChild(currentModeValue);
|
||||
resultBox.appendChild(currentModeRow);
|
||||
|
||||
// 主结果值(随模式变化)
|
||||
const mainValueRow = document.createElement('div');
|
||||
mainValueRow.className = 'bim-measure-row';
|
||||
const mainValueLabel = document.createElement('span');
|
||||
mainValueLabel.className = 'label';
|
||||
this.mainValueLabelEl = mainValueLabel;
|
||||
const mainValueValue = document.createElement('span');
|
||||
mainValueValue.className = 'value';
|
||||
this.mainValueValueEl = mainValueValue;
|
||||
mainValueRow.appendChild(mainValueLabel);
|
||||
mainValueRow.appendChild(mainValueValue);
|
||||
resultBox.appendChild(mainValueRow);
|
||||
|
||||
// XYZ
|
||||
const xyzBox = document.createElement('div');
|
||||
xyzBox.className = 'bim-measure-xyz';
|
||||
|
||||
const makeXyzRow = (labelKey: string, valueElSetter: (el: HTMLElement) => void) => {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'bim-measure-row';
|
||||
const label = document.createElement('span');
|
||||
label.className = 'label';
|
||||
label.dataset.i18nKey = labelKey;
|
||||
const value = document.createElement('span');
|
||||
value.className = 'value';
|
||||
valueElSetter(value);
|
||||
row.appendChild(label);
|
||||
row.appendChild(value);
|
||||
return row;
|
||||
};
|
||||
|
||||
xyzBox.appendChild(makeXyzRow('measure.labels.x', (el) => (this.xyzXEl = el)));
|
||||
xyzBox.appendChild(makeXyzRow('measure.labels.y', (el) => (this.xyzYEl = el)));
|
||||
xyzBox.appendChild(makeXyzRow('measure.labels.z', (el) => (this.xyzZEl = el)));
|
||||
resultBox.appendChild(xyzBox);
|
||||
|
||||
root.appendChild(resultBox);
|
||||
|
||||
// 底部:删除全部 + 设置
|
||||
const footer = document.createElement('div');
|
||||
footer.className = 'bim-measure-footer';
|
||||
|
||||
this.clearBtn = document.createElement('button');
|
||||
this.clearBtn.type = 'button';
|
||||
this.clearBtn.className = 'bim-measure-clear-btn';
|
||||
this.clearBtn.addEventListener('click', () => {
|
||||
this.clearAll();
|
||||
});
|
||||
|
||||
this.settingsBtn = document.createElement('button');
|
||||
this.settingsBtn.type = 'button';
|
||||
this.settingsBtn.className = 'bim-measure-settings-btn';
|
||||
this.settingsBtn.innerHTML = `
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M19.14 12.94c.04-.31.06-.63.06-.94s-.02-.63-.06-.94l2.03-1.58a.5.5 0 0 0 .12-.64l-1.92-3.32a.5.5 0 0 0-.6-.22l-2.39.96a7.27 7.27 0 0 0-1.63-.94l-.36-2.54A.5.5 0 0 0 13.9 1h-3.8a.5.5 0 0 0-.49.42l-.36 2.54c-.58.23-1.12.54-1.63.94l-2.39-.96a.5.5 0 0 0-.6.22L2.71 7.48a.5.5 0 0 0 .12.64l2.03 1.58c-.04.31-.06.63-.06.94s.02.63.06.94L2.83 14.52a.5.5 0 0 0-.12.64l1.92 3.32c.13.22.39.3.6.22l2.39-.96c.5.4 1.05.71 1.63.94l.36 2.54c.04.24.25.42.49.42h3.8c.24 0 .45-.18.49-.42l.36-2.54c.58-.23 1.12-.54 1.63-.94l2.39.96c.22.09.47 0 .6-.22l1.92-3.32a.5.5 0 0 0-.12-.64l-2.03-1.58zM12 15.5A3.5 3.5 0 1 1 12 8a3.5 3.5 0 0 1 0 7.5z"></path>
|
||||
</svg>
|
||||
`;
|
||||
this.settingsBtn.addEventListener('click', () => {
|
||||
this.openSettings();
|
||||
});
|
||||
|
||||
footer.appendChild(this.clearBtn);
|
||||
footer.appendChild(this.settingsBtn);
|
||||
root.appendChild(footer);
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用“展开/收起”状态:默认只显示前 4 个按钮
|
||||
*/
|
||||
private applyExpandedState(): void {
|
||||
let index = 0;
|
||||
for (const btn of this.toolButtons.values()) {
|
||||
// 默认展示前四个,其余根据展开状态显示/隐藏
|
||||
if (index >= 4) {
|
||||
btn.style.display = this.isExpanded ? '' : 'none';
|
||||
} else {
|
||||
btn.style.display = '';
|
||||
}
|
||||
index++;
|
||||
}
|
||||
|
||||
// toggle 样式(旋转箭头)
|
||||
if (this.isExpanded) {
|
||||
this.toggleBtn.classList.add('is-expanded');
|
||||
} else {
|
||||
this.toggleBtn.classList.remove('is-expanded');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用“当前选中按钮”样式
|
||||
*/
|
||||
private applyActiveModeState(): void {
|
||||
for (const [mode, btn] of this.toolButtons.entries()) {
|
||||
if (mode === this.activeMode) {
|
||||
btn.classList.add('is-active');
|
||||
} else {
|
||||
btn.classList.remove('is-active');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染结果区(根据 activeMode 从 result 里取对应字段)
|
||||
*/
|
||||
private renderResult(): void {
|
||||
// 1) 主值
|
||||
const mainText = this.formatMainValue(this.activeMode, this.result);
|
||||
this.mainValueValueEl.textContent = mainText;
|
||||
|
||||
// 2) XYZ
|
||||
const xyz = this.result?.xyz;
|
||||
if (!xyz) {
|
||||
this.xyzXEl.textContent = '--';
|
||||
this.xyzYEl.textContent = '--';
|
||||
this.xyzZEl.textContent = '--';
|
||||
return;
|
||||
}
|
||||
|
||||
// 为了可读性:这里不做 fancy formatter,只做基础展示
|
||||
this.xyzXEl.textContent = this.formatNumber(xyz.x);
|
||||
this.xyzYEl.textContent = this.formatNumber(xyz.y);
|
||||
this.xyzZEl.textContent = this.formatNumber(xyz.z);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模式名称的国际化 key
|
||||
*/
|
||||
private getModeI18nKey(mode: MeasureMode): string {
|
||||
return `measure.modes.${mode}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取“主值 label”的国际化 key(随模式变化)
|
||||
*/
|
||||
private getModeValueLabelI18nKey(mode: MeasureMode): string {
|
||||
return `measure.labels.value.${mode}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将“当前模式”的主值格式化为文本
|
||||
* @param mode 当前模式
|
||||
* @param result 当前结果
|
||||
*/
|
||||
private formatMainValue(mode: MeasureMode, result: MeasureResult | null): string {
|
||||
if (!result) return '--';
|
||||
|
||||
// 根据不同 mode 读取对应字段并格式化单位
|
||||
// 单位文本也走国际化(可替换为英文/中文)
|
||||
switch (mode) {
|
||||
case 'distance':
|
||||
return this.formatWithUnit(result.distanceMm, 'measure.units.mm');
|
||||
case 'minDistance':
|
||||
return this.formatWithUnit(result.minDistanceMm, 'measure.units.mm');
|
||||
case 'angle':
|
||||
return this.formatWithUnit(result.angleDeg, 'measure.units.deg');
|
||||
case 'elevation':
|
||||
return this.formatWithUnit(result.elevationMm, 'measure.units.mm');
|
||||
case 'volume':
|
||||
return this.formatWithUnit(result.volumeM3, 'measure.units.m3');
|
||||
case 'laserDistance':
|
||||
return this.formatWithUnit(result.laserDistanceMm, 'measure.units.mm');
|
||||
case 'slope':
|
||||
return this.formatWithUnit(result.slopePercent, 'measure.units.percent');
|
||||
case 'spaceVolume':
|
||||
return this.formatWithUnit(result.spaceVolumeM3, 'measure.units.m3');
|
||||
default:
|
||||
return '--';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化数值 + 单位(单位走国际化)
|
||||
*/
|
||||
private formatWithUnit(value: number | undefined, unitKey: string): string {
|
||||
if (value === null || value === undefined || Number.isNaN(value)) return '--';
|
||||
return `${this.formatNumber(value)} ${t(unitKey)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 基础数字格式化(可读性优先)
|
||||
*/
|
||||
private formatNumber(value: number): string {
|
||||
// 保留 3 位小数以内(简单策略:整数不带小数,非整数保留到 3 位)
|
||||
if (Number.isInteger(value)) return String(value);
|
||||
return value.toFixed(3).replace(/0+$/g, '').replace(/\.$/g, '');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
96
src/components/measure-panel/types.ts
Normal file
96
src/components/measure-panel/types.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* 测量面板 - 类型定义
|
||||
*
|
||||
* 注意:
|
||||
* - 本次只实现 UI,不实现真实测量逻辑(拾取、画线、计算等)。
|
||||
* - 这里的类型以“可读性优先”为原则,尽量直观、易扩展。
|
||||
*/
|
||||
|
||||
/**
|
||||
* 测量方式(8 种)
|
||||
*
|
||||
* 说明:
|
||||
* - id 采用英文驼峰/小写,便于程序内部使用;
|
||||
* - 显示名称必须通过国际化 key 获取(见 locales)。
|
||||
*/
|
||||
export type MeasureMode =
|
||||
| 'distance' // 距离
|
||||
| 'minDistance' // 最小距离
|
||||
| 'angle' // 角度
|
||||
| 'elevation' // 标高
|
||||
| 'volume' // 体积
|
||||
| 'laserDistance' // 激光测距
|
||||
| 'slope' // 坡度
|
||||
| 'spaceVolume'; // 空间体积
|
||||
|
||||
/**
|
||||
* 3D 坐标(可选展示)
|
||||
*/
|
||||
export interface MeasureXYZ {
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 测量结果数据
|
||||
*
|
||||
* 说明:
|
||||
* - 真实测量未实现,因此结果由外部通过 setResult 传入。
|
||||
* - 不同测量方式对应不同字段;未传入则 UI 显示 “--”。
|
||||
*/
|
||||
export interface MeasureResult {
|
||||
/** 距离(单位:mm) */
|
||||
distanceMm?: number;
|
||||
/** 最小距离(单位:mm) */
|
||||
minDistanceMm?: number;
|
||||
/** 角度(单位:deg) */
|
||||
angleDeg?: number;
|
||||
/** 标高(单位:mm) */
|
||||
elevationMm?: number;
|
||||
/** 体积(单位:m³) */
|
||||
volumeM3?: number;
|
||||
/** 激光测距(单位:mm) */
|
||||
laserDistanceMm?: number;
|
||||
/** 坡度(单位:%) */
|
||||
slopePercent?: number;
|
||||
/** 空间体积(单位:m³) */
|
||||
spaceVolumeM3?: number;
|
||||
|
||||
/** 可选:展示测量点/结果点坐标(单位由引擎侧定义,这里只负责显示) */
|
||||
xyz?: MeasureXYZ;
|
||||
}
|
||||
|
||||
/**
|
||||
* MeasurePanel 组件配置
|
||||
*/
|
||||
export interface MeasurePanelOptions {
|
||||
/** 默认测量方式(不传则默认 distance) */
|
||||
defaultMode?: MeasureMode;
|
||||
/** 是否默认展开(不传则默认 false,即展示前 4 个) */
|
||||
defaultExpanded?: boolean;
|
||||
|
||||
/**
|
||||
* 测量方式切换回调(只通知 UI 状态变化,不包含真实测量逻辑)
|
||||
* @param mode 当前选中的测量方式
|
||||
*/
|
||||
onModeChange?: (mode: MeasureMode) => void;
|
||||
|
||||
/**
|
||||
* “删除全部”回调
|
||||
*/
|
||||
onClearAll?: () => void;
|
||||
|
||||
/**
|
||||
* “设置”回调
|
||||
*/
|
||||
onSettings?: () => void;
|
||||
|
||||
/**
|
||||
* 展开/收起状态变更回调
|
||||
* 说明:用于让外部(如 Dialog)重新计算尺寸。
|
||||
*/
|
||||
onExpandedChange?: (expanded: boolean) => void;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,52 +1,95 @@
|
||||
import { TranslationDictionary } from './types';
|
||||
import {TranslationDictionary} from './types';
|
||||
|
||||
export const enUS: TranslationDictionary = {
|
||||
common: {
|
||||
title: 'BimEngine',
|
||||
description: 'This is a BIM-ENGINE demo.',
|
||||
openTestDialog: 'Open Test Dialog',
|
||||
openInfoDialog: 'Open Info Dialog (Wrapped)',
|
||||
},
|
||||
toolbar: {
|
||||
home: 'Home',
|
||||
info: 'Info',
|
||||
location: 'Location',
|
||||
setting: 'Settings',
|
||||
walk: 'Walk',
|
||||
walkPerson: 'Person',
|
||||
walkBird: 'Bird Eye',
|
||||
walkMenu: 'Menu',
|
||||
tree: 'Tree',
|
||||
},
|
||||
dialog: {
|
||||
testTitle: 'Test Dialog',
|
||||
testContent: '<div style="padding: 10px;">This is a <b>draggable</b> and <b>resizable</b> dialog.<br><br>Try dragging the title bar or resizing from the bottom-right corner.</div>',
|
||||
},
|
||||
menu: {
|
||||
info: 'Info',
|
||||
home: 'Home',
|
||||
},
|
||||
tree: {
|
||||
searchPlaceholder: 'Please enter content to search',
|
||||
},
|
||||
common: {
|
||||
title: 'BimEngine',
|
||||
description: 'This is a BIM-ENGINE demo.',
|
||||
openTestDialog: 'Open Test Dialog',
|
||||
openInfoDialog: 'Open Info Dialog (Wrapped)',
|
||||
},
|
||||
toolbar: {
|
||||
home: 'Home',
|
||||
measure: 'Measure',
|
||||
info: 'Info',
|
||||
location: 'Location',
|
||||
setting: 'Settings',
|
||||
walk: 'Walk',
|
||||
walkPerson: 'Person',
|
||||
walkBird: 'Bird Eye',
|
||||
walkMenu: 'Menu',
|
||||
tree: 'Tree',
|
||||
},
|
||||
dialog: {
|
||||
testTitle: 'Test Dialog',
|
||||
testContent: '<div style="padding: 10px;">This is a <b>draggable</b> and <b>resizable</b> dialog.<br><br>Try dragging the title bar or resizing from the bottom-right corner.</div>',
|
||||
},
|
||||
menu: {
|
||||
info: 'Info',
|
||||
home: 'Home',
|
||||
},
|
||||
tree: {
|
||||
searchPlaceholder: 'Please enter content to search',
|
||||
},
|
||||
constructTree: {
|
||||
title: 'Construct Tree',
|
||||
},
|
||||
tab: {
|
||||
component: 'Component',
|
||||
system: 'System',
|
||||
space: 'Space',
|
||||
},
|
||||
panel: {
|
||||
property: {
|
||||
title: 'Component Details',
|
||||
base: 'Basic Info',
|
||||
material: 'Material',
|
||||
advanced: 'Advanced',
|
||||
tab: {
|
||||
props: 'Properties',
|
||||
material: 'Material'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
tab: {
|
||||
component: 'Component',
|
||||
system: 'System',
|
||||
space: 'Space',
|
||||
},
|
||||
panel: {
|
||||
property: {
|
||||
title: 'Component Details',
|
||||
base: 'Basic Info',
|
||||
material: 'Material',
|
||||
advanced: 'Advanced',
|
||||
tab: {
|
||||
props: 'Properties',
|
||||
material: 'Material'
|
||||
}
|
||||
}
|
||||
},
|
||||
measure: {
|
||||
btnName: 'Measure',
|
||||
dialogTitle: 'Measure',
|
||||
modes: {
|
||||
distance: 'Distance',
|
||||
minDistance: 'Min Distance',
|
||||
angle: 'Angle',
|
||||
elevation: 'Elevation',
|
||||
volume: 'Volume',
|
||||
laserDistance: 'Laser Distance',
|
||||
slope: 'Slope',
|
||||
spaceVolume: 'Space Volume',
|
||||
},
|
||||
actions: {
|
||||
expand: 'Expand',
|
||||
collapse: 'Collapse',
|
||||
clearAll: 'Clear All',
|
||||
settings: 'Settings',
|
||||
},
|
||||
labels: {
|
||||
currentMode: 'Mode:',
|
||||
x: 'X:',
|
||||
y: 'Y:',
|
||||
z: 'Z:',
|
||||
value: {
|
||||
distance: 'Distance:',
|
||||
minDistance: 'Min Distance:',
|
||||
angle: 'Angle:',
|
||||
elevation: 'Elevation:',
|
||||
volume: 'Volume:',
|
||||
laserDistance: 'Laser Distance:',
|
||||
slope: 'Slope:',
|
||||
spaceVolume: 'Space Volume:',
|
||||
}
|
||||
},
|
||||
units: {
|
||||
mm: 'mm',
|
||||
deg: '°',
|
||||
m3: 'm³',
|
||||
percent: '%',
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
/**
|
||||
* 翻译字典接口
|
||||
* 定义所有可用的翻译键值对结构,保证类型安全
|
||||
@@ -11,6 +12,7 @@ export interface TranslationDictionary {
|
||||
};
|
||||
toolbar: {
|
||||
home: string;
|
||||
measure: string;
|
||||
info: string;
|
||||
location: string;
|
||||
setting: string;
|
||||
@@ -51,6 +53,64 @@ export interface TranslationDictionary {
|
||||
system: string;
|
||||
space: string;
|
||||
};
|
||||
measure: {
|
||||
btnName: string;
|
||||
dialogTitle: string;
|
||||
|
||||
/**
|
||||
* 8 种测量方式名称(用于 UI 按钮 tooltip、当前方式显示等)
|
||||
*/
|
||||
modes: {
|
||||
distance: string;
|
||||
minDistance: string;
|
||||
angle: string;
|
||||
elevation: string;
|
||||
volume: string;
|
||||
laserDistance: string;
|
||||
slope: string;
|
||||
spaceVolume: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* 操作按钮文案
|
||||
*/
|
||||
actions: {
|
||||
expand: string;
|
||||
collapse: string;
|
||||
clearAll: string;
|
||||
settings: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* 结果区标签
|
||||
*/
|
||||
labels: {
|
||||
currentMode: string;
|
||||
x: string;
|
||||
y: string;
|
||||
z: string;
|
||||
value: {
|
||||
distance: string;
|
||||
minDistance: string;
|
||||
angle: string;
|
||||
elevation: string;
|
||||
volume: string;
|
||||
laserDistance: string;
|
||||
slope: string;
|
||||
spaceVolume: string;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 单位(也走国际化,避免硬编码)
|
||||
*/
|
||||
units: {
|
||||
mm: string;
|
||||
deg: string;
|
||||
m3: string;
|
||||
percent: string;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,6 +9,7 @@ export const zhCN: TranslationDictionary = {
|
||||
},
|
||||
toolbar: {
|
||||
home: '首页',
|
||||
measure: '测量',
|
||||
info: '信息',
|
||||
location: '定位',
|
||||
setting: '设置',
|
||||
@@ -24,7 +25,7 @@ export const zhCN: TranslationDictionary = {
|
||||
},
|
||||
menu: {
|
||||
info: '信息',
|
||||
home: '首页',
|
||||
home: '首页'
|
||||
},
|
||||
tree: {
|
||||
searchPlaceholder: '请输入要搜索的内容',
|
||||
@@ -48,5 +49,47 @@ export const zhCN: TranslationDictionary = {
|
||||
material: '材质'
|
||||
}
|
||||
}
|
||||
},
|
||||
measure: {
|
||||
btnName: '测量',
|
||||
dialogTitle: '测量',
|
||||
modes: {
|
||||
distance: '距离',
|
||||
minDistance: '最小距离',
|
||||
angle: '角度',
|
||||
elevation: '标高',
|
||||
volume: '体积',
|
||||
laserDistance: '激光测距',
|
||||
slope: '坡度',
|
||||
spaceVolume: '空间体积',
|
||||
},
|
||||
actions: {
|
||||
expand: '展开',
|
||||
collapse: '收起',
|
||||
clearAll: '删除全部',
|
||||
settings: '设置',
|
||||
},
|
||||
labels: {
|
||||
currentMode: '当前测量方式:',
|
||||
x: 'X:',
|
||||
y: 'Y:',
|
||||
z: 'Z:',
|
||||
value: {
|
||||
distance: '距离:',
|
||||
minDistance: '最小距离:',
|
||||
angle: '角度:',
|
||||
elevation: '标高:',
|
||||
volume: '体积:',
|
||||
laserDistance: '激光测距:',
|
||||
slope: '坡度:',
|
||||
spaceVolume: '空间体积:',
|
||||
}
|
||||
},
|
||||
units: {
|
||||
mm: 'mm',
|
||||
deg: '°',
|
||||
m3: 'm³',
|
||||
percent: '%',
|
||||
}
|
||||
}
|
||||
};
|
||||
149
src/managers/measure-dialog-manager.ts
Normal file
149
src/managers/measure-dialog-manager.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import {BimComponent} from '../core/component';
|
||||
import {BimEngine} from '../bim-engine';
|
||||
import {BimDialog} from "../components/dialog";
|
||||
import { MeasurePanel } from '../components/measure-panel';
|
||||
import type { MeasureMode, MeasureResult } from '../components/measure-panel/types';
|
||||
|
||||
/**
|
||||
* 测量弹窗管理器
|
||||
*/
|
||||
export class MeasureDialogManager extends BimComponent {
|
||||
private dialogId = 'measure-dialog';
|
||||
private dialog: BimDialog | null = null;
|
||||
private panel: MeasurePanel | null = null;
|
||||
|
||||
constructor(engine: BimEngine) {
|
||||
super(engine);
|
||||
}
|
||||
|
||||
public init(): void {
|
||||
// 可以在这里监听事件
|
||||
}
|
||||
/**
|
||||
* 显示测量弹窗
|
||||
*/
|
||||
public show() {
|
||||
if (!this.engine.dialog || !this.engine.container) {
|
||||
console.warn('Dialog manager or container is not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
const dialogWidth = 250;
|
||||
const dialogHeight = 300;
|
||||
const paddingRight = 20; // 你想要的右边距
|
||||
const container = this.engine.container;
|
||||
const containerWidth = container.clientWidth;
|
||||
const containerHeight = container.clientHeight;
|
||||
const x = containerWidth - dialogWidth - paddingRight;
|
||||
const y = (containerHeight - dialogHeight) / 2;
|
||||
|
||||
// 如果已打开过,先销毁旧实例,避免重复创建/重复订阅
|
||||
this.destroy();
|
||||
|
||||
// 创建测量面板(只做 UI,不实现真实测量)
|
||||
this.panel = new MeasurePanel({
|
||||
defaultMode: 'distance', // 默认展示前四个,且默认选中“距离”
|
||||
defaultExpanded: false,
|
||||
onModeChange: (mode) => {
|
||||
// 这里只做事件/占位:未来可在这里切换引擎内置工具
|
||||
// 本次需求不实现真实测量,因此仅保留回调位置
|
||||
console.log('[MeasureDialogManager] 当前测量方式已切换:', mode);
|
||||
},
|
||||
onClearAll: () => {
|
||||
// 预留:未来可清理引擎测量绘制/标注
|
||||
console.log('[MeasureDialogManager] 删除全部(仅 UI 清空,本次不清理引擎侧内容)');
|
||||
},
|
||||
onSettings: () => {
|
||||
// 预留:未来可打开设置弹窗/面板
|
||||
console.log('[MeasureDialogManager] 打开设置(仅预留接口)');
|
||||
},
|
||||
onExpandedChange: () => {
|
||||
// 展开/收起时,动态适配 Dialog 高度,避免遮挡底部操作按钮
|
||||
this.dialog?.fitHeight(false);
|
||||
}
|
||||
});
|
||||
this.panel.init();
|
||||
|
||||
// 注意:你要求“组件本身不加边距”,因此在 Manager 这里用 wrapper 增加左右内边距
|
||||
// 这样 MeasurePanel 可以保持通用性,避免在不同场景复用时产生多余 padding。
|
||||
const panelWrapper = document.createElement('div');
|
||||
panelWrapper.style.padding = '12px';
|
||||
panelWrapper.appendChild(this.panel.element);
|
||||
|
||||
this.dialog = this.engine.dialog.create({
|
||||
id: this.dialogId,
|
||||
title: 'measure.dialogTitle',
|
||||
content: panelWrapper,
|
||||
width: dialogWidth,
|
||||
// 高度交给 fitHeight 动态计算(避免内容展开后遮挡底部操作区)
|
||||
height: 'auto',
|
||||
position: {
|
||||
x: x,
|
||||
y: y
|
||||
},
|
||||
onClose: () => {
|
||||
this.engine.toolbar?.setBtnActive('measure', false)
|
||||
}
|
||||
});
|
||||
this.dialog.init();
|
||||
|
||||
// 初次打开时也执行一次自适应高度(收起态)
|
||||
this.dialog.fitHeight(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前测量方式
|
||||
* 说明:如果面板未创建,则返回 null
|
||||
*/
|
||||
public getActiveMode(): MeasureMode | null {
|
||||
return this.panel ? this.panel.getActiveMode() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换测量方式(你要求的“切换类型的方法”)
|
||||
* @param mode 测量方式
|
||||
*/
|
||||
public switchMode(mode: MeasureMode): void {
|
||||
if (!this.panel) return;
|
||||
this.panel.switchMode(mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置测量结果(由外部注入,仅用于显示)
|
||||
* @param result 测量结果;传 null 表示清空
|
||||
*/
|
||||
public setResult(result: MeasureResult | null): void {
|
||||
if (!this.panel) return;
|
||||
this.panel.setResult(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除全部(仅清空 UI;真实测量清理逻辑后续再接)
|
||||
*/
|
||||
public clearAll(): void {
|
||||
if (!this.panel) return;
|
||||
this.panel.clearAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开设置(仅预留方法/回调)
|
||||
*/
|
||||
public openSettings(): void {
|
||||
if (!this.panel) return;
|
||||
this.panel.openSettings();
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
// 关闭弹窗
|
||||
if (this.dialog) {
|
||||
this.dialog.destroy();
|
||||
this.dialog = null;
|
||||
}
|
||||
|
||||
// 销毁测量面板(清理订阅与 DOM)
|
||||
if (this.panel) {
|
||||
this.panel.destroy();
|
||||
this.panel = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,6 +60,7 @@ export class ToolbarManager extends BimComponent {
|
||||
public addButton(config: ButtonConfig) { this.toolbar?.addButton(config); this.toolbar?.render(); }
|
||||
public setButtonVisibility(id: string, v: boolean) { this.toolbar?.updateButtonVisibility(id, v); }
|
||||
public setShowLabel(show: boolean) { this.toolbar?.setShowLabel(show); }
|
||||
public setBtnActive(id: string, active?: boolean) { this.toolbar?.setBtnActive(id, active); }
|
||||
public setVisible(visible: boolean) {
|
||||
if (this.toolbarContainer) {
|
||||
this.toolbarContainer.style.visibility = visible ? 'visible' : 'hidden';
|
||||
|
||||
Reference in New Issue
Block a user