增加测量窗口

This commit is contained in:
yuding
2025-12-22 18:48:38 +08:00
parent e1bb5558ff
commit 7d522afb70
25 changed files with 4625 additions and 2403 deletions

View File

@@ -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 服务类清单

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

120
dist/index.d.ts vendored
View File

@@ -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;
/** 体积单位 */
volumeM3?: number;
/** 激光测距单位mm */
laserDistanceMm?: number;
/** 坡度(单位:% */
slopePercent?: number;
/** 空间体积单位 */
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;

View File

@@ -1191,3 +1191,4 @@ type ExpandDirection = 'up' | 'down' | 'left' | 'right';

View File

@@ -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 {

View File

@@ -609,3 +609,4 @@ interface ModelLoadOptions {

View 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`

View File

@@ -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();
}

View File

@@ -94,13 +94,13 @@ export class BimButtonGroup implements IBimComponent {
}
this.updatePosition();
// 添加事件拦截,防止点击穿透到 3D 引擎
this.setupEventInterception(this.container);
}
/**
* 设置事件拦截,防止事件<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);
@@ -428,7 +453,7 @@ export class BimButtonGroup implements IBimComponent {
// 先添加到 DOM 以便计算尺寸
document.body.appendChild(dropdown);
// 添加事件拦截
this.setupEventInterception(dropdown);
@@ -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();

View File

@@ -7,6 +7,7 @@ export interface ButtonConfig {
label: string;
icon?: string;
keepActive?: boolean;
isActive?:boolean;
disabled?: boolean;
onClick?: (button: OptButton) => void;
children?: ButtonConfig[];
@@ -43,10 +44,10 @@ export interface ButtonGroupColors {
// --- 新增布局类型 ---
/** 弹窗/按钮组位置 */
export type GroupPosition =
| 'center'
| 'top-left' | 'top-center' | 'top-right'
| 'left-center' | 'right-center'
export type GroupPosition =
| 'center'
| 'top-left' | 'top-center' | 'top-right'
| 'left-center' | 'right-center'
| 'bottom-left' | 'bottom-center' | 'bottom-right'
| { x: number; y: number }
| 'static'; // static 表示不绝对定位,随文档流
@@ -62,16 +63,16 @@ export type ExpandDirection = 'up' | 'down' | 'left' | 'right';
export interface ButtonGroupOptions extends ButtonGroupColors {
container: HTMLElement | string;
/** 屏幕位置 (如 top-left) */
position?: GroupPosition;
/** 按钮组排列方向 (默认 row) */
direction?: GroupDirection;
/** 按钮内部图标文字排列 (默认 vertical) */
align?: ButtonAlign;
/** 菜单展开方向 */
expand?: ExpandDirection;
@@ -84,4 +85,4 @@ export interface ClickPayload {
button: OptButton;
action: 'activate' | 'deactivate' | 'trigger';
isActive?: boolean;
}
}

View 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()
}
}
};
};

View File

@@ -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.');
}

View File

@@ -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`;
}
/**
* 初始化弹窗位置
*/

View 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);
}

View 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, '');
}
}

View 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;
/** 体积单位 */
volumeM3?: number;
/** 激光测距单位mm */
laserDistanceMm?: number;
/** 坡度(单位:% */
slopePercent?: number;
/** 空间体积单位 */
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;
}

View File

@@ -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: '%',
}
}
};

View File

@@ -1,3 +1,4 @@
/**
* 翻译字典接口
* 定义所有可用的翻译键值对结构,保证类型安全
@@ -11,6 +12,7 @@ export interface TranslationDictionary {
};
toolbar: {
home: string;
measure: string;
info: string;
location: string;
setting: string;
@@ -51,9 +53,67 @@ 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;
};
}
}
/**
* 语言<E8AFAD><E8A880>码类型
*/
export type LocaleType = 'zh-CN' | 'en-US';
export type LocaleType = 'zh-CN' | 'en-US';

View File

@@ -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: '%',
}
}
};
};

View File

@@ -85,4 +85,4 @@ export class DialogManager extends BimComponent {
this.activeDialogs.forEach(d => d.destroy());
this.activeDialogs = [];
}
}
}

View 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;
}
}
}

View File

@@ -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';