增加测量窗口
This commit is contained in:
@@ -322,6 +322,15 @@ interface IBimComponent {
|
|||||||
- **`index.type.ts`**: 标签页类型定义
|
- **`index.type.ts`**: 标签页类型定义
|
||||||
- **`index.css`**: 标签页样式
|
- **`index.css`**: 标签页样式
|
||||||
|
|
||||||
|
#### `measure-panel/`
|
||||||
|
- **`index.ts`**: `MeasurePanel` 类 - 测量面板组件(仅 UI,不包含真实测量算法)
|
||||||
|
- 顶部:8 种测量方式按钮(默认显示前 4 种,可展开/收起)
|
||||||
|
- 中部:显示“当前测量方式”与“测量结果”(结果由外部注入)
|
||||||
|
- 底部:删除全部/设置入口(本期仅预留方法/回调)
|
||||||
|
- 实现 `IBimComponent` 接口,支持主题与国际化
|
||||||
|
- **`types.ts`**: 测量面板类型定义(`MeasureMode`、`MeasureResult`、`MeasurePanelOptions`)
|
||||||
|
- **`index.css`**: 测量面板样式
|
||||||
|
|
||||||
### 3.4 管理器目录 (`src/managers/`)
|
### 3.4 管理器目录 (`src/managers/`)
|
||||||
|
|
||||||
#### `dialog-manager.ts`
|
#### `dialog-manager.ts`
|
||||||
@@ -512,6 +521,7 @@ const dialog = engine.dialog.create({
|
|||||||
| `RightKeyManager` | `src/managers/right-key-manager.ts` | 管理右键菜单 (Context Menu) | `BimComponent` |
|
| `RightKeyManager` | `src/managers/right-key-manager.ts` | 管理右键菜单 (Context Menu) | `BimComponent` |
|
||||||
| `ModelTreeManager` | `src/managers/model-tree-manager.ts` | 模型树业务管理器 | `BimComponent` |
|
| `ModelTreeManager` | `src/managers/model-tree-manager.ts` | 模型树业务管理器 | `BimComponent` |
|
||||||
| `PropertyPanelManager` | `src/managers/property-panel-manager.ts` | 属性面板业务管理器 (演示 Collapse) | `BimComponent` |
|
| `PropertyPanelManager` | `src/managers/property-panel-manager.ts` | 属性面板业务管理器 (演示 Collapse) | `BimComponent` |
|
||||||
|
| `MeasureDialogManager` | `src/managers/measure-dialog-manager.ts` | 测量弹窗管理器 | `BimComponent` |
|
||||||
|
|
||||||
### 4.2 组件类清单
|
### 4.2 组件类清单
|
||||||
|
|
||||||
@@ -528,6 +538,7 @@ const dialog = engine.dialog.create({
|
|||||||
| `BimTab` | `src/components/tab/index.ts` | 固定标签页组件 | `IBimComponent` |
|
| `BimTab` | `src/components/tab/index.ts` | 固定标签页组件 | `IBimComponent` |
|
||||||
| `BimCollapse` | `src/components/collapse/index.ts` | 折叠面板组件 | `IBimComponent` |
|
| `BimCollapse` | `src/components/collapse/index.ts` | 折叠面板组件 | `IBimComponent` |
|
||||||
| `BimDescription` | `src/components/description/index.ts` | 描述列表组件 (Key-Value) | `IBimComponent` |
|
| `BimDescription` | `src/components/description/index.ts` | 描述列表组件 (Key-Value) | `IBimComponent` |
|
||||||
|
| `MeasurePanel` | `src/components/measure-panel/index.ts` | 测量面板组件(仅 UI) | `IBimComponent` |
|
||||||
|
|
||||||
### 4.3 服务类清单
|
### 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;
|
protected emit<K extends keyof EngineEvents>(event: K, payload: EngineEvents[K]): void;
|
||||||
private initContainer;
|
private initContainer;
|
||||||
/**
|
/**
|
||||||
* 设置事件拦截,防止事件<EFBFBD><EFBFBD>泡到下层元素(如 3D 引擎)
|
* 设置事件拦截,防止事件冒泡到下层元素(如 3D 引擎)
|
||||||
*/
|
*/
|
||||||
private setupEventInterception;
|
private setupEventInterception;
|
||||||
private updatePosition;
|
private updatePosition;
|
||||||
@@ -45,6 +45,12 @@ declare class BimButtonGroup implements IBimComponent {
|
|||||||
render(): void;
|
render(): void;
|
||||||
private renderGroup;
|
private renderGroup;
|
||||||
private renderButton;
|
private renderButton;
|
||||||
|
/**
|
||||||
|
* 设置按钮的激活状态
|
||||||
|
* @param id 按钮 ID
|
||||||
|
* @param active 可选,如果不传则切换(toggle)当前状态
|
||||||
|
*/
|
||||||
|
setBtnActive(id: string, active?: boolean): void;
|
||||||
private handleClick;
|
private handleClick;
|
||||||
private handleMouseEnter;
|
private handleMouseEnter;
|
||||||
private handleMouseLeave;
|
private handleMouseLeave;
|
||||||
@@ -120,6 +126,21 @@ declare class BimDialog implements IBimComponent {
|
|||||||
* @param recenter 是否重新计算定位(例如保持居中),默认 true
|
* @param recenter 是否重新计算定位(例如保持居中),默认 true
|
||||||
*/
|
*/
|
||||||
fitWidth(recenter?: boolean): void;
|
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 {
|
export declare class BimEngine extends EventEmitter {
|
||||||
private container;
|
container: HTMLElement;
|
||||||
private wrapper;
|
private wrapper;
|
||||||
toolbar: ToolbarManager | null;
|
toolbar: ToolbarManager | null;
|
||||||
constructTreeBtn: ConstructTreeManagerBtn | null;
|
constructTreeBtn: ConstructTreeManagerBtn | null;
|
||||||
@@ -157,6 +178,7 @@ export declare class BimEngine extends EventEmitter {
|
|||||||
engine: EngineManager | null;
|
engine: EngineManager | null;
|
||||||
rightKey: RightKeyManager | null;
|
rightKey: RightKeyManager | null;
|
||||||
propertyPanel: PropertyPanelManager | null;
|
propertyPanel: PropertyPanelManager | null;
|
||||||
|
measure: MeasureDialogManager | null;
|
||||||
constructor(container: HTMLElement | string, options?: {
|
constructor(container: HTMLElement | string, options?: {
|
||||||
locale?: LocaleType;
|
locale?: LocaleType;
|
||||||
theme?: ThemeType;
|
theme?: ThemeType;
|
||||||
@@ -247,6 +269,7 @@ export declare interface ButtonConfig {
|
|||||||
label: string;
|
label: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
keepActive?: boolean;
|
keepActive?: boolean;
|
||||||
|
isActive?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
onClick?: (button: OptButton) => void;
|
onClick?: (button: OptButton) => void;
|
||||||
children?: ButtonConfig[];
|
children?: ButtonConfig[];
|
||||||
@@ -657,6 +680,98 @@ declare type Listener<T = any> = (payload: T) => void;
|
|||||||
*/
|
*/
|
||||||
declare type LocaleType = 'zh-CN' | 'en-US';
|
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;
|
addButton(config: ButtonConfig): void;
|
||||||
setButtonVisibility(id: string, v: boolean): void;
|
setButtonVisibility(id: string, v: boolean): void;
|
||||||
setShowLabel(show: boolean): void;
|
setShowLabel(show: boolean): void;
|
||||||
|
setBtnActive(id: string, active?: boolean): void;
|
||||||
setVisible(visible: boolean): void;
|
setVisible(visible: boolean): void;
|
||||||
setBackgroundColor(color: string): void;
|
setBackgroundColor(color: string): void;
|
||||||
setColors(colors: ButtonGroupColors): void;
|
setColors(colors: ButtonGroupColors): void;
|
||||||
|
|||||||
@@ -1191,3 +1191,4 @@ type ExpandDirection = 'up' | 'down' | 'left' | 'right';
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -105,6 +105,20 @@ constructor(options: DialogOptions)
|
|||||||
- 清空当前内容
|
- 清空当前内容
|
||||||
- 设置新内容(支持 HTML 字符串或 DOM 元素)
|
- 设置新内容(支持 HTML 字符串或 DOM 元素)
|
||||||
|
|
||||||
|
#### `fitHeight(recenter: boolean = false): void`
|
||||||
|
根据内容自动调整弹窗高度
|
||||||
|
|
||||||
|
**使用场景**:
|
||||||
|
- 弹窗内容存在“展开/收起”等高度变化的交互(例如测量面板展开后需要增高弹窗,避免遮挡底部操作按钮)
|
||||||
|
|
||||||
|
**参数**:
|
||||||
|
- `recenter`: 是否根据 `options.position` 重新计算定位(默认 `false`)
|
||||||
|
|
||||||
|
**行为**:
|
||||||
|
- 先将高度设置为 `auto`,获取自然高度
|
||||||
|
- 再将高度夹紧到 `[minHeight, containerHeight]` 范围内
|
||||||
|
- 默认不重置用户拖拽后的 `left/top`,仅做边界夹紧;若 `recenter=true` 则按 `position` 重新定位
|
||||||
|
|
||||||
#### `close(): void`
|
#### `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 './bim-engine.css';
|
||||||
import { ToolbarManager } from './managers/toolbar-manager';
|
import {ToolbarManager} from './managers/toolbar-manager';
|
||||||
import { ButtonGroupManager } from './managers/button-group-manager';
|
import {ButtonGroupManager} from './managers/button-group-manager';
|
||||||
import { DialogManager } from './managers/dialog-manager';
|
import {DialogManager} from './managers/dialog-manager';
|
||||||
import { EngineManager } from './managers/engine-manager';
|
import {EngineManager} from './managers/engine-manager';
|
||||||
import { RightKeyManager } from './managers/right-key-manager';
|
import {RightKeyManager} from './managers/right-key-manager';
|
||||||
import { ConstructTreeManagerBtn } from './managers/construct-tree-manager-btn';
|
import {ConstructTreeManagerBtn} from './managers/construct-tree-manager-btn';
|
||||||
import { PropertyPanelManager } from './managers/property-panel-manager';
|
import {PropertyPanelManager} from './managers/property-panel-manager';
|
||||||
import type { EngineOptions, ModelLoadOptions } from './components/engine';
|
import {MeasureDialogManager} from './managers/measure-dialog-manager';
|
||||||
import { localeManager } from './services/locale';
|
import type {EngineOptions, ModelLoadOptions} from './components/engine';
|
||||||
import { themeManager } from './services/theme';
|
import {localeManager} from './services/locale';
|
||||||
import type { LocaleType } from './locales/types';
|
import {themeManager} from './services/theme';
|
||||||
import type { ThemeType, ThemeConfig } from './themes/types';
|
import type {LocaleType} from './locales/types';
|
||||||
import { EventEmitter } from './core/event-emitter';
|
import type {ThemeType, ThemeConfig} from './themes/types';
|
||||||
import { EngineEvents } from './types/events';
|
import {EventEmitter} from './core/event-emitter';
|
||||||
|
import {EngineEvents} from './types/events';
|
||||||
|
|
||||||
export type { EngineOptions, ModelLoadOptions };
|
export type {EngineOptions, ModelLoadOptions};
|
||||||
|
|
||||||
export class BimEngine extends EventEmitter {
|
export class BimEngine extends EventEmitter {
|
||||||
private container: HTMLElement;
|
public container: HTMLElement;
|
||||||
private wrapper: HTMLElement | null = null;
|
private wrapper: HTMLElement | null = null;
|
||||||
|
|
||||||
public toolbar: ToolbarManager | null = null; // 底部专用
|
public toolbar: ToolbarManager | null = null; // 底部专用
|
||||||
@@ -27,6 +28,7 @@ export class BimEngine extends EventEmitter {
|
|||||||
public engine: EngineManager | null = null; // 3D 引擎管理器
|
public engine: EngineManager | null = null; // 3D 引擎管理器
|
||||||
public rightKey: RightKeyManager | null = null; // 右键菜单管理器
|
public rightKey: RightKeyManager | null = null; // 右键菜单管理器
|
||||||
public propertyPanel: PropertyPanelManager | null = null; // 属性面板 (演示 Collapse)
|
public propertyPanel: PropertyPanelManager | null = null; // 属性面板 (演示 Collapse)
|
||||||
|
public measure: MeasureDialogManager | null = null; // 测量面板
|
||||||
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -62,10 +64,21 @@ export class BimEngine extends EventEmitter {
|
|||||||
return super.on(event, listener);
|
return super.on(event, listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
public setLocale(locale: LocaleType) { localeManager.setLocale(locale); }
|
public setLocale(locale: LocaleType) {
|
||||||
public getLocale(): LocaleType { return localeManager.getLocale(); }
|
localeManager.setLocale(locale);
|
||||||
public setTheme(theme: 'dark' | 'light') { themeManager.setTheme(theme); }
|
}
|
||||||
public setCustomTheme(theme: ThemeConfig) { themeManager.setCustomTheme(theme); }
|
|
||||||
|
public getLocale(): LocaleType {
|
||||||
|
return localeManager.getLocale();
|
||||||
|
}
|
||||||
|
|
||||||
|
public setTheme(theme: 'dark' | 'light') {
|
||||||
|
themeManager.setTheme(theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
public setCustomTheme(theme: ThemeConfig) {
|
||||||
|
themeManager.setCustomTheme(theme);
|
||||||
|
}
|
||||||
|
|
||||||
private init() {
|
private init() {
|
||||||
this.container.innerHTML = '';
|
this.container.innerHTML = '';
|
||||||
@@ -81,6 +94,7 @@ export class BimEngine extends EventEmitter {
|
|||||||
this.rightKey = new RightKeyManager(this, this.wrapper);
|
this.rightKey = new RightKeyManager(this, this.wrapper);
|
||||||
this.constructTreeBtn = new ConstructTreeManagerBtn(this, this.wrapper);
|
this.constructTreeBtn = new ConstructTreeManagerBtn(this, this.wrapper);
|
||||||
this.propertyPanel = new PropertyPanelManager(this);
|
this.propertyPanel = new PropertyPanelManager(this);
|
||||||
|
this.measure = new MeasureDialogManager(this);
|
||||||
|
|
||||||
// 初始主题
|
// 初始主题
|
||||||
this.updateTheme(themeManager.getTheme());
|
this.updateTheme(themeManager.getTheme());
|
||||||
@@ -105,6 +119,7 @@ export class BimEngine extends EventEmitter {
|
|||||||
this.dialog?.destroy();
|
this.dialog?.destroy();
|
||||||
this.rightKey?.destroy();
|
this.rightKey?.destroy();
|
||||||
this.propertyPanel?.destroy();
|
this.propertyPanel?.destroy();
|
||||||
|
this.measure?.destroy();
|
||||||
this.container.innerHTML = '';
|
this.container.innerHTML = '';
|
||||||
this.clear();
|
this.clear();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,13 +94,13 @@ export class BimButtonGroup implements IBimComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.updatePosition();
|
this.updatePosition();
|
||||||
|
|
||||||
// 添加事件拦截,防止点击穿透到 3D 引擎
|
// 添加事件拦截,防止点击穿透到 3D 引擎
|
||||||
this.setupEventInterception(this.container);
|
this.setupEventInterception(this.container);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置事件拦截,防止事件<EFBFBD><EFBFBD>泡到下层元素(如 3D 引擎)
|
* 设置事件拦截,防止事件冒泡到下层元素(如 3D 引擎)
|
||||||
*/
|
*/
|
||||||
private setupEventInterception(el: HTMLElement): void {
|
private setupEventInterception(el: HTMLElement): void {
|
||||||
const stopPropagation = (e: Event) => {
|
const stopPropagation = (e: Event) => {
|
||||||
@@ -319,6 +319,11 @@ export class BimButtonGroup implements IBimComponent {
|
|||||||
const btnEl = document.createElement('div');
|
const btnEl = document.createElement('div');
|
||||||
btnEl.className = 'opt-btn';
|
btnEl.className = 'opt-btn';
|
||||||
|
|
||||||
|
// 初始化时根据 button 自身的属性同步 active 状态
|
||||||
|
if (button.isActive) {
|
||||||
|
this.activeBtnIds.add(button.id);
|
||||||
|
}
|
||||||
|
|
||||||
// 按钮优先使用自己的 align,否则使用全局配置,默认为 vertical
|
// 按钮优先使用自己的 align,否则使用全局配置,默认为 vertical
|
||||||
const align = button.align || this.options.align || 'vertical';
|
const align = button.align || this.options.align || 'vertical';
|
||||||
if (align === 'horizontal') {
|
if (align === 'horizontal') {
|
||||||
@@ -380,14 +385,34 @@ export class BimButtonGroup implements IBimComponent {
|
|||||||
return wrapper;
|
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 {
|
private handleClick(button: OptButton): void {
|
||||||
if (button.disabled) return;
|
if (button.disabled) return;
|
||||||
if (!button.children || button.children.length === 0) {
|
if (!button.children || button.children.length === 0) {
|
||||||
if (button.keepActive) {
|
if (button.keepActive) {
|
||||||
const wasActive = this.activeBtnIds.has(button.id);
|
this.setBtnActive(button.id);
|
||||||
if (wasActive) this.activeBtnIds.delete(button.id);
|
|
||||||
else this.activeBtnIds.add(button.id);
|
|
||||||
this.updateButtonState(button.id);
|
|
||||||
}
|
}
|
||||||
this.closeDropdown();
|
this.closeDropdown();
|
||||||
if (button.onClick) button.onClick(button);
|
if (button.onClick) button.onClick(button);
|
||||||
@@ -428,7 +453,7 @@ export class BimButtonGroup implements IBimComponent {
|
|||||||
|
|
||||||
// 先添加到 DOM 以便计算尺寸
|
// 先添加到 DOM 以便计算尺寸
|
||||||
document.body.appendChild(dropdown);
|
document.body.appendChild(dropdown);
|
||||||
|
|
||||||
// 添加事件拦截
|
// 添加事件拦截
|
||||||
this.setupEventInterception(dropdown);
|
this.setupEventInterception(dropdown);
|
||||||
|
|
||||||
@@ -518,16 +543,22 @@ export class BimButtonGroup implements IBimComponent {
|
|||||||
private updateButtonState(buttonId: string): void {
|
private updateButtonState(buttonId: string): void {
|
||||||
const btnEl = this.btnRefs.get(buttonId);
|
const btnEl = this.btnRefs.get(buttonId);
|
||||||
if (btnEl) {
|
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; }
|
private getIcon(icon?: string): string { return icon || this.DEFAULT_ICON; }
|
||||||
|
|
||||||
public updateButtonVisibility(id: string, visible: boolean): void {
|
public updateButtonVisibility(id: string, visible: boolean): void {
|
||||||
if (!this.options.visibility) this.options.visibility = {};
|
if (!this.options.visibility) this.options.visibility = {};
|
||||||
this.options.visibility[id] = visible;
|
this.options.visibility[id] = visible;
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
public setShowLabel(show: boolean): void {
|
public setShowLabel(show: boolean): void {
|
||||||
this.options.showLabel = show;
|
this.options.showLabel = show;
|
||||||
this.updateLabelsVisibility();
|
this.updateLabelsVisibility();
|
||||||
@@ -557,8 +588,10 @@ export class BimButtonGroup implements IBimComponent {
|
|||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
public setBackgroundColor(color: string): void { this.setColors({ backgroundColor: color }); }
|
public setBackgroundColor(color: string): void { this.setColors({ backgroundColor: color }); }
|
||||||
private isVisible(id: string): boolean { return this.options.visibility?.[id] !== false; }
|
private isVisible(id: string): boolean { return this.options.visibility?.[id] !== false; }
|
||||||
|
|
||||||
public destroy(): void {
|
public destroy(): void {
|
||||||
if (this.unsubscribeLocale) {
|
if (this.unsubscribeLocale) {
|
||||||
this.unsubscribeLocale();
|
this.unsubscribeLocale();
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export interface ButtonConfig {
|
|||||||
label: string;
|
label: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
keepActive?: boolean;
|
keepActive?: boolean;
|
||||||
|
isActive?:boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
onClick?: (button: OptButton) => void;
|
onClick?: (button: OptButton) => void;
|
||||||
children?: ButtonConfig[];
|
children?: ButtonConfig[];
|
||||||
@@ -43,10 +44,10 @@ export interface ButtonGroupColors {
|
|||||||
// --- 新增布局类型 ---
|
// --- 新增布局类型 ---
|
||||||
|
|
||||||
/** 弹窗/按钮组位置 */
|
/** 弹窗/按钮组位置 */
|
||||||
export type GroupPosition =
|
export type GroupPosition =
|
||||||
| 'center'
|
| 'center'
|
||||||
| 'top-left' | 'top-center' | 'top-right'
|
| 'top-left' | 'top-center' | 'top-right'
|
||||||
| 'left-center' | 'right-center'
|
| 'left-center' | 'right-center'
|
||||||
| 'bottom-left' | 'bottom-center' | 'bottom-right'
|
| 'bottom-left' | 'bottom-center' | 'bottom-right'
|
||||||
| { x: number; y: number }
|
| { x: number; y: number }
|
||||||
| 'static'; // static 表示不绝对定位,随文档流
|
| 'static'; // static 表示不绝对定位,随文档流
|
||||||
@@ -62,16 +63,16 @@ export type ExpandDirection = 'up' | 'down' | 'left' | 'right';
|
|||||||
|
|
||||||
export interface ButtonGroupOptions extends ButtonGroupColors {
|
export interface ButtonGroupOptions extends ButtonGroupColors {
|
||||||
container: HTMLElement | string;
|
container: HTMLElement | string;
|
||||||
|
|
||||||
/** 屏幕位置 (如 top-left) */
|
/** 屏幕位置 (如 top-left) */
|
||||||
position?: GroupPosition;
|
position?: GroupPosition;
|
||||||
|
|
||||||
/** 按钮组排列方向 (默认 row) */
|
/** 按钮组排列方向 (默认 row) */
|
||||||
direction?: GroupDirection;
|
direction?: GroupDirection;
|
||||||
|
|
||||||
/** 按钮内部图标文字排列 (默认 vertical) */
|
/** 按钮内部图标文字排列 (默认 vertical) */
|
||||||
align?: ButtonAlign;
|
align?: ButtonAlign;
|
||||||
|
|
||||||
/** 菜单展开方向 */
|
/** 菜单展开方向 */
|
||||||
expand?: ExpandDirection;
|
expand?: ExpandDirection;
|
||||||
|
|
||||||
@@ -84,4 +85,4 @@ export interface ClickPayload {
|
|||||||
button: OptButton;
|
button: OptButton;
|
||||||
action: 'activate' | 'deactivate' | 'trigger';
|
action: 'activate' | 'deactivate' | 'trigger';
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
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 { walkBirdButton } = await import('./buttons/walk/walk-bird');
|
||||||
const { settingButton } = await import('./buttons/setting');
|
const { settingButton } = await import('./buttons/setting');
|
||||||
const { infoButton } = await import('./buttons/info');
|
const { infoButton } = await import('./buttons/info');
|
||||||
|
const { createMeasureButton } = await import('./buttons/measure');
|
||||||
|
|
||||||
this.addGroup('group-1');
|
this.addGroup('group-1');
|
||||||
|
|
||||||
// 使用工厂函数创建按钮,并注入 engine
|
// 使用工厂函数创建按钮,并注入 engine
|
||||||
if (this.engine) {
|
if (this.engine) {
|
||||||
this.addButton(createHomeButton(this.engine));
|
this.addButton(createHomeButton(this.engine));
|
||||||
|
this.addButton(createMeasureButton(this.engine));
|
||||||
} else {
|
} else {
|
||||||
console.warn('[Toolbar] Engine not available when creating buttons.');
|
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 = {
|
export const enUS: TranslationDictionary = {
|
||||||
common: {
|
common: {
|
||||||
title: 'BimEngine',
|
title: 'BimEngine',
|
||||||
description: 'This is a BIM-ENGINE demo.',
|
description: 'This is a BIM-ENGINE demo.',
|
||||||
openTestDialog: 'Open Test Dialog',
|
openTestDialog: 'Open Test Dialog',
|
||||||
openInfoDialog: 'Open Info Dialog (Wrapped)',
|
openInfoDialog: 'Open Info Dialog (Wrapped)',
|
||||||
},
|
},
|
||||||
toolbar: {
|
toolbar: {
|
||||||
home: 'Home',
|
home: 'Home',
|
||||||
info: 'Info',
|
measure: 'Measure',
|
||||||
location: 'Location',
|
info: 'Info',
|
||||||
setting: 'Settings',
|
location: 'Location',
|
||||||
walk: 'Walk',
|
setting: 'Settings',
|
||||||
walkPerson: 'Person',
|
walk: 'Walk',
|
||||||
walkBird: 'Bird Eye',
|
walkPerson: 'Person',
|
||||||
walkMenu: 'Menu',
|
walkBird: 'Bird Eye',
|
||||||
tree: 'Tree',
|
walkMenu: 'Menu',
|
||||||
},
|
tree: 'Tree',
|
||||||
dialog: {
|
},
|
||||||
testTitle: 'Test Dialog',
|
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>',
|
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',
|
menu: {
|
||||||
home: 'Home',
|
info: 'Info',
|
||||||
},
|
home: 'Home',
|
||||||
tree: {
|
},
|
||||||
searchPlaceholder: 'Please enter content to search',
|
tree: {
|
||||||
},
|
searchPlaceholder: 'Please enter content to search',
|
||||||
|
},
|
||||||
constructTree: {
|
constructTree: {
|
||||||
title: 'Construct Tree',
|
title: 'Construct Tree',
|
||||||
},
|
},
|
||||||
tab: {
|
tab: {
|
||||||
component: 'Component',
|
component: 'Component',
|
||||||
system: 'System',
|
system: 'System',
|
||||||
space: 'Space',
|
space: 'Space',
|
||||||
},
|
},
|
||||||
panel: {
|
panel: {
|
||||||
property: {
|
property: {
|
||||||
title: 'Component Details',
|
title: 'Component Details',
|
||||||
base: 'Basic Info',
|
base: 'Basic Info',
|
||||||
material: 'Material',
|
material: 'Material',
|
||||||
advanced: 'Advanced',
|
advanced: 'Advanced',
|
||||||
tab: {
|
tab: {
|
||||||
props: 'Properties',
|
props: 'Properties',
|
||||||
material: 'Material'
|
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: {
|
toolbar: {
|
||||||
home: string;
|
home: string;
|
||||||
|
measure: string;
|
||||||
info: string;
|
info: string;
|
||||||
location: string;
|
location: string;
|
||||||
setting: string;
|
setting: string;
|
||||||
@@ -51,9 +53,67 @@ export interface TranslationDictionary {
|
|||||||
system: string;
|
system: string;
|
||||||
space: 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;
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 语言<E8AFAD><E8A880>码类型
|
* 语言<E8AFAD><E8A880>码类型
|
||||||
*/
|
*/
|
||||||
export type LocaleType = 'zh-CN' | 'en-US';
|
export type LocaleType = 'zh-CN' | 'en-US';
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export const zhCN: TranslationDictionary = {
|
|||||||
},
|
},
|
||||||
toolbar: {
|
toolbar: {
|
||||||
home: '首页',
|
home: '首页',
|
||||||
|
measure: '测量',
|
||||||
info: '信息',
|
info: '信息',
|
||||||
location: '定位',
|
location: '定位',
|
||||||
setting: '设置',
|
setting: '设置',
|
||||||
@@ -24,7 +25,7 @@ export const zhCN: TranslationDictionary = {
|
|||||||
},
|
},
|
||||||
menu: {
|
menu: {
|
||||||
info: '信息',
|
info: '信息',
|
||||||
home: '首页',
|
home: '首页'
|
||||||
},
|
},
|
||||||
tree: {
|
tree: {
|
||||||
searchPlaceholder: '请输入要搜索的内容',
|
searchPlaceholder: '请输入要搜索的内容',
|
||||||
@@ -48,5 +49,47 @@ export const zhCN: TranslationDictionary = {
|
|||||||
material: '材质'
|
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: '%',
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -85,4 +85,4 @@ export class DialogManager extends BimComponent {
|
|||||||
this.activeDialogs.forEach(d => d.destroy());
|
this.activeDialogs.forEach(d => d.destroy());
|
||||||
this.activeDialogs = [];
|
this.activeDialogs = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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 addButton(config: ButtonConfig) { this.toolbar?.addButton(config); this.toolbar?.render(); }
|
||||||
public setButtonVisibility(id: string, v: boolean) { this.toolbar?.updateButtonVisibility(id, v); }
|
public setButtonVisibility(id: string, v: boolean) { this.toolbar?.updateButtonVisibility(id, v); }
|
||||||
public setShowLabel(show: boolean) { this.toolbar?.setShowLabel(show); }
|
public setShowLabel(show: boolean) { this.toolbar?.setShowLabel(show); }
|
||||||
|
public setBtnActive(id: string, active?: boolean) { this.toolbar?.setBtnActive(id, active); }
|
||||||
public setVisible(visible: boolean) {
|
public setVisible(visible: boolean) {
|
||||||
if (this.toolbarContainer) {
|
if (this.toolbarContainer) {
|
||||||
this.toolbarContainer.style.visibility = visible ? 'visible' : 'hidden';
|
this.toolbarContainer.style.visibility = visible ? 'visible' : 'hidden';
|
||||||
|
|||||||
Reference in New Issue
Block a user