初始化

This commit is contained in:
yuding
2025-12-24 19:02:34 +08:00
parent 4b5eb78bbb
commit 04a5e74284
51 changed files with 8576 additions and 5334 deletions

33
.idea/workspace.xml generated
View File

@@ -4,19 +4,22 @@
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="728b1ce9-7308-4507-bebd-62399c54bf21" name="更改" comment="添加折叠面板">
<list default="true" id="728b1ce9-7308-4507-bebd-62399c54bf21" name="更改" comment="增加测量窗口">
<change beforePath="$PROJECT_DIR$/dist/bim-engine-sdk.es.js" beforeDir="false" afterPath="$PROJECT_DIR$/dist/bim-engine-sdk.es.js" afterDir="false" />
<change beforePath="$PROJECT_DIR$/dist/bim-engine-sdk.es.js.map" beforeDir="false" afterPath="$PROJECT_DIR$/dist/bim-engine-sdk.es.js.map" afterDir="false" />
<change beforePath="$PROJECT_DIR$/dist/bim-engine-sdk.umd.js" beforeDir="false" afterPath="$PROJECT_DIR$/dist/bim-engine-sdk.umd.js" afterDir="false" />
<change beforePath="$PROJECT_DIR$/dist/bim-engine-sdk.umd.js.map" beforeDir="false" afterPath="$PROJECT_DIR$/dist/bim-engine-sdk.umd.js.map" afterDir="false" />
<change beforePath="$PROJECT_DIR$/dist/index.d.ts" beforeDir="false" afterPath="$PROJECT_DIR$/dist/index.d.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/components/collapse/index.css" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/collapse/index.css" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/components/tab/index.css" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/tab/index.css" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/index.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/index.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/docs/components/button-group.md" beforeDir="false" afterPath="$PROJECT_DIR$/docs/components/button-group.md" afterDir="false" />
<change beforePath="$PROJECT_DIR$/docs/components/engine.md" beforeDir="false" afterPath="$PROJECT_DIR$/docs/components/engine.md" afterDir="false" />
<change beforePath="$PROJECT_DIR$/docs/components/measure-panel.md" beforeDir="false" afterPath="$PROJECT_DIR$/docs/components/measure-panel.md" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/components/measure-panel/index.css" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/measure-panel/index.css" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/components/measure-panel/index.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/measure-panel/index.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/components/measure-panel/types.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/measure-panel/types.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/locales/en-US.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/locales/en-US.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/locales/types.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/locales/types.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/locales/zh-CN.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/locales/zh-CN.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/managers/property-panel-manager.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/managers/property-panel-manager.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/managers/measure-dialog-manager.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/managers/measure-dialog-manager.ts" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -44,7 +47,7 @@
&quot;git-widget-placeholder&quot;: &quot;main&quot;,
&quot;go.import.settings.migrated&quot;: &quot;true&quot;,
&quot;kotlin-language-version-configured&quot;: &quot;true&quot;,
&quot;last_opened_file_path&quot;: &quot;/Users/yuding/WORK/LYZ/project/bimEngine/engine/src/managers&quot;,
&quot;last_opened_file_path&quot;: &quot;/Users/yuding/WORK/LYZ/project/bimEngine/engine/src/components/button-group/toolbar/buttons/rule&quot;,
&quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
&quot;node.js.detected.package.tslint&quot;: &quot;true&quot;,
&quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
@@ -57,6 +60,7 @@
}</component>
<component name="RecentsManager">
<key name="CopyFile.RECENT_KEYS">
<recent name="$PROJECT_DIR$/src/components/button-group/toolbar/buttons/rule" />
<recent name="$PROJECT_DIR$/src/managers" />
</key>
</component>
@@ -90,7 +94,9 @@
<workItem from="1765943119364" duration="2464000" />
<workItem from="1766108012524" duration="1315000" />
<workItem from="1766371049964" duration="671000" />
<workItem from="1766385054791" duration="7532000" />
<workItem from="1766385054791" duration="15099000" />
<workItem from="1766453884590" duration="6000" />
<workItem from="1766454569112" duration="3202000" />
</task>
<task id="LOCAL-00001" summary="添加测试信息">
<option name="closed" value="true" />
@@ -140,7 +146,15 @@
<option name="project" value="LOCAL" />
<updated>1766389199604</updated>
</task>
<option name="localTasksCounter" value="7" />
<task id="LOCAL-00007" summary="增加测量窗口">
<option name="closed" value="true" />
<created>1766400520027</created>
<option name="number" value="00007" />
<option name="presentableId" value="LOCAL-00007" />
<option name="project" value="LOCAL" />
<updated>1766400520027</updated>
</task>
<option name="localTasksCounter" value="8" />
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
@@ -150,7 +164,8 @@
<MESSAGE value="添加测试信息" />
<MESSAGE value="修改" />
<MESSAGE value="添加折叠面板" />
<option name="LAST_COMMIT_MESSAGE" value="添加折叠面板" />
<MESSAGE value="增加测量窗口" />
<option name="LAST_COMMIT_MESSAGE" value="增加测量窗口" />
</component>
<component name="VgoProject">
<settings-migrated>true</settings-migrated>

View File

@@ -303,6 +303,7 @@ interface IBimComponent {
- **`toolbar/index.ts`**: `Toolbar` 类 - 底部工具栏(继承 `BimButtonGroup`
- **`toolbar/buttons/`**: 工具栏按钮配置
- `home/`: 首页按钮
- `zoom-box/`: 选框放大按钮(占位,事件暂未实现)
- `info/`: 信息按钮
- `location/`: 定位按钮
- `setting/`: 设置按钮
@@ -512,39 +513,39 @@ const dialog = engine.dialog.create({
### 4.1 Manager 类清单
| 类名 | 文件路径 | 功能 | 继承关系 |
|------|---------|------|---------|
| `DialogManager` | `src/managers/dialog-manager.ts` | 管理弹窗实例 | `BimComponent` |
| `ToolbarManager` | `src/managers/toolbar-manager.ts` | 管理底部工具栏 | `BimComponent` |
| `ButtonGroupManager` | `src/managers/button-group-manager.ts` | 管理通用按钮组 | `BimComponent` |
| `EngineManager` | `src/managers/engine-manager.ts` | 管理 3D 引擎 | `BimComponent` |
| `RightKeyManager` | `src/managers/right-key-manager.ts` | 管理右键菜单 (Context Menu) | `BimComponent` |
| `ModelTreeManager` | `src/managers/model-tree-manager.ts` | 模型树业务管理器 | `BimComponent` |
| 类名 | 文件路径 | 功能 | 继承关系 |
| ---------------------- | ---------------------------------------- | ---------------------------------- | -------------- |
| `DialogManager` | `src/managers/dialog-manager.ts` | 管理弹窗实例 | `BimComponent` |
| `ToolbarManager` | `src/managers/toolbar-manager.ts` | 管理底部工具栏 | `BimComponent` |
| `ButtonGroupManager` | `src/managers/button-group-manager.ts` | 管理通用按钮组 | `BimComponent` |
| `EngineManager` | `src/managers/engine-manager.ts` | 管理 3D 引擎 | `BimComponent` |
| `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` |
| `MeasureDialogManager` | `src/managers/measure-dialog-manager.ts` | 测量弹窗管理器 | `BimComponent` |
### 4.2 组件类清单
| 类名 | 文件路径 | 功能 | 实现接口 |
|------|---------|------|---------|
| `BimDialog` | `src/components/dialog/index.ts` | 通用弹窗组件 | `IBimComponent` |
| `BimInfoDialog` | `src/components/dialog/bimInfoDialog/index.ts` | 信息弹窗组件 | 继承 `BimDialog` |
| `BimButtonGroup` | `src/components/button-group/index.ts` | 通用按钮组组件 | `IBimComponent` |
| `Toolbar` | `src/components/button-group/toolbar/index.ts` | 底部工具栏组件 | 继承 `BimButtonGroup` |
| `Engine` | `src/components/engine/index.ts` | 3D 引擎组件 | `IBimComponent` |
| `BimRightKey` | `src/components/right-key/index.ts` | 右键浮层容器 | `IBimComponent` |
| `BimMenu` | `src/components/menu/index.ts` | 通用菜单列表 | `IBimComponent` |
| `BimTree` | `src/components/tree/index.ts` | 通用树形组件 | `IBimComponent` |
| `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` |
| 类名 | 文件路径 | 功能 | 实现接口 |
| ---------------- | ---------------------------------------------- | ------------------------ | --------------------- |
| `BimDialog` | `src/components/dialog/index.ts` | 通用弹窗组件 | `IBimComponent` |
| `BimInfoDialog` | `src/components/dialog/bimInfoDialog/index.ts` | 信息弹窗组件 | 继承 `BimDialog` |
| `BimButtonGroup` | `src/components/button-group/index.ts` | 通用按钮组组件 | `IBimComponent` |
| `Toolbar` | `src/components/button-group/toolbar/index.ts` | 底部工具栏组件 | 继承 `BimButtonGroup` |
| `Engine` | `src/components/engine/index.ts` | 3D 引擎组件 | `IBimComponent` |
| `BimRightKey` | `src/components/right-key/index.ts` | 右键浮层容器 | `IBimComponent` |
| `BimMenu` | `src/components/menu/index.ts` | 通用菜单列表 | `IBimComponent` |
| `BimTree` | `src/components/tree/index.ts` | 通用树形组件 | `IBimComponent` |
| `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 服务类清单
| 类名 | 文件路径 | 功能 | 模式 |
|------|---------|------|------|
| `ThemeManager` | `src/services/theme.ts` | 主题管理 | 单例 |
| 类名 | 文件路径 | 功能 | 模式 |
| --------------- | ------------------------ | -------- | ---- |
| `ThemeManager` | `src/services/theme.ts` | 主题管理 | 单例 |
| `LocaleManager` | `src/services/locale.ts` | 语言管理 | 单例 |
### 4.4 事件总线定义
@@ -644,11 +645,11 @@ this.on('ui:open-dialog', (payload) => {
### 4.5 核心基类
| 类名 | 文件路径 | 功能 | 继承/实现 |
|------|---------|------|----------|
| `EventEmitter` | `src/core/event-emitter.ts` | 事件总线基础类 | - |
| `BimComponent` | `src/core/component.ts` | 组件基类 | 抽象类 |
| `BimEngine` | `src/bim-engine.ts` | 主引擎类 | 继承 `EventEmitter` |
| 类名 | 文件路径 | 功能 | 继承/实现 |
| -------------- | --------------------------- | -------------- | ------------------- |
| `EventEmitter` | `src/core/event-emitter.ts` | 事件总线基础类 | - |
| `BimComponent` | `src/core/component.ts` | 组件基类 | 抽象类 |
| `BimEngine` | `src/bim-engine.ts` | 主引擎类 | 继承 `EventEmitter` |
---
@@ -1334,8 +1335,8 @@ function example(param1: string, param2: number): boolean {
## 📝 文档维护记录
| 日期 | 修改内容 | 修改人 |
|------|---------|--------|
| 日期 | 修改内容 | 修改人 |
| ---------- | -------- | ------------ |
| 2024-XX-XX | 初始创建 | AI Assistant |
---

10741
dist/bim-engine-sdk.es.js vendored

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

165
dist/index.d.ts vendored
View File

@@ -27,6 +27,10 @@ declare class BimButtonGroup implements IBimComponent {
* 应用样式到容器
*/
private applyStyles;
/**
* 设置主题的primary颜色用于边框等
*/
private setPrimaryColor;
/**
* 设置主题颜色
* 只会应用到没有被用户自定义的颜色属性上
@@ -52,6 +56,11 @@ declare class BimButtonGroup implements IBimComponent {
*/
setBtnActive(id: string, active?: boolean): void;
private handleClick;
/**
* 互斥关闭同范围内的其它已激活按钮,并触发它们的 onClick
* @param button 当前被激活的按钮
*/
private deactivateExclusiveSiblings;
private handleMouseEnter;
private handleMouseLeave;
private showDropdown;
@@ -179,6 +188,9 @@ export declare class BimEngine extends EventEmitter {
rightKey: RightKeyManager | null;
propertyPanel: PropertyPanelManager | null;
measure: MeasureDialogManager | null;
sectionPlane: SectionPlaneDialogManager | null;
sectionAxis: SectionAxisDialogManager | null;
sectionBox: SectionBoxDialogManager | null;
constructor(container: HTMLElement | string, options?: {
locale?: LocaleType;
theme?: ThemeType;
@@ -269,6 +281,18 @@ export declare interface ButtonConfig {
label: string;
icon?: string;
keepActive?: boolean;
/**
* 是否互斥(开关互斥)
*
* 行为说明:
* - 当按钮从“未激活”切换到“激活”时,如果该按钮开启了 exclusive
* 会自动关闭同互斥范围内的其它已激活按钮,并触发它们的 onClick用于执行关闭逻辑
* - 一级按钮:互斥范围 = 同 groupId 下的一级按钮
* - 二级按钮:互斥范围 = 同 groupId 且同 parentId 下的二级按钮
*
* 注意:该能力通常与 keepActive 搭配使用。
*/
exclusive?: boolean;
isActive?: boolean;
disabled?: boolean;
onClick?: (button: OptButton) => void;
@@ -717,10 +741,11 @@ declare class MeasureDialogManager extends BimComponent {
*/
switchMode(mode: MeasureMode): void;
/**
* 设置测量结果(由外部注入,仅用于显示
* 设置测量结果(推荐使用的新方法名
* 说明:内部直接调用 MeasurePanel.setResult()
* @param result 测量结果;传 null 表示清空
*/
setResult(result: MeasureResult | null): void;
setMeasureResult(result: MeasureResult | null): void;
/**
* 获取测量配置(单位/精度)
* - 如果面板存在:返回面板当前配置
@@ -914,6 +939,142 @@ declare class RightKeyManager extends BimComponent {
private handleContextMenu;
}
/**
* 轴向类型
*/
declare type SectionAxis = 'x' | 'y' | 'z';
/**
* 轴向剖切弹窗管理器
*/
declare class SectionAxisDialogManager extends BimComponent {
private dialogId;
private dialog;
private panel;
constructor(engine: BimEngine);
init(): void;
/**
* 显示弹窗
*/
show(): void;
/**
* 隐藏弹窗
*/
hide(): void;
/**
* 获取隐藏状态
*/
getHiddenState(): boolean;
/**
* 设置隐藏状态
*/
setHiddenState(isHidden: boolean): void;
/**
* 获取当前激活的轴向
*/
getActiveAxis(): SectionAxis;
/**
* 设置激活的轴向
*/
setActiveAxis(axis: SectionAxis): void;
/**
* 销毁弹窗和面板
*/
destroy(): void;
}
/**
* 剖切盒轴向范围
*/
declare interface SectionBoxAxisRange {
/** 最小值0-100的百分比 */
min: number;
/** 最大值0-100的百分比 */
max: number;
}
/**
* 剖切盒弹窗管理器
*/
declare class SectionBoxDialogManager extends BimComponent {
private dialogId;
private dialog;
private panel;
constructor(engine: BimEngine);
init(): void;
/**
* 显示弹窗
*/
show(): void;
/**
* 隐藏弹窗
*/
hide(): void;
/**
* 获取隐藏状态
*/
getHiddenState(): boolean;
/**
* 设置隐藏状态
*/
setHiddenState(isHidden: boolean): void;
/**
* 获取反向状态
*/
getReversedState(): boolean;
/**
* 设置反向状态
*/
setReversedState(isReversed: boolean): void;
/**
* 获取范围值
*/
getRange(): SectionBoxRange | null;
/**
* 设置范围值
*/
setRange(range: Partial<SectionBoxRange>): void;
/**
* 销毁弹窗和面板
*/
destroy(): void;
}
/**
* 剖切盒范围数据
*/
declare interface SectionBoxRange {
/** X轴范围 */
x: SectionBoxAxisRange;
/** Y轴范围 */
y: SectionBoxAxisRange;
/** Z轴范围 */
z: SectionBoxAxisRange;
}
/**
* 拾取面剖切弹窗管理器
*/
declare class SectionPlaneDialogManager extends BimComponent {
private dialogId;
private dialog;
private panel;
constructor(engine: BimEngine);
init(): void;
/**
* 显示拾取面剖切弹窗
*/
show(): void;
/**
* 隐藏弹窗
*/
hide(): void;
/**
* 销毁弹窗
*/
destroy(): void;
}
/**
* 全局主题配置接口
* 定义系统通用的语义化颜色

View File

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

View File

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

View File

@@ -0,0 +1,13 @@
## 图标资源目录说明
该目录用于存放项目内使用的 **SVG 图标资源**(原始文件)。
### 命名建议
- 使用小写短横线:例如 `zoom-box.svg``measure-distance.svg`
- 尽量语义化,避免 `icon1.svg` 这类不可读命名
### 使用建议
- **优先保留原始 SVG**,便于后续替换/优化
- 组件里如果需要内联 SVG可将文件内容复制到 `icon` 字符串中,或后续扩展为统一的图标加载器

View File

@@ -0,0 +1,20 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32" viewBox="0 0 32 32">
<defs>
<style>
.cls-1 {
clip-path: url(#clip-体积);
}
.cls-2 {
fill: #fff;
}
</style>
<clipPath id="clip-体积">
<rect width="32" height="32"/>
</clipPath>
</defs>
<g id="体积" class="cls-1">
<rect class="cls-2" width="32" height="32"/>
<path id="路径_10" data-name="路径 10" d="M94.74,86.658V71.189a.371.371,0,0,1,.2-.329l13.869-7.22a.371.371,0,0,1,.344,0l13.053,6.891h0l.819.431a.371.371,0,0,1,.2.328v15.3a.371.371,0,0,1-.2.328l-13.872,7.255a.371.371,0,0,1-.342,0L94.94,86.987a.371.371,0,0,1-.2-.329Zm2.119-.837,11.2,5.8a.024.024,0,0,0,.035-.022V79.483a.371.371,0,0,0-.2-.328l-11.2-5.909a.024.024,0,0,0-.035.021V85.492A.371.371,0,0,0,96.859,85.821Zm13.151-6.459v12a.12.12,0,0,0,.176.106L114,89.474l3.334-1.745,3.771-1.978a.371.371,0,0,0,.2-.328V73.5a.193.193,0,0,0-.284-.171l-10.812,5.708A.371.371,0,0,0,110.01,79.362ZM97.925,71.725l10.839,5.72a.371.371,0,0,0,.346,0L119.8,71.808a.214.214,0,0,0,0-.378l-10.649-5.621a.371.371,0,0,0-.344,0L97.925,71.47A.144.144,0,0,0,97.925,71.725Z" transform="translate(-92.982 -62.907)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,20 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32" viewBox="0 0 32 32">
<defs>
<style>
.cls-1 {
clip-path: url(#clip-坡度);
}
.cls-2 {
fill: #fff;
}
</style>
<clipPath id="clip-坡度">
<rect width="32" height="32"/>
</clipPath>
</defs>
<g id="坡度" class="cls-1">
<rect class="cls-2" width="32" height="32"/>
<path id="路径_15" data-name="路径 15" d="M202.1,188.337l2.629-2.191-8.447-3.106,1.533,8.871,2.629-2.194,9.341,11.209,1.656-1.379Zm-13.726-.435a1.075,1.075,0,0,0-1.07-.341,1.057,1.057,0,0,0-.5.277l-5.11,4.08a1.08,1.08,0,0,0-.406.84l-.007,17.386a1.079,1.079,0,0,0,1.077,1.077L205.7,211.2a1.078,1.078,0,0,0,.822-1.774Zm-4.934,21.164.007-15.788,3.968-3.171,15.974,18.941Z" transform="translate(-180.36 -181.131)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 876 B

View File

@@ -0,0 +1,26 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32" viewBox="0 0 32 32">
<defs>
<style>
.cls-1 {
stroke: rgba(0,0,0,0);
stroke-miterlimit: 10;
}
.cls-2 {
clip-path: url(#clip-最小距离);
}
.cls-3 {
fill: #fff;
}
</style>
<clipPath id="clip-最小距离">
<rect width="32" height="32"/>
</clipPath>
</defs>
<g id="最小距离" class="cls-2">
<rect class="cls-3" width="32" height="32"/>
<path id="减去_1" data-name="减去 1" class="cls-1" d="M-5.839,24.8H-34.16A1.875,1.875,0,0,1-36,22.933V16.52a1.887,1.887,0,0,1,1.9-1.871H-5.9A1.887,1.887,0,0,1-4,16.52v6.412A1.875,1.875,0,0,1-5.839,24.8ZM-34.1,16.252a.27.27,0,0,0-.272.268v6.412a.27.27,0,0,0,.272.267H-5.9a.269.269,0,0,0,.271-.267V16.52a.27.27,0,0,0-.271-.268H-8.61V21.33H-9.695V16.252h-1.085v2.939h-1.085V16.252h-1.085v2.939h-1.085V16.252h-1.084v2.939H-16.2V16.252h-1.085v2.939h-1.085V16.252h-1.084V21.33h-1.084V16.252h-1.085v2.939h-1.085V16.252H-23.8v2.939h-1.085V16.252h-1.085v2.939h-1.085V16.252h-1.084v2.939H-29.22V16.252H-30.3V21.33H-31.39V16.252Z" transform="translate(36 2)"/>
<path id="交叉_1" data-name="交叉 1" d="M23.716,7.947V4.875c0-.8-.232-1.085-.765-1.085a1.573,1.573,0,0,0-1.133.585V7.947H20.4V2.75h1.163l.1.687H21.7a2.547,2.547,0,0,1,1.763-.817c1.172,0,1.676.78,1.676,2.089V7.947Zm-7.26,0V2.62h1.58V7.947Zm-3.8,0V4.875c0-.8-.243-1.085-.76-1.085a1.606,1.606,0,0,0-1.049.585V7.947H9.421V4.875c0-.8-.243-1.085-.758-1.085a1.608,1.608,0,0,0-1.05.585V7.947H6.194V2.75H7.36l.1.7H7.5A2.326,2.326,0,0,1,9.169,2.62a1.486,1.486,0,0,1,1.5.91A2.445,2.445,0,0,1,12.4,2.62c1.156,0,1.691.78,1.691,2.089V7.947Zm3.8-6.849a.79.79,0,0,1,1.58,0,.79.79,0,0,1-1.58,0Z" transform="translate(0.333 3.053)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,20 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32" viewBox="0 0 32 32">
<defs>
<style>
.cls-1 {
clip-path: url(#clip-标高);
}
.cls-2 {
fill: #fff;
}
</style>
<clipPath id="clip-标高">
<rect width="32" height="32"/>
</clipPath>
</defs>
<g id="标高" class="cls-1">
<rect class="cls-2" width="32" height="32"/>
<path id="路径_8" data-name="路径 8" d="M84.131,193.119a1.056,1.056,0,0,1,1.116.982v7.857a1.056,1.056,0,0,1-1.116.982H54.367a1.056,1.056,0,0,1-1.116-.982V194.1a1.056,1.056,0,0,1,1.116-.982Zm-1.116,1.964H55.483v5.893H83.015Zm1.116-13.749a1.064,1.064,0,0,1,1.114.935,1.032,1.032,0,0,1-1.007,1.025l-.107,0H71.2l-7.858,6.914a1.227,1.227,0,0,1-1.578,0l-8.185-7.2-.018-.016-.032-.031.049.047a1.107,1.107,0,0,1-.092-.092l-.011-.014a.869.869,0,0,1-.182-.857l0-.008L53.31,182l.012-.029.02-.045.019-.035a1.1,1.1,0,0,1,.891-.552h.007q.053,0,.107,0ZM68.043,183.3H57.06l5.492,4.831Z" transform="translate(-53.247 -176.136)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,23 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32" viewBox="0 0 32 32">
<defs>
<style>
.cls-1 {
clip-path: url(#clip-激光边距);
}
.cls-2 {
fill: #fff;
}
</style>
<clipPath id="clip-激光边距">
<rect width="32" height="32"/>
</clipPath>
</defs>
<g id="激光边距" class="cls-1">
<rect class="cls-2" width="32" height="32"/>
<g id="组_4" data-name="组 4" transform="translate(0 -1.293)">
<path id="路径_13" data-name="路径 13" d="M0,1.293v31.96H32V1.293ZM30.97,32.182H1.03V2.323H30.97Z"/>
<path id="路径_14" data-name="路径 14" d="M160.026,291.9l1.6,1.6,7.305-7.305-7.305-7.305-1.6,1.6,4.794,4.566h-6.392v2.283h6.392Zm-5.251,0-4.566-4.566h6.164v-2.283H150.21l4.566-4.566-1.37-1.6L146.1,286.19l7.305,7.305Z" transform="translate(-141.535 -268.917)"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 934 B

View File

@@ -0,0 +1,23 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32" viewBox="0 0 32 32">
<defs>
<style>
.cls-1 {
clip-path: url(#clip-空间体积);
}
.cls-2 {
fill: #fff;
}
</style>
<clipPath id="clip-空间体积">
<rect width="32" height="32"/>
</clipPath>
</defs>
<g id="空间体积" class="cls-1">
<rect class="cls-2" width="32" height="32"/>
<g id="组_5" data-name="组 5" transform="translate(-106.35 -97.661)">
<path id="路径_16" data-name="路径 16" d="M125.977,128.829l13.076-7.363v-13.6l-13.076,6.8Zm-3.126-15.655a.565.565,0,0,1-.258-.064L109.3,106.323a.567.567,0,0,1-.011-1L122.578,98a.567.567,0,0,1,.55,0l13.288,7.325a.567.567,0,0,1-.011,1l-13.292,6.79A.63.63,0,0,1,122.851,113.174ZM110.773,105.8l12.078,6.172,12.078-6.172-12.078-6.657Z" transform="translate(-1.922)"/>
<path id="路径_17" data-name="路径 17" d="M120.649,322.52a.58.58,0,0,1-.262-.064l-13.08-6.8a.573.573,0,0,1-.307-.5V301a.566.566,0,0,1,.273-.486.573.573,0,0,1,.558-.019l13.076,6.8a.573.573,0,0,1,.307.5v14.161a.57.57,0,0,1-.565.569Zm-12.511-7.708,11.942,6.206V308.136l-11.942-6.206Zm15.917,9.408a.585.585,0,0,1-.288-.076.567.567,0,0,1-.281-.489V309.49a.562.562,0,0,1,.307-.5l13.076-6.8a.573.573,0,0,1,.558.019.562.562,0,0,1,.273.486v13.6a.568.568,0,0,1-.288.493l-13.076,7.359A.557.557,0,0,1,124.055,324.22Zm.569-14.385V322.68l11.942-6.722V303.629Z" transform="translate(0 -194.822)"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,20 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32" viewBox="0 0 32 32">
<defs>
<style>
.cls-1 {
clip-path: url(#clip-角度);
}
.cls-2 {
fill: #fff;
}
</style>
<clipPath id="clip-角度">
<rect width="32" height="32"/>
</clipPath>
</defs>
<g id="角度" class="cls-1">
<rect class="cls-2" width="32" height="32"/>
<path id="路径_7" data-name="路径 7" d="M39.587,50.766h13.7a1,1,0,0,1,0,2H23.171a1,1,0,0,1,0-2h1.418l6.582-7.006v-.006a.517.517,0,0,1,.14-.357.456.456,0,0,1,.337-.144l12.1-12.876a.451.451,0,0,1,.665,0,.524.524,0,0,1,0,.708L32.883,43.355a8.3,8.3,0,0,1,6.7,7.411Zm-.949,0a7.254,7.254,0,0,0-6.611-6.5l-6.108,6.5Z" transform="translate(-22.229 -26.489)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 816 B

View File

@@ -0,0 +1,35 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32" viewBox="0 0 32 32">
<defs>
<style>
.cls-1 {
fill: none;
}
.cls-2 {
clip-path: url(#clip-距离);
}
.cls-3 {
clip-path: url(#clip-path);
}
.cls-4 {
fill: #fff;
}
</style>
<clipPath id="clip-path">
<rect id="矩形_1" data-name="矩形 1" class="cls-1" width="32" height="23.606" transform="translate(0 0)"/>
</clipPath>
<clipPath id="clip-距离">
<rect width="32" height="32"/>
</clipPath>
</defs>
<g id="距离" class="cls-2">
<rect class="cls-4" width="32" height="32"/>
<g id="组_2" data-name="组 2" transform="translate(0 4.197)">
<g id="组_1" data-name="组 1" class="cls-3">
<path id="路径_1" data-name="路径 1" d="M29.692,3.03,27.55.919a.529.529,0,0,1-.014-.756A.549.549,0,0,1,28.3.15l.014.013,3.067,3.023a.529.529,0,0,1,0,.756L28.317,6.966a.549.549,0,0,1-.767.013.529.529,0,0,1-.014-.756l.014-.013L29.692,4.1H2.31L4.452,6.21a.528.528,0,0,1,.013.756.547.547,0,0,1-.766.013l-.014-.013L.616,3.942a.531.531,0,0,1,0-.756L3.685.163a.548.548,0,0,1,.767.014.528.528,0,0,1,0,.742L2.31,3.03ZM24.136,15.055H23.051V18H21.966v-2.94H20.882V18H19.8v-2.94H18.712V18H17.627v-2.94H16.543v5.078H15.458V15.055H14.373V18H13.288v-2.94H12.2V18H11.119v-2.94H10.034V18H8.949v-2.94H7.865V18H6.78v-2.94H5.7v5.078H4.61V15.055H1.9a.27.27,0,0,0-.272.268v6.413A.269.269,0,0,0,1.9,22H30.1a.268.268,0,0,0,.271-.267V15.323a.269.269,0,0,0-.271-.268H27.39v5.078H26.305V15.055H25.221V18H24.136Zm5.966-1.6A1.884,1.884,0,0,1,32,15.323v6.413a1.885,1.885,0,0,1-1.9,1.871H1.9A1.885,1.885,0,0,1,0,21.736V15.323a1.885,1.885,0,0,1,1.9-1.871Z"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -7,6 +7,9 @@ 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 {SectionPlaneDialogManager} from './managers/section-plane-dialog-manager';
import {SectionAxisDialogManager} from './managers/section-axis-dialog-manager';
import {SectionBoxDialogManager} from './managers/section-box-dialog-manager';
import type {EngineOptions, ModelLoadOptions} from './components/engine';
import {localeManager} from './services/locale';
import {themeManager} from './services/theme';
@@ -29,6 +32,9 @@ export class BimEngine extends EventEmitter {
public rightKey: RightKeyManager | null = null; // 右键菜单管理器
public propertyPanel: PropertyPanelManager | null = null; // 属性面板 (演示 Collapse)
public measure: MeasureDialogManager | null = null; // 测量面板
public sectionPlane: SectionPlaneDialogManager | null = null; // 拾取面剖切面板
public sectionAxis: SectionAxisDialogManager | null = null; // 轴向剖切面板
public sectionBox: SectionBoxDialogManager | null = null; // 剖切盒面板
constructor(
@@ -95,6 +101,9 @@ export class BimEngine extends EventEmitter {
this.constructTreeBtn = new ConstructTreeManagerBtn(this, this.wrapper);
this.propertyPanel = new PropertyPanelManager(this);
this.measure = new MeasureDialogManager(this);
this.sectionPlane = new SectionPlaneDialogManager(this);
this.sectionAxis = new SectionAxisDialogManager(this);
this.sectionBox = new SectionBoxDialogManager(this);
// 初始主题
this.updateTheme(themeManager.getTheme());

View File

@@ -56,7 +56,7 @@
display: flex;
cursor: pointer;
border-radius: 4px;
transition: background-color 0.2s, color 0.2s;
transition: background-color 0.2s, color 0.2s, border-color 0.2s;
color: var(--bim-btn-text-color, #ccc);
background-color: var(--bim-btn-bg, transparent);
padding: 6px;
@@ -64,6 +64,8 @@
position: relative;
/* 为绝对定位提供锚点 */
justify-content: center;
border: 1px solid transparent;
outline: none;
}
.opt-btn:hover {
@@ -73,11 +75,10 @@
.opt-btn.active {
background-color: var(--bim-btn-active-bg, rgba(255, 255, 255, 0.15));
color: var(--bim-btn-text-active-color, #fff);
/* 开关按钮激活时不显示边框,只改变背景 */
}
.opt-btn.active .opt-btn-icon {
color: var(--bim-icon-active-color, #fff);
}
/* 开关按钮激活时图标颜色保持不变,继承默认颜色 */
.opt-btn.disabled {
opacity: 0.5;
@@ -246,15 +247,30 @@
cursor: pointer;
border-radius: 4px;
color: var(--bim-btn-text-color, #ccc);
transition: background 0.2s;
transition: background 0.2s, border-color 0.2s, color 0.2s;
box-sizing: border-box;
border: 1px solid transparent;
outline: none;
}
/* 二级菜单项的图标默认颜色 */
.opt-btn-dropdown-item .opt-btn-icon {
color: var(--bim-icon-color, #ccc);
}
.opt-btn-dropdown-item:hover {
background-color: var(--bim-btn-hover-bg, #444);
color: #fff;
}
/* 二级菜单项激活态(支持 keepActive 的可视化) */
.opt-btn-dropdown-item.active {
background-color: var(--bim-btn-active-bg, rgba(255, 255, 255, 0.15));
color: var(--bim-btn-text-active-color, #fff);
/* 开关按钮激活时不显示边框,只改变背景 */
}
/* 开关按钮激活时图标颜色保持不变,继承默认颜色 */
/* 下拉菜单项 - 横向布局(图标在左,默认) */
.opt-btn-dropdown-item.align-horizontal {
flex-direction: row;

View File

@@ -183,6 +183,32 @@ export class BimButtonGroup implements IBimComponent {
if (this.options.iconActiveColor) style.setProperty('--bim-icon-active-color', this.options.iconActiveColor);
if (this.options.textColor) style.setProperty('--bim-btn-text-color', this.options.textColor);
if (this.options.textActiveColor) style.setProperty('--bim-btn-text-active-color', this.options.textActiveColor);
// 同步更新所有已存在的dropdown元素的CSS变量dropdown被添加到body无法继承容器的CSS变量
const dropdowns = document.querySelectorAll('.opt-btn-dropdown');
dropdowns.forEach(dropdown => {
const dropdownStyle = (dropdown as HTMLElement).style;
if (this.options.iconColor) dropdownStyle.setProperty('--bim-icon-color', this.options.iconColor);
if (this.options.iconActiveColor) dropdownStyle.setProperty('--bim-icon-active-color', this.options.iconActiveColor);
if (this.options.textColor) dropdownStyle.setProperty('--bim-btn-text-color', this.options.textColor);
if (this.options.textActiveColor) dropdownStyle.setProperty('--bim-btn-text-active-color', this.options.textActiveColor);
if (this.options.btnBackgroundColor) dropdownStyle.setProperty('--bim-btn-bg', this.options.btnBackgroundColor);
if (this.options.btnHoverColor) dropdownStyle.setProperty('--bim-btn-hover-bg', this.options.btnHoverColor);
if (this.options.btnActiveColor) dropdownStyle.setProperty('--bim-btn-active-bg', this.options.btnActiveColor);
});
}
/**
* 设置主题的primary颜色用于边框等
*/
private setPrimaryColor(color: string): void {
this.container.style.setProperty('--bim-primary-color', color);
// 同步更新所有dropdowndropdown被添加到body无法继承容器的CSS变量
const dropdowns = document.querySelectorAll('.opt-btn-dropdown');
dropdowns.forEach(dropdown => {
(dropdown as HTMLElement).style.setProperty('--bim-primary-color', color);
});
}
/**
@@ -210,6 +236,7 @@ export class BimButtonGroup implements IBimComponent {
});
this.applyStyles();
this.setPrimaryColor(theme.primary);
}
/**
@@ -339,6 +366,10 @@ export class BimButtonGroup implements IBimComponent {
const hasLabel = this.options.showLabel && button.label;
if (!hasLabel) {
btnEl.classList.add('no-label');
// 当不显示 label 时,添加 title 属性作为 tooltip
if (button.label) {
btnEl.title = t(button.label);
}
}
// 应用按钮的自定义样式
@@ -412,13 +443,64 @@ export class BimButtonGroup implements IBimComponent {
if (button.disabled) return;
if (!button.children || button.children.length === 0) {
if (button.keepActive) {
this.setBtnActive(button.id);
// 1) 先切换自身激活状态onClick 里通常会根据 isActive 决定“打开/关闭”逻辑)
const wasActive = this.activeBtnIds.has(button.id);
const newState = !wasActive;
this.setBtnActive(button.id, newState);
// 2) 互斥逻辑:仅在“本次切换为激活”时触发
// - 一级按钮:同 groupId 下其它一级按钮互斥
// - 二级按钮:同 groupId + 同 parentId 下其它二级按钮互斥
// - 被关闭的按钮需要触发 onClick用于执行关闭逻辑
if (newState && button.exclusive && button.groupId) {
this.deactivateExclusiveSiblings(button);
}
}
this.closeDropdown();
if (button.onClick) button.onClick(button);
}
}
/**
* 互斥关闭同范围内的其它已激活按钮,并触发它们的 onClick
* @param button 当前被激活的按钮
*/
private deactivateExclusiveSiblings(button: OptButton): void {
const group = this.groups.find(g => g.id === button.groupId);
if (!group) return;
// 二级按钮:同 groupId + 同 parentId
if (button.parentId) {
const parent = this.findButton(group.buttons, button.parentId);
const siblings = parent?.children || [];
for (const sib of siblings) {
if (!sib) continue;
if (sib.id === button.id) continue;
if (sib.parentId !== button.parentId) continue;
if (sib.groupId !== button.groupId) continue;
if (this.activeBtnIds.has(sib.id)) {
this.setBtnActive(sib.id, false);
// 触发被关闭按钮的 onClick此时 sib.isActive 已经同步为 false
if (sib.onClick) sib.onClick(sib);
}
}
return;
}
// 一级按钮:同 groupId 下其它一级按钮(不包含二级)
for (const sib of group.buttons) {
if (sib.id === button.id) continue;
if (sib.groupId !== button.groupId) continue;
if (sib.parentId) continue; // 只处理一级按钮
if (this.activeBtnIds.has(sib.id)) {
this.setBtnActive(sib.id, false);
if (sib.onClick) sib.onClick(sib);
}
}
}
private handleMouseEnter(button: OptButton, btnEl: HTMLElement): void {
if (this.hoverTimeout) clearTimeout(this.hoverTimeout);
if (button.children && button.children.length > 0) {
@@ -440,6 +522,16 @@ export class BimButtonGroup implements IBimComponent {
dropdown.className = 'opt-btn-dropdown';
if (this.options.backgroundColor) dropdown.style.setProperty('--bim-toolbar-bg', this.options.backgroundColor);
// 将主题CSS变量复制到dropdown元素上因为dropdown被添加到body无法继承容器的CSS变量
const dropdownStyle = dropdown.style;
if (this.options.iconColor) dropdownStyle.setProperty('--bim-icon-color', this.options.iconColor);
if (this.options.iconActiveColor) dropdownStyle.setProperty('--bim-icon-active-color', this.options.iconActiveColor);
if (this.options.textColor) dropdownStyle.setProperty('--bim-btn-text-color', this.options.textColor);
if (this.options.textActiveColor) dropdownStyle.setProperty('--bim-btn-text-active-color', this.options.textActiveColor);
if (this.options.btnBackgroundColor) dropdownStyle.setProperty('--bim-btn-bg', this.options.btnBackgroundColor);
if (this.options.btnHoverColor) dropdownStyle.setProperty('--bim-btn-hover-bg', this.options.btnHoverColor);
if (this.options.btnActiveColor) dropdownStyle.setProperty('--bim-btn-active-bg', this.options.btnActiveColor);
// 获取按钮的位置信息
const btnRect = btnEl.getBoundingClientRect();
const expand = this.options.expand || 'down';
@@ -503,6 +595,14 @@ export class BimButtonGroup implements IBimComponent {
item.classList.add('align-vertical');
}
// 二级菜单项的 active 状态渲染(修复 keepActive 在二级按钮“看起来不生效”的问题)
// 说明:
// - keepActive 的状态会记录在 activeBtnIds / button.isActive 上
// - 下拉菜单每次打开都会重新渲染,因此必须在这里同步一次 active 样式
if (this.activeBtnIds.has(button.id) || button.isActive) {
item.classList.add('active');
}
// 应用按钮的自定义样式
const iconSize = button.iconSize || 32; // 二级菜单默认图标更小
const minWidth = button.minWidth; // 不设置默认值,让下拉菜单项保持紧凑
@@ -523,6 +623,9 @@ export class BimButtonGroup implements IBimComponent {
label.className = 'opt-btn-dropdown-label';
label.textContent = t(button.label);
item.appendChild(label);
} else if (button.label) {
// 当不显示 label 时,添加 title 属性作为 tooltip
item.title = t(button.label);
}
item.addEventListener('click', (e) => { e.stopPropagation(); this.handleClick(button); });
@@ -572,11 +675,17 @@ export class BimButtonGroup implements IBimComponent {
const hasLabel = this.options.showLabel && button.label;
// 只需要更新 no-label 类CSS 会处理显示/隐藏
// 更新 no-label 类和 title 属性
if (hasLabel) {
btnEl.classList.remove('no-label');
// 显示标签时,移除 title避免重复显示
btnEl.removeAttribute('title');
} else {
btnEl.classList.add('no-label');
// 隐藏标签时,添加 title 作为 tooltip
if (button.label) {
btnEl.title = t(button.label);
}
}
});
}

View File

@@ -7,6 +7,18 @@ export interface ButtonConfig {
label: string;
icon?: string;
keepActive?: boolean;
/**
* 是否互斥(开关互斥)
*
* 行为说明:
* - 当按钮从“未激活”切换到“激活”时,如果该按钮开启了 exclusive
* 会自动关闭同互斥范围内的其它已激活按钮,并触发它们的 onClick用于执行关闭逻辑
* - 一级按钮:互斥范围 = 同 groupId 下的一级按钮
* - 二级按钮:互斥范围 = 同 groupId 且同 parentId 下的二级按钮
*
* 注意:该能力通常与 keepActive 搭配使用。
*/
exclusive?: boolean;
isActive?:boolean;
disabled?: boolean;
onClick?: (button: OptButton) => void;

View File

@@ -1,13 +1,21 @@
import { ButtonConfig } from '../../../index.type';
import type { ButtonConfig } from '../../../index.type';
import type { BimEngine } from '../../../../../bim-engine';
import { infoIcon } from './icon';
export const infoButton: ButtonConfig = {
id: 'toolbar-info',
type: 'button',
label: 'toolbar.info',
icon: infoIcon,
onClick: () => {
// WORKAROUND: Dispatch a standard custom event on document
document.dispatchEvent(new CustomEvent('bim-demo:open-property-panel'));
}
/**
* 信息按钮配置
* 说明:当前仍保留 demo 的事件触发方式engine 已注入,便于未来替换为 SDK 内部逻辑。
*/
export const createInfoButton = (_engine: BimEngine): ButtonConfig => {
return {
id: 'toolbar-info',
groupId: 'group-2',
type: 'button',
label: 'toolbar.info',
icon: infoIcon,
onClick: () => {
// WORKAROUND: Dispatch a standard custom event on document
document.dispatchEvent(new CustomEvent('bim-demo:open-property-panel'));
}
};
};

View File

@@ -1,16 +1,20 @@
import type { ButtonConfig } from '../../../index.type';
import type { BimEngine } from '../../../../../bim-engine';
/**
* 定位按钮配置
*/
export const locationButton: ButtonConfig = {
id: 'location',
groupId: 'group-1',
type: 'button',
label: 'toolbar.location',
icon: '<svg width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M9 13h2v-2.75h2V13h2V8.25l-3-2l-3 2zm3 9q-4.025-3.425-6.012-6.362T4 10.2q0-3.75 2.413-5.975T12 2t5.588 2.225T20 10.2q0 2.5-1.987 5.438T12 22"/></svg>',
keepActive: false,
onClick: (button) => {
console.log('定位按钮被点击:', button.id);
}
export const createLocationButton = (_engine: BimEngine): ButtonConfig => {
return {
id: 'location',
groupId: 'group-1',
type: 'button',
label: 'toolbar.location',
icon: '<svg width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M9 13h2v-2.75h2V13h2V8.25l-3-2l-3 2zm3 9q-4.025-3.425-6.012-6.362T4 10.2q0-3.75 2.413-5.975T12 2t5.588 2.225T20 10.2q0 2.5-1.987 5.438T12 22"/></svg>',
keepActive: false,
onClick: (button) => {
// 预留:未来接入定位逻辑(此处已注入 engine
console.log('定位按钮被点击:', button.id);
}
};
};

View File

@@ -0,0 +1,26 @@
import type { ButtonConfig } from '../../../../index.type';
import type { BimEngine } from '../../../../../../bim-engine';
/**
* 轴向剖切按钮配置
*/
export const createSectionAxisButton = (engine: BimEngine): ButtonConfig => {
return {
id: 'section-axis',
groupId: 'group-1',
parentId: 'section',
type: 'button',
keepActive: true,
exclusive: true,
align: 'vertical',
label: 'toolbar.sectionAxis',
icon: '<svg width="32" height="32" viewBox="0 0 24 24"><circle cx="12" cy="12" r="6" fill="currentColor"/></svg>',
onClick: (button) => {
if (button.isActive) {
engine.sectionAxis?.show();
} else {
engine.sectionAxis?.destroy();
}
}
};
};

View File

@@ -0,0 +1,29 @@
import type { ButtonConfig } from '../../../../index.type';
import type { BimEngine } from '../../../../../../bim-engine';
/**
* 剖切盒按钮配置
*/
export const createSectionBoxButton = (engine: BimEngine): ButtonConfig => {
return {
id: 'section-box',
groupId: 'group-1',
parentId: 'section',
type: 'button',
keepActive: true,
exclusive: true,
align: 'vertical',
label: 'toolbar.sectionBox',
icon: '<svg width="32" height="32" viewBox="0 0 24 24"><circle cx="12" cy="12" r="6" fill="currentColor"/></svg>',
onClick: (button) => {
console.log('剖切盒被点击:', button.id, '激活状态:', button.isActive);
if (button.isActive) {
// 激活时显示弹窗
engine.sectionBox?.show();
} else {
// 关闭时隐藏弹窗
engine.sectionBox?.hide();
}
}
};
};

View File

@@ -0,0 +1,20 @@
import type { ButtonConfig } from '../../../../index.type';
import type { BimEngine } from '../../../../../../bim-engine';
/**
* 剖切菜单按钮配置
*/
export const createSectionMenuButton = (_engine: BimEngine): ButtonConfig => {
return {
id: 'section',
groupId: 'group-1',
type: 'menu',
label: 'toolbar.section',
align: 'vertical',
icon: '<svg width="32" height="32" viewBox="0 0 24 24"><circle cx="12" cy="12" r="8" fill="currentColor"/></svg>',
keepActive: true,
onClick: (button) => {
console.log('剖切按钮被点击:', button.id);
}
};
};

View File

@@ -0,0 +1,29 @@
import type { ButtonConfig } from '../../../../index.type';
import type { BimEngine } from '../../../../../../bim-engine';
/**
* 拾取面剖切按钮配置
*/
export const createSectionPlaneButton = (engine: BimEngine): ButtonConfig => {
return {
id: 'section-plane',
groupId: 'group-1',
parentId: 'section',
type: 'button',
keepActive: true,
exclusive: true,
align: 'vertical',
label: 'toolbar.sectionPlane',
icon: '<svg width="32" height="32" viewBox="0 0 24 24"><circle cx="12" cy="12" r="6" fill="currentColor"/></svg>',
onClick: (button) => {
console.log('拾取面剖切被点击:', button.id, '激活状态:', button.isActive);
if (button.isActive) {
// 激活时显示弹窗
engine.sectionPlane?.show();
} else {
// 关闭时隐藏弹窗
engine.sectionPlane?.hide();
}
}
};
};

View File

@@ -1,16 +1,20 @@
import type { ButtonConfig } from '../../../index.type';
import type { BimEngine } from '../../../../../bim-engine';
/**
* 定位按钮配置
* 设置按钮配置
*/
export const settingButton: ButtonConfig = {
id: 'setting',
groupId: 'group-2',
type: 'button',
label: 'toolbar.setting',
icon: '<svg width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="m9.25 22l-.4-3.2q-.325-.125-.612-.3t-.563-.375L4.7 19.375l-2.75-4.75l2.575-1.95Q4.5 12.5 4.5 12.338v-.675q0-.163.025-.338L1.95 9.375l2.75-4.75l2.975 1.25q.275-.2.575-.375t.6-.3l.4-3.2h5.5l.4 3.2q.325.125.613.3t.562.375l2.975-1.25l2.75 4.75l-2.575 1.95q.025.175.025.338v.674q0 .163-.05.338l2.575 1.95l-2.75 4.75l-2.95-1.25q-.275.2-.575.375t-.6.3l-.4 3.2zM11 20h1.975l.35-2.65q.775-.2 1.438-.587t1.212-.938l2.475 1.025l.975-1.7l-2.15-1.625q.125-.35.175-.737T17.5 12t-.05-.787t-.175-.738l2.15-1.625l-.975-1.7l-2.475 1.05q-.55-.575-1.212-.962t-1.438-.588L13 4h-1.975l-.35 2.65q-.775.2-1.437.588t-1.213.937L5.55 7.15l-.975 1.7l2.15 1.6q-.125.375-.175.75t-.05.8q0 .4.05.775t.175.75l-2.15 1.625l.975 1.7l2.475-1.05q.55.575 1.213.963t1.437.587zm1.05-4.5q1.45 0 2.475-1.025T15.55 12t-1.025-2.475T12.05 8.5q-1.475 0-2.488 1.025T8.55 12t1.013 2.475T12.05 15.5M12 12"/></svg>',
keepActive: false,
onClick: (button) => {
console.log('设置按钮被点击:', button.id);
}
export const createSettingButton = (_engine: BimEngine): ButtonConfig => {
return {
id: 'setting',
groupId: 'group-2',
type: 'button',
label: 'toolbar.setting',
icon: '<svg width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="m9.25 22l-.4-3.2q-.325-.125-.612-.3t-.563-.375L4.7 19.375l-2.75-4.75l2.575-1.95Q4.5 12.5 4.5 12.338v-.675q0-.163.025-.338L1.95 9.375l2.75-4.75l2.975 1.25q.275-.2.575-.375t.6-.3l.4-3.2h5.5l.4 3.2q.325.125.613.3t.562.375l2.975-1.25l2.75 4.75l-2.575 1.95q.025.175.025.338v.674q0 .163-.05.338l2.575 1.95l-2.75 4.75l-2.95-1.25q-.275.2-.575.375t-.6.3l-.4 3.2zM11 20h1.975l.35-2.65q.775-.2 1.438-.587t1.212-.938l2.475 1.025l.975-1.7l-2.15-1.625q.125-.35.175-.737T17.5 12t-.05-.787t-.175-.738l2.15-1.625l-.975-1.7l-2.475 1.05q-.55-.575-1.212-.962t-1.438-.588L13 4h-1.975l-.35 2.65q-.775.2-1.437.588t-1.213.937L5.55 7.15l-.975 1.7l2.15 1.6q-.125.375-.175.75t-.05.8q0 .4.05.775t.175.75l-2.15 1.625l.975 1.7l2.475-1.05q.55.575 1.213.963t1.437.587zm1.05-4.5q1.45 0 2.475-1.025T15.55 12t-1.025-2.475T12.05 8.5q-1.475 0-2.488 1.025T8.55 12t1.013 2.475T12.05 15.5M12 12"/></svg>',
keepActive: false,
onClick: (button) => {
// 预留:未来接入设置逻辑(此处已注入 engine
console.log('设置按钮被点击:', button.id);
}
};
};

View File

@@ -1,14 +1,22 @@
import type { ButtonConfig } from '../../../../index.type';
import type { BimEngine } from '../../../../../../bim-engine';
export const walkBirdButton: ButtonConfig = {
id: 'walk-bird',
groupId: 'group-1',
parentId: 'walk',
align: 'vertical',
type: 'button',
label: 'toolbar.walkBird',
icon: '<svg width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M9 22V8.775q-2.275-.6-3.637-2.512T4 2h2q0 2.075 1.338 3.538T10.75 7h2.5q.75 0 1.4.275t1.175.8L20.35 12.6l-1.4 1.4L15 10.05V22h-2v-6h-2v6zm3-16q-.825 0-1.412-.587T10 4t.588-1.412T12 2t1.413.588T14 4t-.587 1.413T12 6"/></svg>',
onClick: (button) => {
console.log('鸟瞰漫游被点击:', button.id);
}
/**
* 第三人称(鸟瞰)漫游按钮配置
*/
export const createWalkBirdButton = (_engine: BimEngine): ButtonConfig => {
return {
id: 'walk-bird',
groupId: 'group-1',
parentId: 'walk',
align: 'vertical',
keepActive: true,
exclusive: true,
type: 'button',
label: 'toolbar.walkBird',
icon: '<svg width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M9 22V8.775q-2.275-.6-3.637-2.512T4 2h2q0 2.075 1.338 3.538T10.75 7h2.5q.75 0 1.4.275t1.175.8L20.35 12.6l-1.4 1.4L15 10.05V22h-2v-6h-2v6zm3-16q-.825 0-1.412-.587T10 4t.588-1.412T12 2t1.413.588T14 4t-.587 1.413T12 6"/></svg>',
onClick: (button) => {
console.log('鸟瞰漫游被点击:', button.id);
}
};
};

View File

@@ -1,17 +1,20 @@
import type { ButtonConfig } from '../../../../index.type';
import type { BimEngine } from '../../../../../../bim-engine';
/**
* 漫游菜单按钮配置
*/
export const walkMenuButton: ButtonConfig = {
id: 'walk',
groupId: 'group-1',
type: 'menu',
label: 'toolbar.walk',
align: 'vertical',
icon: '<svg width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M9 22V8.775q-2.275-.6-3.637-2.512T4 2h2q0 2.075 1.338 3.538T10.75 7h2.5q.75 0 1.4.275t1.175.8L20.35 12.6l-1.4 1.4L15 10.05V22h-2v-6h-2v6zm3-16q-.825 0-1.412-.587T10 4t.588-1.412T12 2t1.413.588T14 4t-.587 1.413T12 6"/></svg>',
keepActive: true,
onClick: (button) => {
console.log('漫游按钮被点击:', button.id);
}
export const createWalkMenuButton = (_engine: BimEngine): ButtonConfig => {
return {
id: 'walk',
groupId: 'group-1',
type: 'menu',
label: 'toolbar.walk',
align: 'vertical',
icon: '<svg width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M9 22V8.775q-2.275-.6-3.637-2.512T4 2h2q0 2.075 1.338 3.538T10.75 7h2.5q.75 0 1.4.275t1.175.8L20.35 12.6l-1.4 1.4L15 10.05V22h-2v-6h-2v6zm3-16q-.825 0-1.412-.587T10 4t.588-1.412T12 2t1.413.588T14 4t-.587 1.413T12 6"/></svg>',
keepActive: true,
onClick: (button) => {
console.log('漫游按钮被点击:', button.id);
}
};
};

View File

@@ -1,14 +1,22 @@
import type { ButtonConfig } from '../../../../index.type';
import type { BimEngine } from '../../../../../../bim-engine';
export const walkPersonButton: ButtonConfig = {
id: 'walk-person',
groupId: 'group-1',
parentId: 'walk',
type: 'button',
align: 'vertical',
label: 'toolbar.walkPerson',
icon: '<svg width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M9 22V8.775q-2.275-.6-3.637-2.512T4 2h2q0 2.075 1.338 3.538T10.75 7h2.5q.75 0 1.4.275t1.175.8L20.35 12.6l-1.4 1.4L15 10.05V22h-2v-6h-2v6zm3-16q-.825 0-1.412-.587T10 4t.588-1.412T12 2t1.413.588T14 4t-.587 1.413T12 6"/></svg>',
onClick: (button) => {
console.log('人视漫游被点击:', button.id);
}
/**
* 第一人称漫游按钮配置
*/
export const createWalkPersonButton = (_engine: BimEngine): ButtonConfig => {
return {
id: 'walk-person',
groupId: 'group-1',
parentId: 'walk',
type: 'button',
keepActive: true,
exclusive: true,
align: 'vertical',
label: 'toolbar.walkPerson',
icon: '<svg width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M9 22V8.775q-2.275-.6-3.637-2.512T4 2h2q0 2.075 1.338 3.538T10.75 7h2.5q.75 0 1.4.275t1.175.8L20.35 12.6l-1.4 1.4L15 10.05V22h-2v-6h-2v6zm3-16q-.825 0-1.412-.587T10 4t.588-1.412T12 2t1.413.588T14 4t-.587 1.413T12 6"/></svg>',
onClick: (button) => {
console.log('人视漫游被点击:', button.id);
}
};
};

View File

@@ -0,0 +1,26 @@
import type { ButtonConfig } from '../../../index.type';
import type { BimEngine } from '../../../../../bim-engine';
/**
* 选框放大按钮配置
*
* 说明:
* - 当前仅添加 UI 按钮,点击事件先留空(后续接入引擎能力再实现)
* - 使用工厂函数模式注入 engine便于未来调用 engine API
*/
export const createZoomBoxButton = (_engine: BimEngine): ButtonConfig => {
return {
id: 'zoom-box',
groupId: 'group-1',
keepActive: true,
type: 'button',
label: 'toolbar.zoomBox',
icon: '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M16.5 20q-1.875 0-3.187-1.312T12 15.5t1.313-3.187T16.5 11t3.188 1.313T21 15.5q0 .65-.187 1.25T20.3 17.9l2 2q.275.275.275.7t-.275.7t-.7.275t-.7-.275l-2-2q-.55.325-1.15.513T16.5 20m0-2q1.05 0 1.775-.725T19 15.5t-.725-1.775T16.5 13t-1.775.725T14 15.5t.725 1.775T16.5 18M4 18V9v1v-4zm0 2q-.825 0-1.412-.587T2 18V6q0-.825.588-1.412T4 4h16q.825 0 1.413.588T22 6v3q0 .425-.288.713T21 10h-8V6H4v12h5q.425 0 .713.288T10 19t-.288.713T9 20z"/></svg>',
onClick: () => {
// 事件先留空:后续实现“框选放大/框选缩放”能力时再接入
// 这里不做任何动作,避免误触影响用户操作
}
};
};

View File

@@ -13,32 +13,42 @@ export class Toolbar extends BimButtonGroup {
// 动态加载默认按钮配置
const { createHomeButton } = await import('./buttons/home');
const { locationButton } = await import('./buttons/location');
const { walkMenuButton } = await import('./buttons/walk/walk-menu');
const { walkPersonButton } = await import('./buttons/walk/walk-person');
const { walkBirdButton } = await import('./buttons/walk/walk-bird');
const { settingButton } = await import('./buttons/setting');
const { infoButton } = await import('./buttons/info');
const { createZoomBoxButton } = await import('./buttons/zoom-box');
const { createLocationButton } = await import('./buttons/location');
const { createWalkMenuButton } = await import('./buttons/walk/walk-menu');
const { createWalkPersonButton } = await import('./buttons/walk/walk-person');
const { createWalkBirdButton } = await import('./buttons/walk/walk-bird');
const { createSettingButton } = await import('./buttons/setting');
const { createInfoButton } = await import('./buttons/info');
const { createMeasureButton } = await import('./buttons/measure');
const { createSectionMenuButton } = await import('./buttons/section/section-menu');
const { createSectionPlaneButton } = await import('./buttons/section/section-plane');
const { createSectionAxisButton } = await import('./buttons/section/section-axis');
const { createSectionBoxButton } = await import('./buttons/section/section-box');
this.addGroup('group-1');
// 使用工厂函数创建按钮,并注入 engine
if (this.engine) {
this.addButton(createHomeButton(this.engine));
// 你要求:在"首页"后面添加"选框放大"
this.addButton(createZoomBoxButton(this.engine));
this.addButton(createMeasureButton(this.engine));
this.addButton(createSectionMenuButton(this.engine));
this.addButton(createSectionPlaneButton(this.engine));
this.addButton(createSectionAxisButton(this.engine));
this.addButton(createSectionBoxButton(this.engine));
this.addButton(createWalkMenuButton(this.engine));
this.addButton(createWalkPersonButton(this.engine));
this.addButton(createWalkBirdButton(this.engine));
this.addButton(createLocationButton(this.engine));
this.addGroup('group-2');
this.addButton(createSettingButton(this.engine));
this.addButton(createInfoButton(this.engine));
} else {
console.warn('[Toolbar] Engine not available when creating buttons.');
}
this.addButton(walkMenuButton);
this.addButton(walkPersonButton);
this.addButton(walkBirdButton);
this.addButton(locationButton);
this.addGroup('group-2');
this.addButton(settingButton);
this.addButton(infoButton);
this.render();
}
}

View File

@@ -148,8 +148,8 @@
}
.bim-measure-tool-icon {
width: 22px;
height: 22px;
width: 28px;
height: 28px;
display: inline-flex;
align-items: center;
justify-content: center;
@@ -162,6 +162,8 @@
fill: currentColor;
}
/* 说明:测量方式图标已在 TS 中“瘦身”为纯 pathcurrentColor一般无需额外隐藏背景 rect。 */
.bim-measure-toggle {
display: flex;
justify-content: flex-end;

View File

@@ -5,6 +5,25 @@ import { localeManager, t } from '../../services/locale';
import { themeManager } from '../../services/theme';
import type { MeasureConfig, MeasureMode, MeasurePanelOptions, MeasurePrecision, MeasureResult, MeasureUnit } from './types';
/**
* 测量方式图标SVG
*
* 说明:
* - 你上传的 SVG 原文件放在 `src/assets/icons/` 目录
* - 原始 SVG 含 defs/clipPath/style/背景 rect直接内联时容易出现渲染/裁剪异常(尤其多个图标同时出现)
* - 这里把图标“瘦身”为纯 path并统一使用 currentColor确保稳定渲染
*/
const MEASURE_MODE_ICON_SVGS: Record<MeasureMode, string> = {
distance: `<svg viewBox="0 0 32 32" aria-hidden="true"><g transform="translate(0 4.197)"><path fill="currentColor" d="M29.692,3.03,27.55.919a.529.529,0,0,1-.014-.756A.549.549,0,0,1,28.3.15l.014.013,3.067,3.023a.529.529,0,0,1,0,.756L28.317,6.966a.549.549,0,0,1-.767.013.529.529,0,0,1-.014-.756l.014-.013L29.692,4.1H2.31L4.452,6.21a.528.528,0,0,1,.013.756.547.547,0,0,1-.766.013l-.014-.013L.616,3.942a.531.531,0,0,1,0-.756L3.685.163a.548.548,0,0,1,.767.014.528.528,0,0,1,0,.742L2.31,3.03ZM24.136,15.055H23.051V18H21.966v-2.94H20.882V18H19.8v-2.94H18.712V18H17.627v-2.94H16.543v5.078H15.458V15.055H14.373V18H13.288v-2.94H12.2V18H11.119v-2.94H10.034V18H8.949v-2.94H7.865V18H6.78v-2.94H5.7v5.078H4.61V15.055H1.9a.27.27,0,0,0-.272.268v6.413A.269.269,0,0,0,1.9,22H30.1a.268.268,0,0,0,.271-.267V15.323a.269.269,0,0,0-.271-.268H27.39v5.078H26.305V15.055H25.221V18H24.136Zm5.966-1.6A1.884,1.884,0,0,1,32,15.323v6.413a1.885,1.885,0,0,1-1.9,1.871H1.9A1.885,1.885,0,0,1,0,21.736V15.323a1.885,1.885,0,0,1,1.9-1.871Z"/></g></svg>`,
minDistance: `<svg viewBox="0 0 32 32" aria-hidden="true"><path fill="currentColor" d="M-5.839,24.8H-34.16A1.875,1.875,0,0,1-36,22.933V16.52a1.887,1.887,0,0,1,1.9-1.871H-5.9A1.887,1.887,0,0,1-4,16.52v6.412A1.875,1.875,0,0,1-5.839,24.8ZM-34.1,16.252a.27.27,0,0,0-.272.268v6.412a.27.27,0,0,0,.272.267H-5.9a.269.269,0,0,0,.271-.267V16.52a.27.27,0,0,0-.271-.268H-8.61V21.33H-9.695V16.252h-1.085v2.939h-1.085V16.252h-1.085v2.939h-1.085V16.252h-1.084v2.939H-16.2V16.252h-1.085v2.939h-1.085V16.252h-1.084V21.33h-1.084V16.252h-1.085v2.939h-1.085V16.252H-23.8v2.939h-1.085V16.252h-1.085v2.939h-1.085V16.252h-1.084v2.939H-29.22V16.252H-30.3V21.33H-31.39V16.252Z" transform="translate(36 2)"/><path fill="currentColor" d="M23.716,7.947V4.875c0-.8-.232-1.085-.765-1.085a1.573,1.573,0,0,0-1.133.585V7.947H20.4V2.75h1.163l.1.687H21.7a2.547,2.547,0,0,1,1.763-.817c1.172,0,1.676.78,1.676,2.089V7.947Zm-7.26,0V2.62h1.58V7.947Zm-3.8,0V4.875c0-.8-.243-1.085-.76-1.085a1.606,1.606,0,0,0-1.049.585V7.947H9.421V4.875c0-.8-.243-1.085-.758-1.085a1.608,1.608,0,0,0-1.05.585V7.947H6.194V2.75H7.36l.1.7H7.5A2.326,2.326,0,0,1,9.169,2.62a1.486,1.486,0,0,1,1.5.91A2.445,2.445,0,0,1,12.4,2.62c1.156,0,1.691.78,1.691,2.089V7.947Zm3.8-6.849a.79.79,0,0,1,1.58,0,.79.79,0,0,1-1.58,0Z" transform="translate(0.333 3.053)"/></svg>`,
angle: `<svg viewBox="0 0 32 32" aria-hidden="true"><path fill="currentColor" d="M39.587,50.766h13.7a1,1,0,0,1,0,2H23.171a1,1,0,0,1,0-2h1.418l6.582-7.006v-.006a.517.517,0,0,1,.14-.357.456.456,0,0,1,.337-.144l12.1-12.876a.451.451,0,0,1,.665,0,.524.524,0,0,1,0,.708L32.883,43.355a8.3,8.3,0,0,1,6.7,7.411Zm-.949,0a7.254,7.254,0,0,0-6.611-6.5l-6.108,6.5Z" transform="translate(-22.229 -26.489)"/></svg>`,
elevation: `<svg viewBox="0 0 32 32" aria-hidden="true"><path fill="currentColor" d="M84.131,193.119a1.056,1.056,0,0,1,1.116.982v7.857a1.056,1.056,0,0,1-1.116.982H54.367a1.056,1.056,0,0,1-1.116-.982V194.1a1.056,1.056,0,0,1,1.116-.982Zm-1.116,1.964H55.483v5.893H83.015Zm1.116-13.749a1.064,1.064,0,0,1,1.114.935,1.032,1.032,0,0,1-1.007,1.025l-.107,0H71.2l-7.858,6.914a1.227,1.227,0,0,1-1.578,0l-8.185-7.2-.018-.016-.032-.031.049.047a1.107,1.107,0,0,1-.092-.092l-.011-.014a.869.869,0,0,1-.182-.857l0-.008L53.31,182l.012-.029.02-.045.019-.035a1.1,1.1,0,0,1,.891-.552h.007q.053,0,.107,0ZM68.043,183.3H57.06l5.492,4.831Z" transform="translate(-53.247 -176.136)"/></svg>`,
volume: `<svg viewBox="0 0 32 32" aria-hidden="true"><path fill="currentColor" d="M94.74,86.658V71.189a.371.371,0,0,1,.2-.329l13.869-7.22a.371.371,0,0,1,.344,0l13.053,6.891h0l.819.431a.371.371,0,0,1,.2.328v15.3a.371.371,0,0,1-.2.328l-13.872,7.255a.371.371,0,0,1-.342,0L94.94,86.987a.371.371,0,0,1-.2-.329Zm2.119-.837,11.2,5.8a.024.024,0,0,0,.035-.022V79.483a.371.371,0,0,0-.2-.328l-11.2-5.909a.024.024,0,0,0-.035.021V85.492A.371.371,0,0,0,96.859,85.821Zm13.151-6.459v12a.12.12,0,0,0,.176.106L114,89.474l3.334-1.745,3.771-1.978a.371.371,0,0,0,.2-.328V73.5a.193.193,0,0,0-.284-.171l-10.812,5.708A.371.371,0,0,0,110.01,79.362ZM97.925,71.725l10.839,5.72a.371.371,0,0,0,.346,0L119.8,71.808a.214.214,0,0,0,0-.378l-10.649-5.621a.371.371,0,0,0-.344,0L97.925,71.47A.144.144,0,0,0,97.925,71.725Z" transform="translate(-92.982 -62.907)"/></svg>`,
laserDistance: `<svg viewBox="0 0 32 32" aria-hidden="true"><g transform="translate(0 -1.293)"><path fill="currentColor" d="M0,1.293v31.96H32V1.293ZM30.97,32.182H1.03V2.323H30.97Z"/><path fill="currentColor" d="M160.026,291.9l1.6,1.6,7.305-7.305-7.305-7.305-1.6,1.6,4.794,4.566h-6.392v2.283h6.392Zm-5.251,0-4.566-4.566h6.164v-2.283H150.21l4.566-4.566-1.37-1.6L146.1,286.19l7.305,7.305Z" transform="translate(-141.535 -268.917)"/></g></svg>`,
slope: `<svg viewBox="0 0 32 32" aria-hidden="true"><path fill="currentColor" d="M202.1,188.337l2.629-2.191-8.447-3.106,1.533,8.871,2.629-2.194,9.341,11.209,1.656-1.379Zm-13.726-.435a1.075,1.075,0,0,0-1.07-.341,1.057,1.057,0,0,0-.5.277l-5.11,4.08a1.08,1.08,0,0,0-.406.84l-.007,17.386a1.079,1.079,0,0,0,1.077,1.077L205.7,211.2a1.078,1.078,0,0,0,.822-1.774Zm-4.934,21.164.007-15.788,3.968-3.171,15.974,18.941Z" transform="translate(-180.36 -181.131)"/></svg>`,
spaceVolume: `<svg viewBox="0 0 32 32" aria-hidden="true"><g transform="translate(-106.35 -97.661)"><path fill="currentColor" d="M125.977,128.829l13.076-7.363v-13.6l-13.076,6.8Zm-3.126-15.655a.565.565,0,0,1-.258-.064L109.3,106.323a.567.567,0,0,1-.011-1L122.578,98a.567.567,0,0,1,.55,0l13.288,7.325a.567.567,0,0,1-.011,1l-13.292,6.79A.63.63,0,0,1,122.851,113.174ZM110.773,105.8l12.078,6.172,12.078-6.172-12.078-6.657Z" transform="translate(-1.922)"/><path fill="currentColor" d="M120.649,322.52a.58.58,0,0,1-.262-.064l-13.08-6.8a.573.573,0,0,1-.307-.5V301a.566.566,0,0,1,.273-.486.573.573,0,0,1,.558-.019l13.076,6.8a.573.573,0,0,1,.307.5v14.161a.57.57,0,0,1-.565.569Zm-12.511-7.708,11.942,6.206V308.136l-11.942-6.206Zm15.917,9.408a.585.585,0,0,1-.288-.076.567.567,0,0,1-.281-.489V309.49a.562.562,0,0,1,.307-.5l13.076-6.8a.573.573,0,0,1,.558.019.562.562,0,0,1,.273.486v13.6a.568.568,0,0,1-.288.493l-13.076,7.359A.557.557,0,0,1,124.055,324.22Zm.569-14.385V322.68l11.942-6.722V303.629Z" transform="translate(0 -194.822)"/></g></svg>`
};
/**
* 测量面板组件(只做 UI不实现真实测量
*
@@ -371,8 +390,9 @@ export class MeasurePanel implements IBimComponent {
'spaceVolume'
];
// 图标占位:统一用圆形(你要求的“圆形占位”
const circleIconSvg = `
// 图标:优先使用你上传的 SVG 文件内容(已内联到 MEASURE_MODE_ICON_SVGS
// 兜底:如果某个 mode 没有配置图标,则使用圆形占位(防止页面空白)
const fallbackCircleIconSvg = `
<svg viewBox="0 0 24 24" aria-hidden="true">
<circle cx="12" cy="12" r="9"></circle>
</svg>
@@ -389,7 +409,7 @@ export class MeasurePanel implements IBimComponent {
// icon
const icon = document.createElement('span');
icon.className = 'bim-measure-tool-icon';
icon.innerHTML = circleIconSvg;
icon.innerHTML = MEASURE_MODE_ICON_SVGS[mode] || fallbackCircleIconSvg;
btn.appendChild(icon);
// 点击切换模式

View File

@@ -0,0 +1,92 @@
/**
* 轴向剖切面板样式
*/
.section-axis-panel {
display: flex;
flex-direction: column;
gap: 8px;
padding: 12px 12px 16px 12px;
box-sizing: border-box;
}
/* 第一行:隐藏、反向 */
.section-axis-row-1 {
display: flex;
gap: 8px;
padding-bottom: 8px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
/* 第二行X、Y、Z */
.section-axis-row-2 {
display: flex;
gap: 8px;
padding-top: 8px;
}
/* 按钮基础样式 */
.section-axis-btn {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
padding: 8px;
background: var(--bim-section-axis-btn-bg, rgba(255, 255, 255, 0.06));
border: 1px solid transparent;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
outline: none;
color: var(--bim-text-color, rgba(255, 255, 255, 0.90));
min-height: 48px;
}
.section-axis-btn:hover {
background: var(--bim-section-axis-btn-hover, rgba(255, 255, 255, 0.10));
}
/* 激活状态 */
.section-axis-btn.active {
background: var(--bim-section-axis-btn-active, rgba(255, 255, 255, 0.14));
border-color: var(--bim-text-active-color, #fff);
color: var(--bim-text-active-color, #fff);
}
/* 图标样式 */
.section-axis-btn-icon {
width: 20px;
height: 20px;
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--bim-icon-color, #ccc);
}
.section-axis-btn-icon svg {
width: 100%;
height: 100%;
}
/* 标签样式 */
.section-axis-btn-label {
font-size: 12px;
color: inherit;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
}
/* 文字按钮样式XYZ */
.section-axis-btn-text {
min-height: 40px;
}
.section-axis-btn-text .section-axis-btn-label {
font-size: 18px;
font-weight: 600;
}

View File

@@ -0,0 +1,300 @@
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 { SectionAxisPanelOptions, SectionAxis } from './types';
/**
* 轴向剖切面板组件
* 第一行隐藏toggle、反向
* 第二行X、Y、Z互斥按钮组
*/
export class SectionAxisPanel implements IBimComponent {
public element: HTMLElement;
private options: SectionAxisPanelOptions;
// 状态
private isHidden: boolean = false;
private activeAxis: SectionAxis = 'x';
// DOM 引用 - 第一行
private hideBtn!: HTMLButtonElement;
private reverseBtn!: HTMLButtonElement;
private hideLabelEl!: HTMLElement;
private reverseLabelEl!: HTMLElement;
// DOM 引用 - 第二行
private axisXBtn!: HTMLButtonElement;
private axisYBtn!: HTMLButtonElement;
private axisZBtn!: HTMLButtonElement;
// 订阅清理
private unsubscribeLocale: (() => void) | null = null;
private unsubscribeTheme: (() => void) | null = null;
constructor(options: SectionAxisPanelOptions = {}) {
this.options = options;
this.isHidden = options.defaultHidden ?? false;
this.activeAxis = options.defaultAxis ?? 'x';
this.element = this.createDom();
}
/**
* 初始化组件
*/
public init(): void {
// 订阅语言变更
this.unsubscribeLocale = localeManager.subscribe(() => {
this.setLocales();
});
// 订阅主题变更
this.unsubscribeTheme = themeManager.subscribe((theme) => {
this.setTheme(theme);
});
// 初始应用
this.setLocales();
this.setTheme(themeManager.getTheme());
// 初始化按钮状态
this.updateHideButtonState();
this.updateAxisButtonsState();
}
/**
* 设置主题
*/
public setTheme(theme: ThemeConfig): void {
const style = this.element.style;
style.setProperty('--bim-section-axis-btn-bg', theme.componentBackground ?? 'rgba(255, 255, 255, 0.06)');
style.setProperty('--bim-section-axis-btn-hover', theme.componentHover ?? 'rgba(255, 255, 255, 0.10)');
style.setProperty('--bim-section-axis-btn-active', theme.componentActive ?? 'rgba(255, 255, 255, 0.14)');
style.setProperty('--bim-primary-color', theme.primary ?? '#1890ff');
style.setProperty('--bim-icon-color', theme.icon ?? '#ccc');
style.setProperty('--bim-text-color', theme.textSecondary ?? 'rgba(255, 255, 255, 0.90)');
style.setProperty('--bim-text-active-color', theme.textPrimary ?? '#fff');
}
/**
* 设置语言
*/
public setLocales(): void {
this.hideLabelEl.textContent = t('sectionAxis.actions.hide');
this.reverseLabelEl.textContent = t('sectionAxis.actions.reverse');
// XYZ按钮的文字不需要国际化保持为单个字母
this.hideBtn.title = t('sectionAxis.actions.hide');
this.reverseBtn.title = t('sectionAxis.actions.reverse');
this.axisXBtn.title = t('sectionAxis.actions.axisX');
this.axisYBtn.title = t('sectionAxis.actions.axisY');
this.axisZBtn.title = t('sectionAxis.actions.axisZ');
}
/**
* 设置隐藏状态
*/
public setHiddenState(isHidden: boolean): void {
this.isHidden = isHidden;
this.updateHideButtonState();
}
/**
* 获取隐藏状态
*/
public getHiddenState(): boolean {
return this.isHidden;
}
/**
* 设置激活的轴向
*/
public setActiveAxis(axis: SectionAxis): void {
this.activeAxis = axis;
this.updateAxisButtonsState();
}
/**
* 获取激活的轴向
*/
public getActiveAxis(): SectionAxis {
return this.activeAxis;
}
/**
* 销毁组件
*/
public destroy(): void {
if (this.unsubscribeLocale) {
this.unsubscribeLocale();
this.unsubscribeLocale = null;
}
if (this.unsubscribeTheme) {
this.unsubscribeTheme();
this.unsubscribeTheme = null;
}
this.element.remove();
}
/**
* 创建 DOM
*/
private createDom(): HTMLElement {
const root = document.createElement('div');
root.className = 'section-axis-panel';
// 第一行:隐藏、反向
const row1 = document.createElement('div');
row1.className = 'section-axis-row-1';
this.hideBtn = this.createButton(
'hide',
'<svg viewBox="0 0 24 24"><path fill="currentColor" d="M12 7c2.76 0 5 2.24 5 5 0 .65-.13 1.26-.36 1.83l2.92 2.92c1.51-1.26 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-3.98.7l2.16 2.16C10.74 7.13 11.35 7 12 7zM2 4.27l2.28 2.28.46.46A11.804 11.804 0 0 0 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.42.42L19.73 22 21 20.73 3.27 3 2 4.27zM7.53 9.8l1.55 1.55c-.05.21-.08.43-.08.65 0 1.66 1.34 3 3 3 .22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53-2.76 0-5-2.24-5-5 0-.79.2-1.53.53-2.2zm4.31-.78l3.15 3.15.02-.16c0-1.66-1.34-3-3-3l-.17.01z"/></svg>',
() => this.handleHideToggle()
);
this.reverseBtn = this.createButton(
'reverse',
'<svg viewBox="0 0 24 24"><path fill="currentColor" d="M9 3L5 6.99h3V14h2V6.99h3L9 3zm7 14.01V10h-2v7.01h-3L15 21l4-3.99h-3z"/></svg>',
() => this.handleReverse()
);
row1.appendChild(this.hideBtn);
row1.appendChild(this.reverseBtn);
// 第二行X、Y、Z
const row2 = document.createElement('div');
row2.className = 'section-axis-row-2';
this.axisXBtn = this.createAxisButton('axisX', 'X', () => this.handleAxisChange('x'));
this.axisYBtn = this.createAxisButton('axisY', 'Y', () => this.handleAxisChange('y'));
this.axisZBtn = this.createAxisButton('axisZ', 'Z', () => this.handleAxisChange('z'));
row2.appendChild(this.axisXBtn);
row2.appendChild(this.axisYBtn);
row2.appendChild(this.axisZBtn);
root.appendChild(row1);
root.appendChild(row2);
return root;
}
/**
* 创建按钮(带图标)
*/
private createButton(
type: 'hide' | 'reverse',
iconSvg: string,
onClick: () => void
): HTMLButtonElement {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'section-axis-btn';
// 图标
const icon = document.createElement('div');
icon.className = 'section-axis-btn-icon';
icon.innerHTML = iconSvg;
btn.appendChild(icon);
// 标签
const label = document.createElement('div');
label.className = 'section-axis-btn-label';
btn.appendChild(label);
// 保存 label 引用
if (type === 'hide') {
this.hideLabelEl = label;
} else if (type === 'reverse') {
this.reverseLabelEl = label;
}
// 点击事件
btn.addEventListener('click', onClick);
return btn;
}
/**
* 创建轴向按钮(仅文字)
*/
private createAxisButton(
_type: 'axisX' | 'axisY' | 'axisZ',
text: string,
onClick: () => void
): HTMLButtonElement {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'section-axis-btn section-axis-btn-text';
// 文字标签(既是图标也是标签)
const label = document.createElement('div');
label.className = 'section-axis-btn-label';
label.textContent = text;
btn.appendChild(label);
// 点击事件
btn.addEventListener('click', onClick);
return btn;
}
/**
* 处理隐藏按钮切换
*/
private handleHideToggle(): void {
this.isHidden = !this.isHidden;
this.updateHideButtonState();
if (this.options.onHideToggle) {
this.options.onHideToggle(this.isHidden);
}
}
/**
* 处理反向按钮点击
*/
private handleReverse(): void {
if (this.options.onReverse) {
this.options.onReverse();
}
}
/**
* 处理轴向切换
*/
private handleAxisChange(axis: SectionAxis): void {
if (this.activeAxis === axis) {
return; // 已经是激活状态,不重复触发
}
this.activeAxis = axis;
this.updateAxisButtonsState();
if (this.options.onAxisChange) {
this.options.onAxisChange(axis);
}
}
/**
* 更新隐藏按钮状态
*/
private updateHideButtonState(): void {
if (this.isHidden) {
this.hideBtn.classList.add('active');
} else {
this.hideBtn.classList.remove('active');
}
}
/**
* 更新轴向按钮状态
*/
private updateAxisButtonsState(): void {
this.axisXBtn.classList.toggle('active', this.activeAxis === 'x');
this.axisYBtn.classList.toggle('active', this.activeAxis === 'y');
this.axisZBtn.classList.toggle('active', this.activeAxis === 'z');
}
}

View File

@@ -0,0 +1,36 @@
/**
* 轴向类型
*/
export type SectionAxis = 'x' | 'y' | 'z';
/**
* 轴向剖切面板配置选项
*/
export interface SectionAxisPanelOptions {
/**
* 隐藏按钮切换回调
* @param isHidden 是否隐藏
*/
onHideToggle?: (isHidden: boolean) => void;
/**
* 反向按钮回调
*/
onReverse?: () => void;
/**
* 轴向切换回调
* @param axis 当前激活的轴向
*/
onAxisChange?: (axis: SectionAxis) => void;
/**
* 默认激活的轴向(默认 'x'
*/
defaultAxis?: SectionAxis;
/**
* 初始隐藏状态(默认 false
*/
defaultHidden?: boolean;
}

View File

@@ -0,0 +1,154 @@
.section-box-panel {
display: flex;
flex-direction: column;
padding: 12px;
box-sizing: border-box;
user-select: none;
}
.section-box-row-buttons {
display: flex;
gap: 6px;
padding-bottom: 12px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.section-box-btn {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
padding: 6px;
background: var(--bim-section-box-btn-bg);
border: 1px solid transparent;
border-radius: 4px;
cursor: pointer;
color: var(--bim-text-color);
min-height: 44px;
transition: all 0.2s;
}
.section-box-btn:hover {
background: var(--bim-section-box-btn-hover);
}
.section-box-btn.active {
background: var(--bim-section-box-btn-active);
border-color: var(--bim-text-active-color);
color: var(--bim-text-active-color);
}
.section-box-btn-icon {
width: 18px;
height: 18px;
color: var(--bim-icon-color);
}
.section-box-btn-icon svg {
width: 100%;
height: 100%;
}
.section-box-btn-label {
font-size: 11px;
white-space: nowrap;
}
/* 滑块区域 */
.section-box-sliders {
display: flex;
flex-direction: column;
gap: 16px;
/* 增加行间距防止重叠 */
padding-top: 16px;
}
.section-box-slider {
display: flex;
align-items: center;
gap: 12px;
position: relative;
z-index: 1;
}
/* 鼠标移入当前轴时提升层级 */
.section-box-slider:hover {
z-index: 10;
}
.section-box-slider-label {
font-size: 13px;
font-weight: bold;
color: var(--bim-text-color);
min-width: 14px;
}
.section-box-slider-track {
position: relative;
flex: 1;
height: 4px;
/* 轨道变细 */
background: rgba(255, 255, 255, 0.1);
border-radius: 2px;
}
.section-box-slider-range {
position: absolute;
top: 0;
height: 100%;
background: var(--bim-primary-color);
border-radius: 2px;
pointer-events: none;
/* 防止遮挡手柄点击 */
}
.section-box-slider-handle {
position: absolute;
top: 50%;
width: 14px;
/* 手柄变小 */
height: 14px;
background: #fff;
border: 2px solid var(--bim-primary-color);
border-radius: 50%;
transform: translate(-50%, -50%);
cursor: grab;
z-index: 5;
touch-action: none;
transition: transform 0.2s, box-shadow 0.2s;
}
.section-box-slider-handle:hover {
transform: translate(-50%, -50%) scale(1.2);
box-shadow: 0 0 0 4px rgba(24, 144, 255, 0.2);
}
.section-box-slider-handle.dragging {
cursor: grabbing;
transform: translate(-50%, -50%) scale(1.2);
background: var(--bim-primary-color);
}
/* 数值显示气泡 */
.section-box-slider-handle::after {
content: attr(data-value);
position: absolute;
top: -22px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.8);
color: #fff;
padding: 2px 5px;
border-radius: 3px;
font-size: 10px;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s;
}
.section-box-slider-handle:hover::after,
.section-box-slider-handle.dragging::after {
opacity: 1;
}

View File

@@ -0,0 +1,360 @@
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 { SectionBoxPanelOptions, SectionBoxRange } from './types';
const DEFAULT_RANGE: SectionBoxRange = {
x: { min: 0, max: 100 },
y: { min: 0, max: 100 },
z: { min: 0, max: 100 }
};
export class SectionBoxPanel implements IBimComponent {
public element!: HTMLElement;
private options: SectionBoxPanelOptions;
private isHidden: boolean = false;
private isReversed: boolean = false;
private range: SectionBoxRange;
private hideBtn!: HTMLButtonElement;
private reverseBtn!: HTMLButtonElement;
private fitBtn!: HTMLButtonElement;
private resetBtn!: HTMLButtonElement;
private hideLabelEl!: HTMLElement;
private reverseLabelEl!: HTMLElement;
private fitLabelEl!: HTMLElement;
private resetLabelEl!: HTMLElement;
private xLabelEl!: HTMLElement;
private yLabelEl!: HTMLElement;
private zLabelEl!: HTMLElement;
private unsubscribeLocale: (() => void) | null = null;
private unsubscribeTheme: (() => void) | null = null;
private xSlider!: HTMLElement;
private ySlider!: HTMLElement;
private zSlider!: HTMLElement;
private xMinHandle!: HTMLElement;
private xMaxHandle!: HTMLElement;
private yMinHandle!: HTMLElement;
private yMaxHandle!: HTMLElement;
private zMinHandle!: HTMLElement;
private zMaxHandle!: HTMLElement;
private dragState: {
isDragging: boolean;
axis: 'x' | 'y' | 'z' | null;
handleType: 'min' | 'max' | null;
pointerId: number | null;
} = {
isDragging: false,
axis: null,
handleType: null,
pointerId: null
};
constructor(options: SectionBoxPanelOptions = {}) {
this.options = options;
this.isHidden = options.defaultHidden ?? false;
this.isReversed = options.defaultReversed ?? false;
this.range = JSON.parse(JSON.stringify(options.defaultRange ?? DEFAULT_RANGE));
}
public init(): void {
this.element = this.createPanel();
this.unsubscribeLocale = localeManager.subscribe(() => this.setLocales());
this.unsubscribeTheme = themeManager.subscribe((theme) => this.setTheme(theme));
this.setLocales();
this.setTheme(themeManager.getTheme());
this.updateButtonStates();
this.updateAllSlidersUI();
this.setupDragListeners();
}
// --- Public APIs ---
public setHiddenState(isHidden: boolean): void {
this.isHidden = isHidden;
this.updateButtonStates();
}
public getHiddenState(): boolean {
return this.isHidden;
}
public setReversedState(isReversed: boolean): void {
this.isReversed = isReversed;
this.updateButtonStates();
}
public getReversedState(): boolean {
return this.isReversed;
}
public setRange(range: Partial<SectionBoxRange>): void {
if (range.x) this.range.x = { ...this.range.x, ...range.x };
if (range.y) this.range.y = { ...this.range.y, ...range.y };
if (range.z) this.range.z = { ...this.range.z, ...range.z };
this.updateAllSlidersUI();
}
public getRange(): SectionBoxRange {
return JSON.parse(JSON.stringify(this.range));
}
public reset(): void {
this.isHidden = this.options.defaultHidden ?? false;
this.isReversed = this.options.defaultReversed ?? false;
this.range = JSON.parse(JSON.stringify(this.options.defaultRange ?? DEFAULT_RANGE));
this.updateButtonStates();
this.updateAllSlidersUI();
this.options.onReset?.();
this.options.onRangeChange?.(this.range);
}
// --- Private Setup ---
private createPanel(): HTMLElement {
const panel = document.createElement('div');
panel.className = 'section-box-panel';
const buttonsContainer = document.createElement('div');
buttonsContainer.className = 'section-box-row-buttons';
this.hideBtn = this.createButton('hide', t('sectionBox.actions.hide'), () => {
this.isHidden = !this.isHidden;
this.updateButtonStates();
this.options.onHideToggle?.(this.isHidden);
}, 'hide');
this.reverseBtn = this.createButton('reverse', t('sectionBox.actions.reverse'), () => {
this.isReversed = !this.isReversed;
this.updateButtonStates();
this.options.onReverseToggle?.(this.isReversed);
}, 'reverse');
this.fitBtn = this.createButton('fit', t('sectionBox.actions.fitToModel'), () => {
this.options.onFitToModel?.();
}, 'fit');
this.resetBtn = this.createButton('reset', t('sectionBox.actions.reset'), () => this.reset(), 'reset');
[this.hideBtn, this.reverseBtn, this.fitBtn, this.resetBtn].forEach(btn => buttonsContainer.appendChild(btn));
const slidersContainer = document.createElement('div');
slidersContainer.className = 'section-box-sliders';
slidersContainer.addEventListener('pointerdown', (e) => e.stopPropagation());
this.xSlider = this.createSlider('x', t('sectionBox.axes.x'));
this.ySlider = this.createSlider('y', t('sectionBox.axes.y'));
this.zSlider = this.createSlider('z', t('sectionBox.axes.z'));
[this.xSlider, this.ySlider, this.zSlider].forEach(s => slidersContainer.appendChild(s));
panel.appendChild(buttonsContainer);
panel.appendChild(slidersContainer);
return panel;
}
private createButton(type: string, label: string, onClick: () => void, ref?: string): HTMLButtonElement {
const btn = document.createElement('button');
btn.className = 'section-box-btn';
btn.title = label;
const icon = document.createElement('div');
icon.className = 'section-box-btn-icon';
icon.innerHTML = this.getIconSVG(type);
const labelEl = document.createElement('div');
labelEl.className = 'section-box-btn-label';
labelEl.textContent = label;
if (ref === 'hide') this.hideLabelEl = labelEl;
else if (ref === 'reverse') this.reverseLabelEl = labelEl;
else if (ref === 'fit') this.fitLabelEl = labelEl;
else if (ref === 'reset') this.resetLabelEl = labelEl;
btn.appendChild(icon);
btn.appendChild(labelEl);
btn.addEventListener('click', onClick);
return btn;
}
private createSlider(axis: 'x' | 'y' | 'z', label: string): HTMLElement {
const slider = document.createElement('div');
slider.className = 'section-box-slider';
const labelEl = document.createElement('div');
labelEl.className = 'section-box-slider-label';
labelEl.textContent = label;
if (axis === 'x') this.xLabelEl = labelEl;
else if (axis === 'y') this.yLabelEl = labelEl;
else this.zLabelEl = labelEl;
const track = document.createElement('div');
track.className = 'section-box-slider-track';
const range = document.createElement('div');
range.className = 'section-box-slider-range';
const minHandle = document.createElement('div');
minHandle.className = 'section-box-slider-handle';
minHandle.setAttribute('data-axis', axis);
minHandle.setAttribute('data-handle', 'min');
const maxHandle = document.createElement('div');
maxHandle.className = 'section-box-slider-handle';
maxHandle.setAttribute('data-axis', axis);
maxHandle.setAttribute('data-handle', 'max');
track.append(range, minHandle, maxHandle);
slider.append(labelEl, track);
if (axis === 'x') { this.xMinHandle = minHandle; this.xMaxHandle = maxHandle; }
else if (axis === 'y') { this.yMinHandle = minHandle; this.yMaxHandle = maxHandle; }
else { this.zMinHandle = minHandle; this.zMaxHandle = maxHandle; }
return slider;
}
private setupDragListeners(): void {
const handles = [this.xMinHandle, this.xMaxHandle, this.yMinHandle, this.yMaxHandle, this.zMinHandle, this.zMaxHandle];
handles.forEach(handle => {
handle.addEventListener('pointerdown', (e: PointerEvent) => {
e.preventDefault();
e.stopPropagation();
// 核心锁定:确保后续所有移动事件都只发给这个手柄
handle.setPointerCapture(e.pointerId);
this.dragState = {
isDragging: true,
axis: handle.getAttribute('data-axis') as 'x' | 'y' | 'z',
handleType: handle.getAttribute('data-handle') as 'min' | 'max',
pointerId: e.pointerId
};
handle.classList.add('dragging');
(handle.closest('.section-box-slider') as HTMLElement).style.zIndex = '100';
});
handle.addEventListener('pointermove', (e: PointerEvent) => {
if (this.dragState.isDragging && this.dragState.pointerId === e.pointerId) {
this.onDragging(e);
}
});
const stop = (e: PointerEvent) => {
if (this.dragState.isDragging && this.dragState.pointerId === e.pointerId) {
handle.releasePointerCapture(e.pointerId);
(handle.closest('.section-box-slider') as HTMLElement).style.zIndex = '';
handle.classList.remove('dragging');
this.dragState.isDragging = false;
this.dragState.pointerId = null;
}
};
handle.addEventListener('pointerup', stop);
handle.addEventListener('pointercancel', stop);
});
}
private onDragging(e: PointerEvent): void {
const { axis, handleType } = this.dragState;
if (!axis || !handleType) return;
const sliderEl = axis === 'x' ? this.xSlider : (axis === 'y' ? this.ySlider : this.zSlider);
const track = sliderEl.querySelector('.section-box-slider-track') as HTMLElement;
const rect = track.getBoundingClientRect();
let percentage = ((e.clientX - rect.left) / rect.width) * 100;
percentage = Math.max(0, Math.min(100, percentage));
const current = this.range[axis];
if (handleType === 'min') {
current.min = Math.min(percentage, current.max);
} else {
current.max = Math.max(percentage, current.min);
}
this.updateSliderUI(axis);
this.options.onRangeChange?.(this.range);
}
private updateSliderUI(axis: 'x' | 'y' | 'z'): void {
const range = this.range[axis];
const minH = axis === 'x' ? this.xMinHandle : (axis === 'y' ? this.yMinHandle : this.zMinHandle);
const maxH = axis === 'x' ? this.xMaxHandle : (axis === 'y' ? this.yMaxHandle : this.zMaxHandle);
const slider = axis === 'x' ? this.xSlider : (axis === 'y' ? this.ySlider : this.zSlider);
const rangeEl = slider.querySelector('.section-box-slider-range') as HTMLElement;
minH.style.left = `${range.min}%`;
maxH.style.left = `${range.max}%`;
rangeEl.style.left = `${range.min}%`;
rangeEl.style.width = `${range.max - range.min}%`;
minH.setAttribute('data-value', Math.round(range.min).toString());
maxH.setAttribute('data-value', Math.round(range.max).toString());
}
private updateAllSlidersUI(): void {
['x', 'y', 'z'].forEach((a: any) => this.updateSliderUI(a));
}
private updateButtonStates(): void {
if (this.hideBtn) this.hideBtn.classList.toggle('active', this.isHidden);
if (this.reverseBtn) this.reverseBtn.classList.toggle('active', this.isReversed);
}
private getIconSVG(type: string): string {
const icons: Record<string, string> = {
hide: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 7c2.76 0 5 2.24 5 5 0 .65-.13 1.26-.36 1.83l2.92 2.92c1.51-1.26 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-3.98.7l2.16 2.16C10.74 7.13 11.35 7 12 7zM2 4.27l2.28 2.28.46.46C3.08 8.3 1.78 10.02 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.42.42L19.73 22 21 20.73 3.27 3 2 4.27zM7.53 9.8l1.55 1.55c-.05.21-.08.43-.08.65 0 1.66 1.34 3 3 3 .22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53-2.76 0-5-2.24-5-5 0-.79.2-1.53.53-2.2zm4.31-.78l3.15 3.15.02-.16c0-1.66-1.34-3-3-3l-.17.01z"/></svg>',
reverse: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M9 3L5 6.99h3V14h2V6.99h3L9 3zm7 14.01V10h-2v7.01h-3L15 21l4-3.99h-3z"/></svg>',
fit: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M13 3h-2v10h2V3zm4 8h2v10h-2V11zm-8 4H7v6h2v-6zm-4 3H3v3h2v-3z"/></svg>',
reset: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>'
};
return icons[type] || '';
}
public setLocales(): void {
if (!this.hideLabelEl) return;
this.hideLabelEl.textContent = t('sectionBox.actions.hide');
this.reverseLabelEl.textContent = t('sectionBox.actions.reverse');
this.fitLabelEl.textContent = t('sectionBox.actions.fitToModel');
this.resetLabelEl.textContent = t('sectionBox.actions.reset');
this.xLabelEl.textContent = t('sectionBox.axes.x');
this.yLabelEl.textContent = t('sectionBox.axes.y');
this.zLabelEl.textContent = t('sectionBox.axes.z');
this.hideBtn.title = t('sectionBox.actions.hide');
this.reverseBtn.title = t('sectionBox.actions.reverse');
this.fitBtn.title = t('sectionBox.actions.fitToModel');
this.resetBtn.title = t('sectionBox.actions.reset');
}
public setTheme(theme: ThemeConfig): void {
if (!this.element) return;
const style = this.element.style;
style.setProperty('--bim-section-box-btn-bg', theme.componentBackground ?? 'rgba(255, 255, 255, 0.06)');
style.setProperty('--bim-section-box-btn-hover', theme.componentHover ?? 'rgba(255, 255, 255, 0.10)');
style.setProperty('--bim-section-box-btn-active', theme.componentActive ?? 'rgba(255, 255, 255, 0.14)');
style.setProperty('--bim-primary-color', theme.primary ?? '#1890ff');
style.setProperty('--bim-icon-color', theme.icon ?? '#ccc');
style.setProperty('--bim-text-color', theme.textSecondary ?? 'rgba(255, 255, 255, 0.90)');
style.setProperty('--bim-text-active-color', theme.textPrimary ?? '#fff');
}
public destroy(): void {
this.unsubscribeLocale?.();
this.unsubscribeTheme?.();
if (this.element && this.element.parentElement) {
this.element.parentElement.removeChild(this.element);
}
}
}

View File

@@ -0,0 +1,69 @@
/**
* 剖切盒轴向范围
*/
export interface SectionBoxAxisRange {
/** 最小值0-100的百分比 */
min: number;
/** 最大值0-100的百分比 */
max: number;
}
/**
* 剖切盒范围数据
*/
export interface SectionBoxRange {
/** X轴范围 */
x: SectionBoxAxisRange;
/** Y轴范围 */
y: SectionBoxAxisRange;
/** Z轴范围 */
z: SectionBoxAxisRange;
}
/**
* 剖切盒面板配置选项
*/
export interface SectionBoxPanelOptions {
/**
* 隐藏按钮切换回调
* @param isHidden 是否隐藏剖切盒
*/
onHideToggle?: (isHidden: boolean) => void;
/**
* 反向按钮切换回调
* @param isReversed 是否反向
*/
onReverseToggle?: (isReversed: boolean) => void;
/**
* 适应到模型按钮回调
*/
onFitToModel?: () => void;
/**
* 重置按钮回调
*/
onReset?: () => void;
/**
* 范围变化回调
* @param range 当前范围值
*/
onRangeChange?: (range: SectionBoxRange) => void;
/**
* 默认隐藏状态(默认 false
*/
defaultHidden?: boolean;
/**
* 默认反向状态(默认 false
*/
defaultReversed?: boolean;
/**
* 默认范围值
*/
defaultRange?: SectionBoxRange;
}

View File

@@ -0,0 +1,60 @@
/**
* 拾取面剖切面板样式
*/
.section-plane-panel {
display: flex;
gap: 8px;
padding: 12px 12px 16px 12px;
box-sizing: border-box;
}
.section-plane-btn {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
padding: 8px;
border: 1px solid transparent;
background: var(--bim-section-btn-bg, rgba(255, 255, 255, 0.06));
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease;
min-width: 60px;
outline: none;
color: var(--bim-text-color, rgba(255, 255, 255, 0.90));
}
.section-plane-btn:hover {
background: var(--bim-section-btn-hover, rgba(255, 255, 255, 0.10));
}
.section-plane-btn:active {
background: var(--bim-section-btn-active, rgba(255, 255, 255, 0.14));
border-color: var(--bim-text-active-color, #fff);
color: var(--bim-text-active-color, #fff);
}
.section-plane-btn-icon {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
color: var(--bim-icon-color, #ccc);
}
.section-plane-btn-icon svg {
width: 100%;
height: 100%;
fill: currentColor;
}
.section-plane-btn-label {
font-size: 12px;
color: inherit;
text-align: center;
line-height: 1.2;
}

View File

@@ -0,0 +1,174 @@
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 { SectionPlanePanelOptions } from './types';
/**
* 拾取面剖切面板组件
* 包含三个操作按钮:隐藏、反向、重置
*/
export class SectionPlanePanel implements IBimComponent {
public element: HTMLElement;
private options: SectionPlanePanelOptions;
// DOM 引用
private hideBtn!: HTMLButtonElement;
private reverseBtn!: HTMLButtonElement;
private resetBtn!: HTMLButtonElement;
private hideLabelEl!: HTMLElement;
private reverseLabelEl!: HTMLElement;
private resetLabelEl!: HTMLElement;
// 订阅清理
private unsubscribeLocale: (() => void) | null = null;
private unsubscribeTheme: (() => void) | null = null;
constructor(options: SectionPlanePanelOptions = {}) {
this.options = options;
this.element = this.createDom();
}
/**
* 初始化组件
*/
public init(): void {
// 订阅语言变更
this.unsubscribeLocale = localeManager.subscribe(() => {
this.setLocales();
});
// 订阅主题变更
this.unsubscribeTheme = themeManager.subscribe((theme) => {
this.setTheme(theme);
});
// 初始应用
this.setLocales();
this.setTheme(themeManager.getTheme());
}
/**
* 设置主题
*/
public setTheme(theme: ThemeConfig): void {
const style = this.element.style;
style.setProperty('--bim-section-btn-bg', theme.componentBackground ?? 'rgba(255, 255, 255, 0.06)');
style.setProperty('--bim-section-btn-hover', theme.componentHover ?? 'rgba(255, 255, 255, 0.10)');
style.setProperty('--bim-section-btn-active', theme.componentActive ?? 'rgba(255, 255, 255, 0.14)');
style.setProperty('--bim-primary-color', theme.primary ?? '#1890ff');
style.setProperty('--bim-icon-color', theme.icon ?? '#ccc');
style.setProperty('--bim-text-color', theme.textSecondary ?? 'rgba(255, 255, 255, 0.90)');
style.setProperty('--bim-text-active-color', theme.textPrimary ?? '#fff');
}
/**
* 设置语言
*/
public setLocales(): void {
this.hideLabelEl.textContent = t('sectionPlane.actions.hide');
this.reverseLabelEl.textContent = t('sectionPlane.actions.reverse');
this.resetLabelEl.textContent = t('sectionPlane.actions.reset');
this.hideBtn.title = t('sectionPlane.actions.hide');
this.reverseBtn.title = t('sectionPlane.actions.reverse');
this.resetBtn.title = t('sectionPlane.actions.reset');
}
/**
* 销毁组件
*/
public destroy(): void {
if (this.unsubscribeLocale) {
this.unsubscribeLocale();
this.unsubscribeLocale = null;
}
if (this.unsubscribeTheme) {
this.unsubscribeTheme();
this.unsubscribeTheme = null;
}
this.element.remove();
}
/**
* 创建 DOM
*/
private createDom(): HTMLElement {
const root = document.createElement('div');
root.className = 'section-plane-panel';
// 隐藏按钮
this.hideBtn = this.createButton(
'hide',
'<svg viewBox="0 0 24 24"><path fill="currentColor" d="M12 7c2.76 0 5 2.24 5 5 0 .65-.13 1.26-.36 1.83l2.92 2.92c1.51-1.26 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-3.98.7l2.16 2.16C10.74 7.13 11.35 7 12 7zM2 4.27l2.28 2.28.46.46A11.804 11.804 0 0 0 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.42.42L19.73 22 21 20.73 3.27 3 2 4.27zM7.53 9.8l1.55 1.55c-.05.21-.08.43-.08.65 0 1.66 1.34 3 3 3 .22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53-2.76 0-5-2.24-5-5 0-.79.2-1.53.53-2.2zm4.31-.78l3.15 3.15.02-.16c0-1.66-1.34-3-3-3l-.17.01z"/></svg>',
() => {
if (this.options.onHide) {
this.options.onHide();
}
}
);
// 反向按钮
this.reverseBtn = this.createButton(
'reverse',
'<svg viewBox="0 0 24 24"><path fill="currentColor" d="M9 3L5 6.99h3V14h2V6.99h3L9 3zm7 14.01V10h-2v7.01h-3L15 21l4-3.99h-3z"/></svg>',
() => {
if (this.options.onReverse) {
this.options.onReverse();
}
}
);
// 重置按钮
this.resetBtn = this.createButton(
'reset',
'<svg viewBox="0 0 24 24"><path fill="currentColor" d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>',
() => {
if (this.options.onReset) {
this.options.onReset();
}
}
);
root.appendChild(this.hideBtn);
root.appendChild(this.reverseBtn);
root.appendChild(this.resetBtn);
return root;
}
/**
* 创建按钮
*/
private createButton(type: 'hide' | 'reverse' | 'reset', iconSvg: string, onClick: () => void): HTMLButtonElement {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'section-plane-btn';
// 图标
const icon = document.createElement('div');
icon.className = 'section-plane-btn-icon';
icon.innerHTML = iconSvg;
btn.appendChild(icon);
// 标签
const label = document.createElement('div');
label.className = 'section-plane-btn-label';
btn.appendChild(label);
// 保存 label 引用
if (type === 'hide') {
this.hideLabelEl = label;
} else if (type === 'reverse') {
this.reverseLabelEl = label;
} else {
this.resetLabelEl = label;
}
// 点击事件
btn.addEventListener('click', onClick);
return btn;
}
}

View File

@@ -0,0 +1,19 @@
/**
* 拾取面剖切面板配置选项
*/
export interface SectionPlanePanelOptions {
/**
* 隐藏按钮回调
*/
onHide?: () => void;
/**
* 反向按钮回调
*/
onReverse?: () => void;
/**
* 重置按钮回调
*/
onReset?: () => void;
}

View File

@@ -10,6 +10,7 @@ export const enUS: TranslationDictionary = {
toolbar: {
home: 'Home',
measure: 'Measure',
zoomBox: 'Zoom Box',
info: 'Info',
location: 'Location',
setting: 'Settings',
@@ -18,6 +19,10 @@ export const enUS: TranslationDictionary = {
walkBird: 'Bird Eye',
walkMenu: 'Menu',
tree: 'Tree',
section: 'Section',
sectionPlane: 'Plane Section',
sectionAxis: 'Axis Section',
sectionBox: 'Section Box'
},
dialog: {
testTitle: 'Test Dialog',
@@ -102,5 +107,37 @@ export const enUS: TranslationDictionary = {
save: 'Save',
cancel: 'Cancel',
}
},
sectionPlane: {
dialogTitle: 'Plane Section',
actions: {
hide: 'Hide',
reverse: 'Reverse',
reset: 'Reset'
}
},
sectionAxis: {
dialogTitle: 'Axis Section',
actions: {
hide: 'Hide',
reverse: 'Reverse',
axisX: 'X',
axisY: 'Y',
axisZ: 'Z'
}
},
sectionBox: {
dialogTitle: 'Section Box',
actions: {
hide: 'Hide',
reverse: 'Reverse',
fitToModel: 'Fit',
reset: 'Reset'
},
axes: {
x: 'X',
y: 'Y',
z: 'Z'
}
}
};

View File

@@ -13,6 +13,7 @@ export interface TranslationDictionary {
toolbar: {
home: string;
measure: string;
zoomBox: string;
info: string;
location: string;
setting: string;
@@ -21,6 +22,10 @@ export interface TranslationDictionary {
walkPerson: string;
walkBird: string;
tree: string;
section: string;
sectionPlane: string;
sectionAxis: string;
sectionBox: string;
};
panel: {
property: {
@@ -125,7 +130,39 @@ export interface TranslationDictionary {
save: string;
cancel: string;
};
}
};
sectionPlane: {
dialogTitle: string;
actions: {
hide: string;
reverse: string;
reset: string;
};
};
sectionAxis: {
dialogTitle: string;
actions: {
hide: string;
reverse: string;
axisX: string;
axisY: string;
axisZ: string;
};
};
sectionBox: {
dialogTitle: string;
actions: {
hide: string;
reverse: string;
fitToModel: string;
reset: string;
};
axes: {
x: string;
y: string;
z: string;
};
};
}
/**

View File

@@ -10,6 +10,7 @@ export const zhCN: TranslationDictionary = {
toolbar: {
home: '首页',
measure: '测量',
zoomBox: '选框放大',
info: '信息',
location: '定位',
setting: '设置',
@@ -17,7 +18,11 @@ export const zhCN: TranslationDictionary = {
walkMenu: '漫游菜单',
walkPerson: '第一人称',
walkBird: '第三人称',
tree: '模型树'
tree: '模型树',
section: '剖切',
sectionPlane: '拾取面剖切',
sectionAxis: '轴向剖切',
sectionBox: '剖切盒'
},
dialog: {
testTitle: '测试弹窗',
@@ -102,5 +107,37 @@ export const zhCN: TranslationDictionary = {
save: '保存设置',
cancel: '取消',
}
},
sectionPlane: {
dialogTitle: '拾取面剖切',
actions: {
hide: '隐藏',
reverse: '反向',
reset: '重置'
}
},
sectionAxis: {
dialogTitle: '轴向剖切',
actions: {
hide: '隐藏',
reverse: '反向',
axisX: 'X',
axisY: 'Y',
axisZ: 'Z'
}
},
sectionBox: {
dialogTitle: '剖切盒',
actions: {
hide: '隐藏',
reverse: '反向',
fitToModel: '适应',
reset: '重置'
},
axes: {
x: 'X',
y: 'Y',
z: 'Z'
}
}
};

View File

@@ -0,0 +1,135 @@
import { BimComponent } from '../core/component';
import { BimEngine } from '../bim-engine';
import { BimDialog } from '../components/dialog';
import { SectionAxisPanel } from '../components/section-axis-panel';
import type { SectionAxis } from '../components/section-axis-panel/types';
/**
* 轴向剖切弹窗管理器
*/
export class SectionAxisDialogManager extends BimComponent {
private dialogId = 'section-axis-dialog';
private dialog: BimDialog | null = null;
private panel: SectionAxisPanel | null = null;
constructor(engine: BimEngine) {
super(engine);
}
public init(): void {
// 可以在这里监听事件
}
/**
* 显示弹窗
*/
public show(): void {
if (!this.engine.dialog || !this.engine.container) {
console.warn('Dialog manager or container is not initialized');
return;
}
// 如果已打开,先销毁
this.destroy();
// 创建面板
this.panel = new SectionAxisPanel({
defaultAxis: 'x',
defaultHidden: false,
onHideToggle: (isHidden) => {
console.log('[SectionAxisDialogManager] 隐藏切换:', isHidden);
// TODO: 实现隐藏/显示剖切面的逻辑
},
onReverse: () => {
console.log('[SectionAxisDialogManager] 反向剖切');
// TODO: 实现反向剖切的逻辑
},
onAxisChange: (axis) => {
console.log('[SectionAxisDialogManager] 切换轴向:', axis);
// TODO: 实现轴向切换的逻辑
}
});
this.panel.init();
// 创建弹窗
const dialogWidth = 240;
const paddingRight = 20;
const paddingBottom = 50;
const container = this.engine.container;
const containerWidth = container.clientWidth;
const containerHeight = container.clientHeight;
const x = containerWidth - dialogWidth - paddingRight;
const y = containerHeight - paddingBottom - 200; // 临时y值会被fitHeight调整
this.dialog = this.engine.dialog.create({
id: this.dialogId,
title: 'sectionAxis.dialogTitle',
width: dialogWidth,
height: 'auto', // 自动高度
position: { x, y },
draggable: true,
resizable: false,
content: this.panel.element,
onClose: () => {
this.engine.toolbar?.setBtnActive('section-axis', false);
this.hide();
}
});
this.dialog.init();
// 自适应高度
this.dialog.fitHeight(false);
}
/**
* 隐藏弹窗
*/
public hide(): void {
this.destroy();
}
/**
* 获取隐藏状态
*/
public getHiddenState(): boolean {
return this.panel?.getHiddenState() ?? false;
}
/**
* 设置隐藏状态
*/
public setHiddenState(isHidden: boolean): void {
this.panel?.setHiddenState(isHidden);
}
/**
* 获取当前激活的轴向
*/
public getActiveAxis(): SectionAxis {
return this.panel?.getActiveAxis() ?? 'x';
}
/**
* 设置激活的轴向
*/
public setActiveAxis(axis: SectionAxis): void {
this.panel?.setActiveAxis(axis);
}
/**
* 销毁弹窗和面板
*/
public destroy(): void {
// 关闭弹窗
if (this.dialog) {
this.dialog.destroy();
this.dialog = null;
}
// 销毁面板
if (this.panel) {
this.panel.destroy();
this.panel = null;
}
}
}

View File

@@ -0,0 +1,159 @@
import { BimComponent } from '../core/component';
import { BimEngine } from '../bim-engine';
import { BimDialog } from '../components/dialog';
import { SectionBoxPanel } from '../components/section-box-panel';
import type { SectionBoxRange } from '../components/section-box-panel/types';
/**
* 剖切盒弹窗管理器
*/
export class SectionBoxDialogManager extends BimComponent {
private dialogId = 'section-box-dialog';
private dialog: BimDialog | null = null;
private panel: SectionBoxPanel | null = null;
constructor(engine: BimEngine) {
super(engine);
}
public init(): void {
// 可以在这里监听事件
}
/**
* 显示弹窗
*/
public show(): void {
if (!this.engine.dialog || !this.engine.container) {
console.warn('Dialog manager or container is not initialized');
return;
}
// 如果已打开,先销毁
this.destroy();
// 创建面板
this.panel = new SectionBoxPanel({
defaultHidden: false,
defaultReversed: false,
onHideToggle: (isHidden) => {
console.log('[SectionBoxDialogManager] 隐藏切换:', isHidden);
// TODO: 实现隐藏/显示剖切盒的逻辑
},
onReverseToggle: (isReversed) => {
console.log('[SectionBoxDialogManager] 反向切换:', isReversed);
// TODO: 实现反向剖切的逻辑
},
onFitToModel: () => {
console.log('[SectionBoxDialogManager] 适应到模型');
// TODO: 实现自动适应模型的逻辑
},
onReset: () => {
console.log('[SectionBoxDialogManager] 重置');
// 注意:不要在这里调用 panel.reset(),会造成无限递归
// panel 的 reset 按钮已经在内部处理了状态重置
// TODO: 这里只需要通知 3D 引擎重置剖切盒即可
},
onRangeChange: (range) => {
console.log('[SectionBoxDialogManager] 范围变化:', range);
// TODO: 实现范围变化的逻辑
}
});
this.panel.init();
// 创建弹窗
const dialogWidth = 280;
const paddingRight = 20;
const paddingBottom = 50;
const container = this.engine.container;
const containerWidth = container.clientWidth;
const containerHeight = container.clientHeight;
const x = containerWidth - dialogWidth - paddingRight;
const y = containerHeight - paddingBottom - 300; // 临时y值会被fitHeight调整
this.dialog = this.engine.dialog.create({
id: this.dialogId,
title: 'sectionBox.dialogTitle',
width: dialogWidth,
height: 'auto',
position: { x, y },
draggable: true,
resizable: false,
content: this.panel.element,
onClose: () => {
this.engine.toolbar?.setBtnActive('section-box', false);
this.hide();
}
});
this.dialog.init();
// 自适应高度
this.dialog.fitHeight(false);
}
/**
* 隐藏弹窗
*/
public hide(): void {
this.destroy();
}
/**
* 获取隐藏状态
*/
public getHiddenState(): boolean {
return this.panel?.getHiddenState() ?? false;
}
/**
* 设置隐藏状态
*/
public setHiddenState(isHidden: boolean): void {
this.panel?.setHiddenState(isHidden);
}
/**
* 获取反向状态
*/
public getReversedState(): boolean {
return this.panel?.getReversedState() ?? false;
}
/**
* 设置反向状态
*/
public setReversedState(isReversed: boolean): void {
this.panel?.setReversedState(isReversed);
}
/**
* 获取范围值
*/
public getRange(): SectionBoxRange | null {
return this.panel?.getRange() ?? null;
}
/**
* 设置范围值
*/
public setRange(range: Partial<SectionBoxRange>): void {
this.panel?.setRange(range);
}
/**
* 销毁弹窗和面板
*/
public destroy(): void {
// 关闭弹窗
if (this.dialog) {
this.dialog.destroy();
this.dialog = null;
}
// 销毁面板
if (this.panel) {
this.panel.destroy();
this.panel = null;
}
}
}

View File

@@ -0,0 +1,98 @@
import { BimComponent } from '../core/component';
import { BimEngine } from '../bim-engine';
import { BimDialog } from '../components/dialog';
import { SectionPlanePanel } from '../components/section-plane-panel';
/**
* 拾取面剖切弹窗管理器
*/
export class SectionPlaneDialogManager extends BimComponent {
private dialogId = 'section-plane-dialog';
private dialog: BimDialog | null = null;
private panel: SectionPlanePanel | null = null;
constructor(engine: BimEngine) {
super(engine);
}
public init(): void {
// 可以在这里监听事件
}
/**
* 显示拾取面剖切弹窗
*/
public show(): void {
if (!this.engine.dialog || !this.engine.container) {
console.warn('Dialog manager or container is not initialized');
return;
}
// 如果已打开过,先销毁旧实例
this.destroy();
// 创建面板
this.panel = new SectionPlanePanel({
onHide: () => {
console.log('[SectionPlaneDialogManager] 隐藏');
// TODO: 调用引擎的隐藏功能
},
onReverse: () => {
console.log('[SectionPlaneDialogManager] 反向');
// TODO: 调用引擎的反向功能
},
onReset: () => {
console.log('[SectionPlaneDialogManager] 重置');
// TODO: 调用引擎的重置功能
}
});
this.panel.init();
// 创建弹窗
const dialogWidth = 240;
const dialogHeight = 120;
const paddingRight = 20;
const paddingBottom = 50;
const container = this.engine.container;
const containerWidth = container.clientWidth;
const containerHeight = container.clientHeight;
const x = containerWidth - dialogWidth - paddingRight;
const y = containerHeight - dialogHeight - paddingBottom;
this.dialog = this.engine.dialog.create({
id: this.dialogId,
title: 'sectionPlane.dialogTitle',
width: dialogWidth,
height: dialogHeight,
position: { x, y },
draggable: true,
resizable: false,
content: this.panel.element,
onClose: () => {
this.engine.toolbar?.setBtnActive('section-plane', false);
this.hide();
}
});
}
/**
* 隐藏弹窗
*/
public hide(): void {
this.destroy();
}
/**
* 销毁弹窗
*/
public destroy(): void {
if (this.dialog) {
this.dialog.destroy();
this.dialog = null;
}
if (this.panel) {
this.panel.destroy();
this.panel = null;
}
}
}