提交代码

This commit is contained in:
yuding
2026-04-20 10:38:42 +08:00
parent 0b730da6f4
commit 9a185699ae
36 changed files with 17961 additions and 13149 deletions

115
.opencode/package-lock.json generated Normal file
View File

@@ -0,0 +1,115 @@
{
"name": ".opencode",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"@opencode-ai/plugin": "1.4.3"
}
},
"node_modules/@opencode-ai/plugin": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/@opencode-ai/plugin/-/plugin-1.4.3.tgz",
"integrity": "sha512-Ob/3tVSIeuMRJBr2O23RtrnC5djRe01Lglx+TwGEmjrH9yDBJ2tftegYLnNEjRoMuzITgq9LD8168p4pzv+U/A==",
"license": "MIT",
"dependencies": {
"@opencode-ai/sdk": "1.4.3",
"zod": "4.1.8"
},
"peerDependencies": {
"@opentui/core": ">=0.1.97",
"@opentui/solid": ">=0.1.97"
},
"peerDependenciesMeta": {
"@opentui/core": {
"optional": true
},
"@opentui/solid": {
"optional": true
}
}
},
"node_modules/@opencode-ai/sdk": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.4.3.tgz",
"integrity": "sha512-X0CAVbwoGAjTY2iecpWkx2B+GAa2jSaQKYpJ+xILopeF/OGKZUN15mjqci+L7cEuwLHV5wk3x2TStUOVCa5p0A==",
"license": "MIT",
"dependencies": {
"cross-spawn": "7.0.6"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"license": "ISC"
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"license": "MIT",
"dependencies": {
"shebang-regex": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"node-which": "bin/node-which"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/zod": {
"version": "4.1.8",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
}
}
}

View File

@@ -229,6 +229,11 @@
<button onclick="pauseRendering()">暂停渲染</button>
<button onclick="resumeRendering()">恢复渲染</button>
</div>
<div class="btn-container" style="margin-top: 8px;">
<button onclick="readModelCodeFormStoge()">读取缓存编码</button>
<button onclick="startOneClickEncoding()">启动一键编码</button>
<button onclick="checkHasModelCode()">检查模型编码</button>
</div>
<div style="margin-top: 10px; font-size: 0.85rem; color: #666;">
<div>状态: <span id="engine-status">未初始化</span></div>
</div>
@@ -339,7 +344,8 @@
let unsubscribePresetSaved = null;
let unsubscribePresetChanged = null;
let unsubscribePresetDeleted = null;
const DEFAULT_3D_MODEL_URL = 'https://lyz-1259524260.cos.ap-guangzhou.myqcloud.com/iflow/models/417664a3-76c8-4d94-9344-1337246a5d4e/';
// const DEFAULT_3D_MODEL_URL = 'https://pub-8092fd0db2e14822a9f742ad0050750c.r2.dev/e9603d6b-c885-4f1b-84b0-2589bc9dc44f';
const DEFAULT_3D_MODEL_URL = 'https://lyz-1259524260.cos.ap-guangzhou.myqcloud.com/iflow/models/417664a3-76c8-4d94-9344-1337246a5d4e';
//const DEFAULT_3D_MODEL_URL = 'https://pub-8092fd0db2e14822a9f742ad0050750c.r2.dev/66ad9a66-5ca8-47ac-9139-6aa8756069c1/';
let current3dModelUrl = DEFAULT_3D_MODEL_URL;
@@ -698,6 +704,43 @@
console.log('✅ 渲染已恢复');
}
function readModelCodeFormStoge() {
if (!engine || !engine.engine || !engine.engine.isInitialized()) {
alert('请先初始化 3D 引擎!');
return;
}
engine.engine.getEngineComponent()?.readModelCodeFormStoge();
console.log('✅ 读取缓存编码数据已触发');
}
function startOneClickEncoding() {
if (!engine || !engine.engine || !engine.engine.isInitialized()) {
alert('请先初始化 3D 引擎!');
return;
}
engine.on('encoding:start', function (data) {
console.log('编码开始', data);
});
engine.on('encoding:complete', function (data) {
console.log('编码完成', data);
});
engine.on('encoding:error', function (data) {
console.log('编码失败', data);
});
engine.engine.getEngineComponent()?.startOneClickEncoding();
console.log('✅ 一键编码已启动');
}
function checkHasModelCode() {
if (!engine || !engine.engine || !engine.engine.isInitialized()) {
alert('请先初始化 3D 引擎!');
return;
}
var result = engine.engine.getEngineComponent()?.hasModelCode();
console.log('✅ 检查模型编码结果:', result);
alert('模型编码检查结果: ' + (result ? '所有模型已编码' : '未全部编码'));
}
function switchModel() {
if (!engine || !engine.engine || !engine.engine.isInitialized()) {
alert('请先初始化 3D 引擎!');

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

View File

@@ -32,9 +32,10 @@
| 管理器 | 职责 |
|--------|------|
| EngineManager | 3D 引擎管理 |
| ToolbarManager | 工具栏管理 |
| ToolbarManager | 底部工具栏管理(传统线性布局,当前未使用) |
| **RadialToolbarManager** | **径向工具栏管理(当前主交互入口)** |
| DialogManager | 对话框管理 |
| ButtonGroupManager | 按钮组管理 |
| ButtonGroupManager | 按钮组管理(通用,当前未使用) |
| RightKeyManager | 右键菜单管理 |
### 功能管理器
@@ -75,6 +76,7 @@
| BimTree | 树形控件 |
| BimMenu | 菜单组件 |
| BimButtonGroup | 按钮组 |
| **RadialToolbar** | **径向工具栏(圆形扇形菜单,当前主交互入口)** |
### 面板组件

View File

@@ -18,6 +18,7 @@
src/managers/
├── engine-manager.ts
├── toolbar-manager.ts
├── radial-toolbar-manager.ts
├── dialog-manager.ts
├── button-group-manager.ts
├── right-key-manager.ts
@@ -44,6 +45,7 @@ src/managers/
BaseManager
├── EngineManager
├── ToolbarManager
├── **RadialToolbarManager**
├── DialogManager
├── ButtonGroupManager
├── RightKeyManager
@@ -127,7 +129,7 @@ class EngineManager extends BaseManager {
| 类别 | 代表 Manager | 主要职责 |
|---|---|---|
| 核心入口 | `EngineManager` | 引擎生命周期与外部 API |
| UI 容器 | `ToolbarManager`, `DialogManager`, `ButtonGroupManager` | UI 容器与通用交互 |
| UI 容器 | `RadialToolbarManager`, `ToolbarManager`, `DialogManager`, `ButtonGroupManager` | UI 容器与通用交互 |
| 业务编排 | `MeasureDialogManager`, `Section*DialogManager`, `WalkControlManager`, `SettingDialogManager` | 对话框/面板回调与引擎能力编排 |
| 数据/交互 | `ConstructTreeManagerBtn`, `ComponentDetailManager`, `RightKeyManager` | 构件树、属性、右键菜单 |

View File

@@ -20,6 +20,7 @@ src/components/
├── tree/ # 树形控件组件
├── menu/ # 菜单组件
├── button-group/ # 按钮组件
├── radial-toolbar/ # 径向工具栏组件
├── collapse/ # 折叠面板组件
├── tab/ # 标签页组件
├── description/ # 描述列表组件
@@ -408,6 +409,81 @@ class BimButtonGroup {
---
## RadialToolbar径向工具栏组件
### 概述
圆形径向菜单组件用于替代传统线性工具栏。支持多环布局、扇形展开、hover 触发和 toggle 状态。
### 配置
```typescript
interface RadialToolbarOptions {
container: HTMLElement;
items?: RadialMenuItem[];
mainButtonIcon?: string;
mainButtonLabel?: string;
onMainButtonClick?: () => void;
itemsPerRing?: number;
closeDelay?: number;
}
interface RadialMenuItem {
id: string;
label: string;
/** 直接显示的文本(优先级高于 label 的国际化翻译) */
title?: string;
icon?: string;
onClick?: (item: RadialMenuItem) => void;
isToggle?: boolean;
isActive?: boolean;
onToggle?: (nextActive: boolean, item: RadialMenuItem) => void;
}
```
### API
```typescript
class RadialToolbar {
init(): void;
addItem(item: RadialMenuItem): void;
setItemActive(id: string, active: boolean): void;
setTheme(theme: ThemeConfig): void;
setLocales(): void;
destroy(): void;
}
```
### 使用说明
- `label`:国际化键名(如 `'toolbar.home'`SDK 内置按钮使用
- `title`:直接显示文本,优先级高于 `label`,适合外部动态添加按钮
- `itemsPerRing`:每环最多显示的按钮数,超出自动进入下一环
- `isToggle`:是否为切换按钮,配合 `onToggle``setItemActive` 使用
### 布局说明
**角度计算**:按钮中心均匀分布在扇形范围内(默认 180°~270°通过 `(index + 0.5) / count` 计算位置,确保完整填满扇形而不贴边。
**环容量**:每环按钮数量写死在 `src/components/radial-toolbar/index.ts` 中(当前为 `[4, 6, 8]`),如需调整直接修改源码。
### 与 BottomDock 联动
`RadialToolbarManager` 会自动监听 `BottomDock` 状态变化,同步按钮激活状态:
```typescript
// 点击径向工具栏按钮展开/收起 Dock
onToggle: (active) => {
if (active) {
bimEngine.bottomDock?.open('measure');
} else {
bimEngine.bottomDock?.close('measure');
}
}
```
---
## BimCollapse折叠面板组件
### 概述

View File

@@ -172,3 +172,163 @@ bimEngine.engine?.getEngineComponent()?.isolateModels([
{ url: modelUrl, ids: [350518] }
]);
```
---
## 6) 启动一键编码 `startOneClickEncoding()`
### 所属模块
- 调用入口:`bimEngine.engine?.getEngineComponent()`
- 源码位置:`src/components/engine/index.ts`
### 方法签名
```ts
startOneClickEncoding(): void
```
### 入参
- 无入参。
### 行为约定
- 调用前建议先订阅 `encoding:start``encoding:complete``encoding:error` 事件以获取编码进度和结果。
- 若引擎未初始化或底层 `oneClickEncoding` 模块不可用,会在控制台输出警告并静默返回。
- 多次调用将重复触发编码流程(底层行为由 `iflow-engine-base` 决定)。
### 调用示例
```ts
const engineComp = bimEngine.engine?.getEngineComponent();
// 订阅编码事件
bimEngine.on('encoding:start', (data) => {
console.log('编码开始', data);
});
bimEngine.on('encoding:complete', (data) => {
console.log('编码完成', data);
});
bimEngine.on('encoding:error', (data) => {
console.log('编码失败', data);
});
// 启动编码
engineComp?.startOneClickEncoding();
```
---
## 7) 检查模型编码 `hasModelCode()`
### 所属模块
- 调用入口:`bimEngine.engine?.getEngineComponent()`
- 源码位置:`src/components/engine/index.ts`
### 方法签名
```ts
hasModelCode(): boolean
```
### 入参
- 无入参。
### 返回值
- `true`:所有已加载模型均已存在编码数据。
- `false`:至少有一个模型没有编码数据,或引擎未初始化、`oneClickEncoding` 模块不可用。
### 行为约定
- 遍历当前 `engine.models` 数组,对每个模型调用底层 `oneClickEncoding.exitModelCode(url)`
- 仅当所有模型的返回值为 `true` 时,才返回 `true`
- 若当前没有加载任何模型,返回 `false`
### 调用示例
```ts
const hasCode = bimEngine.engine?.getEngineComponent()?.hasModelCode();
console.log('模型是否已编码:', hasCode);
```
---
## 8) 读取缓存编码 `readModelCodeFormStoge()`
### 所属模块
- 调用入口:`bimEngine.engine?.getEngineComponent()`
- 源码位置:`src/components/engine/index.ts`
### 方法签名
```ts
readModelCodeFormStoge(): void
```
### 入参
- 无入参。
### 行为约定
- 遍历当前 `engine.models` 数组,对每个模型调用底层 `oneClickEncoding.readModelCodeFormStoge(url)`
- 若引擎未初始化或底层模块不可用,会在控制台输出警告并静默返回。
- 无返回值,仅触发读取动作。
### 调用示例
```ts
bimEngine.engine?.getEngineComponent()?.readModelCodeFormStoge();
```
---
## 9) 一键编码事件
### 所属模块
- 事件总线:`bimEngine`(通过 `ManagerRegistry` 桥接)
- 源码位置:`src/components/engine/index.ts`
### 事件列表
| 事件名 | 触发时机 | payload |
|--------|---------|---------|
| `encoding:start` | 编码流程开始时 | `{ data?: any }` |
| `encoding:complete` | 编码流程成功完成时 | `{ data?: any }` |
| `encoding:error` | 编码流程失败时 | `{ data?: any }` |
### 行为约定
- 事件由底层 `oneClickEncoding` 模块触发,经 `Engine` 组件桥接后冒泡到 `bimEngine` 事件总线。
- 订阅方式与 SDK 其他事件一致:使用 `bimEngine.on(event, handler)`,返回的函数可用于取消订阅。
- 建议在调用 `startOneClickEncoding()` 之前完成事件订阅,避免漏掉 `encoding:start` 事件。
### 调用示例
```ts
const unsubStart = bimEngine.on('encoding:start', (data) => {
console.log('编码开始', data);
});
const unsubComplete = bimEngine.on('encoding:complete', (data) => {
console.log('编码完成', data);
});
const unsubError = bimEngine.on('encoding:error', (data) => {
console.log('编码失败', data);
});
// 启动编码
bimEngine.engine?.getEngineComponent()?.startOneClickEncoding();
// 需要时取消订阅
// unsubStart();
// unsubComplete();
// unsubError();
```

12
package-lock.json generated
View File

@@ -1,15 +1,15 @@
{
"name": "iflow-engine",
"version": "2.5.9",
"version": "2.5.13",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "iflow-engine",
"version": "2.5.9",
"version": "2.5.13",
"license": "MIT",
"dependencies": {
"iflow-engine-base": "^3.4.10",
"iflow-engine-base": "^3.5.0",
"three": "^0.182.0"
},
"devDependencies": {
@@ -1823,9 +1823,9 @@
}
},
"node_modules/iflow-engine-base": {
"version": "3.4.10",
"resolved": "https://registry.npmjs.org/iflow-engine-base/-/iflow-engine-base-3.4.10.tgz",
"integrity": "sha512-sKgV1qWr8z3Cbli3yoGRolAKz0Za5Iuycu/OAbpA2/oE9LJ+Zb0CH63LrSR7ljRK6NBRw4P0fHrs6/KCWppG7w==",
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/iflow-engine-base/-/iflow-engine-base-3.5.0.tgz",
"integrity": "sha512-d+jZQrD7nO1InfqVZfgvafUz4akFL8Kj0OBiw//A5Qs6yubiBER9Eey4mcMtM7F/yXFcNlJXvvmCo9J9tW6meQ==",
"license": "ISC",
"dependencies": {
"@types/three": "^0.181.0",

View File

@@ -1,6 +1,6 @@
{
"name": "iflow-engine",
"version": "2.5.10",
"version": "2.6.0",
"description": "iFlow Engine SDK for Vue2, Vue3, React and HTML",
"main": "./dist/iflow-engine.umd.js",
"module": "./dist/iflow-engine.es.js",
@@ -59,7 +59,7 @@
"vite-plugin-dts": "^4.5.4"
},
"dependencies": {
"iflow-engine-base": "^3.4.10",
"iflow-engine-base": "^3.5.0",
"three": "^0.182.0"
}
}

View File

@@ -11,7 +11,7 @@ import { themeManager } from './services/theme';
import type { LocaleType } from './locales/types';
import type { ThemeType } from './themes/types';
import type { EngineEvents } from './types/events';
import './iflow-engine-base.css';
/**
* BimEngine2d 构造选项
* 合并引擎配置与主题/语言设置

View File

@@ -11,6 +11,7 @@ import { themeManager } from './services/theme';
import type { LocaleType } from './locales/types';
import type { ThemeType } from './themes/types';
import type { EngineEvents } from './types/events';
import './iflow-engine-base.css';
/**
* BimEngine720 构造选项

View File

@@ -1,5 +1,6 @@
declare const __APP_VERSION__: string;
import './bim-engine.css';
import './iflow-engine-base.css';
import { DialogManager } from './managers/dialog-manager';
import { EngineManager } from './managers/engine-manager';
import { RightKeyManager } from './managers/right-key-manager';

View File

@@ -16,8 +16,6 @@ import type { SectionBoxRange } from '../section-box-panel/types';
import type { ManagerRegistry } from '../../core/manager-registry';
// 导入第三方 SDK 的 createEngine 函数(从 npm 包引入)
import { createEngine as createEngineSDK } from 'iflow-engine-base';
//import { createEngine as createEngineSDK } from '../../../../bim_engine_base/dist/bim-engine-sdk.es';
import "../../../../bim_engine_base/dist/iflow-engine-base.css"
export type {
EngineOptions,
@@ -177,6 +175,22 @@ export class Engine implements IBimComponent {
console.warn('[Engine] 底层引擎不支持 events.on 方法,无法监听点击事件');
}
const oneClickEncoding = (this.engine as any).oneClickEncoding;
if (oneClickEncoding?.on) {
oneClickEncoding.on('encoding-start', (data: any) => {
console.log('[Engine] 底层 encoding-start 事件触发:', data);
this.registry.emit('encoding:start', data);
});
oneClickEncoding.on('encoding-complete', (data: any) => {
console.log('[Engine] 底层 encoding-complete 事件触发:', data);
this.registry.emit('encoding:complete', data);
});
oneClickEncoding.on('encoding-error', (data: any) => {
console.log('[Engine] 底层 encoding-error 事件触发:', data);
this.registry.emit('encoding:error', data);
});
}
} catch (error) {
console.error('[Engine] Failed to initialize engine:', error);
this._isInitialized = false;
@@ -252,6 +266,36 @@ export class Engine implements IBimComponent {
this.engine?.events?.off(event, handler);
}
/**
* 订阅一键编码事件
* @param event 事件名称:'encoding-start' | 'encoding-complete' | 'encoding-error'
* @param handler 事件处理函数
*/
public onOneClickEncodingEvent(event: string, handler: (...args: any[]) => void): void {
this.engine?.oneClickEncoding?.on(event, handler);
}
/**
* 取消订阅一键编码事件
* @param event 事件名称
* @param handler 事件处理函数
*/
public offOneClickEncodingEvent(event: string, handler: (...args: any[]) => void): void {
this.engine?.oneClickEncoding?.off(event, handler);
}
/**
* 启动一键编码
* @remarks 调用底层 engine.oneClickEncoding.start(),需先订阅相关事件
*/
public startOneClickEncoding(): void {
if (!this._isInitialized || !this.engine?.oneClickEncoding) {
console.warn('[Engine] oneClickEncoding not available.');
return;
}
this.engine.oneClickEncoding.start();
}
/**
* 暂停渲染
*/
@@ -631,6 +675,32 @@ export class Engine implements IBimComponent {
this.engine.clipping.reverse();
}
/**
* 设置剖切面填充开关
* @param enabled 是否启用填充
* @remarks 对接底层 `engine.clipping.setFillCutFace(enabled)`
*/
public setFillCutFace(enabled: boolean): void {
if (!this._isInitialized || !this.engine?.clipping) {
console.error('[Engine] Cannot set fill cut face: engine not initialized.');
return;
}
console.log('[Engine] Setting fill cut face:', enabled);
this.engine.clipping.setFillCutFace(enabled);
}
/**
* 获取剖切面填充状态
* @returns true=已启用填充false=未启用
* @remarks 对接底层 `engine.clipping.getFillCutFace()`
*/
public getFillCutFace(): boolean {
if (!this._isInitialized || !this.engine?.clipping) {
return false;
}
return this.engine.clipping.getFillCutFace?.() ?? false;
}
// ==================== 结束:剖切功能 ====================
// ==================== 相机切换 ====================
@@ -1767,6 +1837,49 @@ export class Engine implements IBimComponent {
// ==================== 结束:构件操作 ====================
/**
* 读取所有已加载模型的缓存编码数据
* @remarks 遍历 engine.models对每个模型调用 oneClickEncoding.readModelCodeFormStoge(url)
*/
public readModelCodeFormStoge(): void {
if (!this._isInitialized || !this.engine) {
console.warn('[Engine] Cannot read model code form stoge: engine not initialized.');
return;
}
let count = 0;
this.engine.models.forEach((m: any) => {
this.engine.oneClickEncoding.readModelCodeFormStoge(m.url);
count++;
});
console.log('readModelCodeFormStoge-success:', count);
}
/**
* 检查所有已加载模型是否已有编码数据
* @returns 当所有模型都存在编码时返回 true否则返回 false
*/
public hasModelCode(): boolean {
if (!this._isInitialized || !this.engine) {
console.warn('[Engine] Cannot check model code: engine not initialized.');
return false;
}
const models = (this.engine as any).models;
if (!Array.isArray(models) || models.length === 0) {
return false;
}
const oneClickEncoding = (this.engine as any).oneClickEncoding;
if (!oneClickEncoding || typeof oneClickEncoding.exitModelCode !== 'function') {
console.warn('[Engine] oneClickEncoding.exitModelCode is not available.');
return false;
}
return models.every((m: any) => {
if (!m || typeof m.url !== 'string') {
return false;
}
return oneClickEncoding.exitModelCode(m.url) === true;
});
}
/**
* 销毁组件 (接口实现)
* 清理资源、取消订阅、销毁引擎实例

View File

@@ -25,29 +25,29 @@ export class RadialToolbar implements IBimComponent {
private maxInteractiveRadius = 220;
private readonly closeDelay: number;
private readonly itemsPerRing: number;
// 主按钮直径px
private readonly MAIN_BUTTON_SIZE = 60;
// 子按钮直径px
private readonly SUB_BUTTON_SIZE = 46;
// 第一环子按钮中心”到“主按钮中心的距离px
// 不做防重叠兜底,允许在小半径时出现重叠。
// 第一环"子按钮中心"到"主按钮中心"的距离px
private readonly BASE_RADIUS = 80;
// 多环时相邻两环的中心半径差px
private readonly RING_GAP = 40;
// 扇形展开角度范围:当前 180~270实际就是 90 度)
private readonly RING_GAP = 60;
// 扇形展开角度范围 (180为正左边/下270为正上)
private readonly FAN_START_DEG = 170;
private readonly FAN_END_DEG = 280;
// 扇形边缘留白px防止按钮贴边或裁切
private readonly CANVAS_PADDING = 28;
// 【自定义每环数量】:你可以随意修改这里的数字!多出来的按钮会自动折叠到更外围的新环。
private readonly FIXED_RING_CAPACITIES = [4, 6, 8];
constructor(options: RadialToolbarOptions) {
this.container = options.container;
this.items = options.items === undefined ? this.createDefaultItems() : [...options.items];
this.mainButtonLabel = options.mainButtonLabel ?? 'toolbar.home';
this.onMainButtonClick = options.onMainButtonClick;
this.itemsPerRing = Math.max(3, options.itemsPerRing ?? 5);
this.closeDelay = Math.max(100, options.closeDelay ?? 260);
this.wrapper = this.createWrapper();
@@ -223,19 +223,48 @@ export class RadialToolbar implements IBimComponent {
return btn;
}
/**
* 根据预设的数组计算容量(不足自动向后排)
*/
private getRingCapacities(total: number): number[] {
const capacities: number[] = [];
let remaining = total;
for (const cap of this.FIXED_RING_CAPACITIES) {
if (remaining <= 0) break;
const actualCount = Math.min(cap, remaining);
capacities.push(actualCount);
remaining -= actualCount;
}
// 如果配置的环数不够用,剩下的全部折叠到最后一环
if (remaining > 0) {
capacities.push(remaining);
}
return capacities;
}
private updateItemPositions(): void {
const total = this.itemElements.length;
const fanSpan = this.FAN_END_DEG - this.FAN_START_DEG;
if (total === 0) return;
const fanSpan = this.FAN_END_DEG - this.FAN_START_DEG;
const ringCapacities = this.getRingCapacities(total);
let globalIndex = 0;
ringCapacities.forEach((ringCount, ringIndex) => {
const radius = this.BASE_RADIUS + ringIndex * this.RING_GAP;
for (let ringLocalIndex = 0; ringLocalIndex < ringCount; ringLocalIndex++) {
const btn = this.itemElements[globalIndex];
// 【核心逻辑】:首尾固定:第一个按钮在最下面(180),最后一个在最上面(270),中间按剩余空间均分
// 如果该环只有1个按钮让它待在180度的起始位置
const ratio = ringCount <= 1 ? 0 : ringLocalIndex / (ringCount - 1);
this.itemElements.forEach((btn, globalIndex) => {
const ringIndex = Math.floor(globalIndex / this.itemsPerRing);
const ringStart = ringIndex * this.itemsPerRing;
const ringCount = Math.min(this.itemsPerRing, total - ringStart);
const ringLocalIndex = globalIndex - ringStart;
const ratio = ringCount === 1 ? 0.5 : ringLocalIndex / (ringCount - 1);
const angleDeg = this.FAN_START_DEG + fanSpan * ratio;
const angleRad = (angleDeg * Math.PI) / 180;
const radius = this.getBaseRadius(ringCount) + ringIndex * this.RING_GAP;
const x = Math.cos(angleRad) * radius;
const y = Math.sin(angleRad) * radius;
@@ -247,14 +276,16 @@ export class RadialToolbar implements IBimComponent {
btn.style.setProperty('--rt-y', `${y.toFixed(2)}px`);
btn.style.setProperty('--rt-open-delay', `${openDelay.toFixed(3)}s`);
btn.style.setProperty('--rt-close-delay', `${closeDelay.toFixed(3)}s`);
globalIndex++;
}
});
}
private updateLayoutMetrics(): void {
const total = this.items.length;
const ringCount = Math.max(1, Math.ceil(total / this.itemsPerRing));
const maxItemsPerRing = Math.max(1, Math.min(total, this.itemsPerRing));
const maxRadius = this.getBaseRadius(maxItemsPerRing) + (ringCount - 1) * this.RING_GAP;
const ringCapacities = this.getRingCapacities(total);
const ringCount = ringCapacities.length;
const maxRadius = this.BASE_RADIUS + (ringCount - 1) * this.RING_GAP;
const size = Math.ceil(maxRadius + this.MAIN_BUTTON_SIZE + this.SUB_BUTTON_SIZE + this.CANVAS_PADDING * 2);
this.maxInteractiveRadius = maxRadius + this.SUB_BUTTON_SIZE * 0.7;
@@ -265,10 +296,6 @@ export class RadialToolbar implements IBimComponent {
this.wrapper.style.setProperty('--rt-main-offset', `${(this.MAIN_BUTTON_SIZE - this.SUB_BUTTON_SIZE) / 2}px`);
}
private getBaseRadius(_itemsInRing: number): number {
return this.BASE_RADIUS;
}
private scheduleCollapse(): void {
if (this.timer) {
clearTimeout(this.timer);
@@ -354,7 +381,7 @@ export class RadialToolbar implements IBimComponent {
if (!item) {
return;
}
const text = t(item.label);
const text = item.title ?? t(item.label);
el.title = text;
el.setAttribute('aria-label', text);
this.applyItemActiveClass(el, item);
@@ -362,7 +389,7 @@ export class RadialToolbar implements IBimComponent {
if (!item.icon) {
const iconEl = el.querySelector('.radial-sub-btn-icon');
if (iconEl) {
iconEl.textContent = this.getFallbackLabel(item.label);
iconEl.textContent = this.getFallbackLabel(item.title ?? item.label);
}
}
});
@@ -431,6 +458,12 @@ export class RadialToolbar implements IBimComponent {
}
}
public addItem(item: RadialMenuItem): void {
this.items.push(item);
this.renderItems();
this.updateLayoutMetrics();
}
public init(): void { }
public setLocales(): void {

View File

@@ -1,6 +1,8 @@
export interface RadialMenuItem {
id: string;
label: string;
/** 直接显示的文本(优先级高于 label 的国际化翻译) */
title?: string;
icon?: string;
onClick?: (item: RadialMenuItem) => void;
isToggle?: boolean;
@@ -14,6 +16,5 @@ export interface RadialToolbarOptions {
mainButtonIcon?: string;
mainButtonLabel?: string;
onMainButtonClick?: () => void;
itemsPerRing?: number;
closeDelay?: number;
}

View File

@@ -15,15 +15,16 @@ export class SectionAxisPanel implements IBimComponent {
public element: HTMLElement;
private options: SectionAxisPanelOptions;
// 状态
private isHidden: boolean = false;
private isFilled: boolean = false;
private activeAxis: SectionAxis = 'x';
// DOM 引用 - 第一行
private hideBtn!: HTMLButtonElement;
private reverseBtn!: HTMLButtonElement;
private fillBtn!: HTMLButtonElement;
private hideLabelEl!: HTMLElement;
private reverseLabelEl!: HTMLElement;
private fillLabelEl!: HTMLElement;
// DOM 引用 - 第二行
private axisXBtn!: HTMLButtonElement;
@@ -37,6 +38,7 @@ export class SectionAxisPanel implements IBimComponent {
constructor(options: SectionAxisPanelOptions = {}) {
this.options = options;
this.isHidden = options.defaultHidden ?? false;
this.isFilled = options.defaultFill ?? false;
this.activeAxis = options.defaultAxis ?? 'x';
this.element = this.createDom();
}
@@ -60,7 +62,7 @@ export class SectionAxisPanel implements IBimComponent {
this.setTheme(themeManager.getTheme());
// 初始化按钮状态
this.updateHideButtonState();
this.updateButtonStates();
this.updateAxisButtonsState();
}
@@ -86,41 +88,39 @@ export class SectionAxisPanel implements IBimComponent {
public setLocales(): void {
this.hideLabelEl.textContent = t('sectionAxis.actions.hide');
this.reverseLabelEl.textContent = t('sectionAxis.actions.reverse');
// XYZ按钮的文字不需要国际化保持为单个字母
this.fillLabelEl.textContent = t('sectionAxis.actions.fill');
this.hideBtn.title = t('sectionAxis.actions.hide');
this.reverseBtn.title = t('sectionAxis.actions.reverse');
this.fillBtn.title = t('sectionAxis.actions.fill');
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();
this.updateButtonStates();
}
/**
* 获取隐藏状态
*/
public getHiddenState(): boolean {
return this.isHidden;
}
/**
* 设置激活的轴向
*/
public setFillState(isFilled: boolean): void {
this.isFilled = isFilled;
this.updateButtonStates();
}
public getFillState(): boolean {
return this.isFilled;
}
public setActiveAxis(axis: SectionAxis): void {
this.activeAxis = axis;
this.updateAxisButtonsState();
}
/**
* 获取激活的轴向
*/
public getActiveAxis(): SectionAxis {
return this.activeAxis;
}
@@ -151,6 +151,12 @@ export class SectionAxisPanel implements IBimComponent {
const row1 = document.createElement('div');
row1.className = 'section-axis-row-1';
this.fillBtn = this.createButton(
'fill',
getIcon('填充'),
() => this.handleFillToggle()
);
this.hideBtn = this.createButton(
'hide',
getIcon('隐藏'),
@@ -163,6 +169,7 @@ export class SectionAxisPanel implements IBimComponent {
() => this.handleReverse()
);
row1.appendChild(this.fillBtn);
row1.appendChild(this.hideBtn);
row1.appendChild(this.reverseBtn);
@@ -188,7 +195,7 @@ export class SectionAxisPanel implements IBimComponent {
* 创建按钮(带图标)
*/
private createButton(
type: 'hide' | 'reverse',
type: 'hide' | 'reverse' | 'fill',
iconSvg: string,
onClick: () => void
): HTMLButtonElement {
@@ -207,11 +214,12 @@ export class SectionAxisPanel implements IBimComponent {
label.className = 'section-axis-btn-label';
btn.appendChild(label);
// 保存 label 引用
if (type === 'hide') {
this.hideLabelEl = label;
} else if (type === 'reverse') {
this.reverseLabelEl = label;
} else if (type === 'fill') {
this.fillLabelEl = label;
}
// 点击事件
@@ -249,16 +257,22 @@ export class SectionAxisPanel implements IBimComponent {
*/
private handleHideToggle(): void {
this.isHidden = !this.isHidden;
this.updateHideButtonState();
this.updateButtonStates();
if (this.options.onHideToggle) {
this.options.onHideToggle(this.isHidden);
}
}
/**
* 处理反向按钮点击
*/
private handleFillToggle(): void {
this.isFilled = !this.isFilled;
this.updateButtonStates();
if (this.options.onFillToggle) {
this.options.onFillToggle(this.isFilled);
}
}
private handleReverse(): void {
if (this.options.onReverse) {
this.options.onReverse();
@@ -281,15 +295,9 @@ export class SectionAxisPanel implements IBimComponent {
}
}
/**
* 更新隐藏按钮状态
*/
private updateHideButtonState(): void {
if (this.isHidden) {
this.hideBtn.classList.add('active');
} else {
this.hideBtn.classList.remove('active');
}
private updateButtonStates(): void {
this.hideBtn.classList.toggle('active', this.isHidden);
this.fillBtn.classList.toggle('active', this.isFilled);
}
/**

View File

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

View File

@@ -18,15 +18,18 @@ export class SectionBoxPanel implements IBimComponent {
private isHidden: boolean = false;
private isReversed: boolean = false;
private isFilled: boolean = false;
private range: SectionBoxRange;
private hideBtn!: HTMLButtonElement;
private fitBtn!: HTMLButtonElement;
private resetBtn!: HTMLButtonElement;
private fillBtn!: HTMLButtonElement;
private hideLabelEl!: HTMLElement;
private fitLabelEl!: HTMLElement;
private resetLabelEl!: HTMLElement;
private fillLabelEl!: HTMLElement;
private xLabelEl!: HTMLElement;
private yLabelEl!: HTMLElement;
private zLabelEl!: HTMLElement;
@@ -61,6 +64,7 @@ export class SectionBoxPanel implements IBimComponent {
this.options = options;
this.isHidden = options.defaultHidden ?? false;
this.isReversed = options.defaultReversed ?? false;
this.isFilled = options.defaultFill ?? false;
this.range = JSON.parse(JSON.stringify(options.defaultRange ?? DEFAULT_RANGE));
}
@@ -96,6 +100,15 @@ export class SectionBoxPanel implements IBimComponent {
return this.isReversed;
}
public setFillState(isFilled: boolean): void {
this.isFilled = isFilled;
this.updateButtonStates();
}
public getFillState(): boolean {
return this.isFilled;
}
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 };
@@ -126,6 +139,12 @@ export class SectionBoxPanel implements IBimComponent {
const buttonsContainer = document.createElement('div');
buttonsContainer.className = 'section-box-row-buttons';
this.fillBtn = this.createButton('fill', t('sectionBox.actions.fill'), () => {
this.isFilled = !this.isFilled;
this.updateButtonStates();
this.options.onFillToggle?.(this.isFilled);
}, 'fill');
this.hideBtn = this.createButton('hide', t('sectionBox.actions.hide'), () => {
this.isHidden = !this.isHidden;
this.updateButtonStates();
@@ -138,7 +157,7 @@ export class SectionBoxPanel implements IBimComponent {
this.resetBtn = this.createButton('reset', t('sectionBox.actions.reset'), () => this.reset(), 'reset');
[this.hideBtn, this.fitBtn, this.resetBtn].forEach(btn => buttonsContainer.appendChild(btn));
[this.fillBtn, this.hideBtn, this.fitBtn, this.resetBtn].forEach(btn => buttonsContainer.appendChild(btn));
const slidersContainer = document.createElement('div');
slidersContainer.className = 'section-box-sliders';
@@ -164,7 +183,8 @@ export class SectionBoxPanel implements IBimComponent {
hide: '隐藏',
reverse: '反向',
fit: '适应到模型',
reset: '重置'
reset: '重置',
fill: '填充'
};
const icon = document.createElement('div');
@@ -178,6 +198,7 @@ export class SectionBoxPanel implements IBimComponent {
if (ref === 'hide') this.hideLabelEl = labelEl;
else if (ref === 'fit') this.fitLabelEl = labelEl;
else if (ref === 'reset') this.resetLabelEl = labelEl;
else if (ref === 'fill') this.fillLabelEl = labelEl;
btn.appendChild(icon);
btn.appendChild(labelEl);
@@ -320,12 +341,14 @@ export class SectionBoxPanel implements IBimComponent {
public setLocales(): void {
if (!this.hideLabelEl) return;
this.fillLabelEl.textContent = t('sectionBox.actions.fill');
this.hideLabelEl.textContent = t('sectionBox.actions.hide');
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.fillBtn.title = t('sectionBox.actions.fill');
this.hideBtn.title = t('sectionBox.actions.hide');
this.fitBtn.title = t('sectionBox.actions.fitToModel');
this.resetBtn.title = t('sectionBox.actions.reset');

View File

@@ -24,46 +24,14 @@ export interface SectionBoxRange {
* 剖切盒面板配置选项
*/
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
*/
onFillToggle?: (isFilled: boolean) => void;
defaultHidden?: boolean;
/**
* 默认反向状态(默认 false
*/
defaultReversed?: boolean;
/**
* 默认范围值
*/
defaultFill?: boolean;
defaultRange?: SectionBoxRange;
}

View File

@@ -11,16 +11,18 @@ export interface SectionDockPanelOptions {
defaultType?: SectionDockType | null;
defaultAxis?: SectionDockAxis;
defaultHidden?: boolean;
defaultFill?: boolean;
onTypeChange?: (type: SectionDockType, axis: SectionDockAxis) => void;
onAxisChange?: (axis: SectionDockAxis) => void;
onHideToggle?: (hidden: boolean) => void;
onReverse?: () => void;
onReset?: (type: SectionDockType, axis: SectionDockAxis) => void;
onFitToModel?: () => void;
onFillToggle?: (isFilled: boolean) => void;
}
interface ToolDefinition {
key: 'hide' | 'reverse' | 'reset' | 'fit';
key: 'hide' | 'reverse' | 'reset' | 'fit' | 'fill';
iconName: string;
textKey: string;
onClick: () => void;
@@ -40,12 +42,14 @@ export class SectionDockPanel implements IBimComponent {
private activeType: SectionDockType | null;
private activeAxis: SectionDockAxis;
private isHidden: boolean;
private isFilled: boolean;
constructor(options: SectionDockPanelOptions = {}) {
this.options = options;
this.activeType = options.defaultType ?? null;
this.activeAxis = options.defaultAxis ?? 'x';
this.isHidden = options.defaultHidden ?? false;
this.isFilled = options.defaultFill ?? false;
const { root, toolContainer, axisPanel, divider } = this.createDom();
this.element = root;
@@ -112,6 +116,15 @@ export class SectionDockPanel implements IBimComponent {
return this.activeAxis;
}
public setFillState(isFilled: boolean): void {
this.isFilled = isFilled;
this.renderTools();
}
public getFillState(): boolean {
return this.isFilled;
}
private createDom(): { root: HTMLElement; toolContainer: HTMLElement; axisPanel: HTMLElement; divider: HTMLElement } {
const root = document.createElement('div');
root.className = 'section-dock-panel';
@@ -257,6 +270,18 @@ export class SectionDockPanel implements IBimComponent {
}
private getToolsForType(type: SectionDockType): ToolDefinition[] {
const fillTool: ToolDefinition = {
key: 'fill',
iconName: '填充',
textKey: type === 'box' ? 'sectionBox.actions.fill' : type === 'axis' ? 'sectionAxis.actions.fill' : 'sectionPlane.actions.fill',
isActive: this.isFilled,
onClick: () => {
this.isFilled = !this.isFilled;
this.options.onFillToggle?.(this.isFilled);
this.renderTools();
}
};
const hideTool: ToolDefinition = {
key: 'hide',
iconName: '隐藏',
@@ -280,6 +305,7 @@ export class SectionDockPanel implements IBimComponent {
if (type === 'axis') {
return [
fillTool,
hideTool,
reverseTool,
{
@@ -297,6 +323,7 @@ export class SectionDockPanel implements IBimComponent {
if (type === 'box') {
return [
fillTool,
hideTool,
{
key: 'fit',
@@ -320,6 +347,7 @@ export class SectionDockPanel implements IBimComponent {
}
return [
fillTool,
hideTool,
reverseTool,
{

View File

@@ -15,14 +15,17 @@ export class SectionPlanePanel implements IBimComponent {
private options: SectionPlanePanelOptions;
private isHidden: boolean = false;
private isFilled: boolean = false;
// DOM 引用
private hideBtn!: HTMLButtonElement;
private reverseBtn!: HTMLButtonElement;
private resetBtn!: HTMLButtonElement;
private fillBtn!: HTMLButtonElement;
private hideLabelEl!: HTMLElement;
private reverseLabelEl!: HTMLElement;
private resetLabelEl!: HTMLElement;
private fillLabelEl!: HTMLElement;
// 订阅清理
private unsubscribeLocale: (() => void) | null = null;
@@ -31,6 +34,7 @@ export class SectionPlanePanel implements IBimComponent {
constructor(options: SectionPlanePanelOptions = {}) {
this.options = options;
this.isHidden = options.defaultHidden ?? false;
this.isFilled = options.defaultFill ?? false;
this.element = this.createDom();
}
@@ -43,8 +47,18 @@ export class SectionPlanePanel implements IBimComponent {
return this.isHidden;
}
public setFillState(isFilled: boolean): void {
this.isFilled = isFilled;
this.updateButtonStates();
}
public getFillState(): boolean {
return this.isFilled;
}
private updateButtonStates(): void {
this.hideBtn?.classList.toggle('active', this.isHidden);
this.fillBtn?.classList.toggle('active', this.isFilled);
}
/**
@@ -88,10 +102,12 @@ export class SectionPlanePanel implements IBimComponent {
this.hideLabelEl.textContent = t('sectionPlane.actions.hide');
this.reverseLabelEl.textContent = t('sectionPlane.actions.reverse');
this.resetLabelEl.textContent = t('sectionPlane.actions.reset');
this.fillLabelEl.textContent = t('sectionPlane.actions.fill');
this.hideBtn.title = t('sectionPlane.actions.hide');
this.reverseBtn.title = t('sectionPlane.actions.reverse');
this.resetBtn.title = t('sectionPlane.actions.reset');
this.fillBtn.title = t('sectionPlane.actions.fill');
}
/**
@@ -116,6 +132,17 @@ export class SectionPlanePanel implements IBimComponent {
const root = document.createElement('div');
root.className = 'section-plane-panel';
// 填充按钮
this.fillBtn = this.createButton(
'fill',
getIcon('填充'),
() => {
this.isFilled = !this.isFilled;
this.updateButtonStates();
this.options.onFillToggle?.(this.isFilled);
}
);
// 隐藏按钮
this.hideBtn = this.createButton(
'hide',
@@ -149,6 +176,7 @@ export class SectionPlanePanel implements IBimComponent {
}
);
root.appendChild(this.fillBtn);
root.appendChild(this.hideBtn);
root.appendChild(this.reverseBtn);
root.appendChild(this.resetBtn);
@@ -159,7 +187,7 @@ export class SectionPlanePanel implements IBimComponent {
/**
* 创建按钮
*/
private createButton(type: 'hide' | 'reverse' | 'reset', iconSvg: string, onClick: () => void): HTMLButtonElement {
private createButton(type: 'hide' | 'reverse' | 'reset' | 'fill', iconSvg: string, onClick: () => void): HTMLButtonElement {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'section-plane-btn';
@@ -175,11 +203,12 @@ export class SectionPlanePanel implements IBimComponent {
label.className = 'section-plane-btn-label';
btn.appendChild(label);
// 保存 label 引用
if (type === 'hide') {
this.hideLabelEl = label;
} else if (type === 'reverse') {
this.reverseLabelEl = label;
} else if (type === 'fill') {
this.fillLabelEl = label;
} else {
this.resetLabelEl = label;
}

View File

@@ -7,12 +7,23 @@ export interface SectionPlanePanelOptions {
*/
defaultHidden?: boolean;
/**
* 初始填充状态
*/
defaultFill?: boolean;
/**
* 隐藏状态切换回调
* @param isHidden 是否隐藏
*/
onHideToggle?: (isHidden: boolean) => void;
/**
* 填充状态切换回调
* @param isFilled 是否填充
*/
onFillToggle?: (isFilled: boolean) => void;
/**
* 反向按钮回调
*/

File diff suppressed because one or more lines are too long

View File

@@ -135,7 +135,8 @@ export const enUS: TranslationDictionary = {
actions: {
hide: 'Hide',
reverse: 'Reverse',
reset: 'Reset'
reset: 'Reset',
fill: 'Fill'
}
},
sectionAxis: {
@@ -146,7 +147,8 @@ export const enUS: TranslationDictionary = {
reset: 'Reset',
axisX: 'X',
axisY: 'Y',
axisZ: 'Z'
axisZ: 'Z',
fill: 'Fill'
}
},
sectionBox: {
@@ -155,7 +157,8 @@ export const enUS: TranslationDictionary = {
hide: 'Hide',
reverse: 'Reverse',
fitToModel: 'Fit',
reset: 'Reset'
reset: 'Reset',
fill: 'Fill'
},
axes: {
x: 'X',

View File

@@ -147,6 +147,7 @@ export interface TranslationDictionary {
hide: string;
reverse: string;
reset: string;
fill: string;
};
};
sectionAxis: {
@@ -158,6 +159,7 @@ export interface TranslationDictionary {
axisX: string;
axisY: string;
axisZ: string;
fill: string;
};
};
sectionBox: {
@@ -167,6 +169,7 @@ export interface TranslationDictionary {
reverse: string;
fitToModel: string;
reset: string;
fill: string;
};
axes: {
x: string;

View File

@@ -135,7 +135,8 @@ export const zhCN: TranslationDictionary = {
actions: {
hide: '隐藏',
reverse: '反向',
reset: '重置'
reset: '重置',
fill: '填充'
}
},
sectionAxis: {
@@ -146,7 +147,8 @@ export const zhCN: TranslationDictionary = {
reset: '重置',
axisX: 'X',
axisY: 'Y',
axisZ: 'Z'
axisZ: 'Z',
fill: '填充'
}
},
sectionBox: {
@@ -155,7 +157,8 @@ export const zhCN: TranslationDictionary = {
hide: '隐藏',
reverse: '反向',
fitToModel: '适应',
reset: '重置'
reset: '重置',
fill: '填充'
},
axes: {
x: 'X',

View File

@@ -135,7 +135,8 @@ export const zhTW: TranslationDictionary = {
actions: {
hide: '隱藏',
reverse: '反向',
reset: '重設'
reset: '重設',
fill: '填充'
}
},
sectionAxis: {
@@ -146,7 +147,8 @@ export const zhTW: TranslationDictionary = {
reset: '重設',
axisX: 'X',
axisY: 'Y',
axisZ: 'Z'
axisZ: 'Z',
fill: '填充'
}
},
sectionBox: {
@@ -155,7 +157,8 @@ export const zhTW: TranslationDictionary = {
hide: '隱藏',
reverse: '反向',
fitToModel: '適應',
reset: '重設'
reset: '重設',
fill: '填充'
},
axes: {
x: 'X',

View File

@@ -12,7 +12,6 @@ import { createWalkRadialButton } from '../components/radial-toolbar/buttons/wal
export interface RadialToolbarManagerOptions {
items?: RadialMenuItem[];
itemsPerRing?: number;
}
export class RadialToolbarManager extends BaseManager {
@@ -24,7 +23,6 @@ export class RadialToolbarManager extends BaseManager {
this.toolbar = new RadialToolbar({
container,
items: options?.items ?? this.createDefaultItems(),
itemsPerRing: options?.itemsPerRing ?? 4,
mainButtonIcon: getIcon('主视角'),
mainButtonLabel: 'toolbar.home',
onMainButtonClick: () => {
@@ -61,6 +59,10 @@ export class RadialToolbarManager extends BaseManager {
this.toolbar.setItemActive('walk', dock.isOpen('walk'));
}
public addItem(item: RadialMenuItem): void {
this.toolbar?.addItem(item);
}
public destroy(): void {
if (this.unsubscribeDockState) {
this.unsubscribeDockState();

View File

@@ -61,6 +61,7 @@ export class SectionAxisDialogManager extends BaseDialogManager {
this.panel = new SectionAxisPanel({
defaultAxis: 'x',
defaultHidden: false,
defaultFill: this.engineComponent?.getFillCutFace() ?? false,
onHideToggle: (isHidden) => {
console.log('[SectionAxisDialogManager] 隐藏切换:', isHidden);
if (isHidden) {
@@ -69,6 +70,9 @@ export class SectionAxisDialogManager extends BaseDialogManager {
this.engineComponent?.recoverSection();
}
},
onFillToggle: (isFilled) => {
this.engineComponent?.setFillCutFace(isFilled);
},
onReverse: () => {
this.engineComponent?.reverseSection();
},

View File

@@ -53,6 +53,7 @@ export class SectionBoxDialogManager extends BaseDialogManager {
this.panel = new SectionBoxPanel({
defaultHidden: false,
defaultReversed: false,
defaultFill: this.engineComponent?.getFillCutFace() ?? false,
onHideToggle: (isHidden) => {
console.log('[SectionBoxDialogManager] 隐藏切换:', isHidden);
if (isHidden) {
@@ -61,6 +62,9 @@ export class SectionBoxDialogManager extends BaseDialogManager {
this.engineComponent?.recoverSection();
}
},
onFillToggle: (isFilled) => {
this.engineComponent?.setFillCutFace(isFilled);
},
onFitToModel: () => {
// 对接底层 scaleBox():缩放剖切盒到场景整体包围盒
this.engineComponent?.scaleSectionBox();
@@ -70,7 +74,7 @@ export class SectionBoxDialogManager extends BaseDialogManager {
// UI 侧会自行将滑块强制恢复到 0-100并将隐藏/反向按钮恢复为关闭状态。
this.engineComponent?.deactivateSection();
this.engineComponent?.activeSection('box');
// 确保剖切可见(避免上一次处于隐藏状态导致看起来没重置
// 确保剖切可见(避免上一次处于隐藏状态导致"看起来没重置"
this.engineComponent?.recoverSection();
},
onRangeChange: (range) => {

View File

@@ -91,6 +91,10 @@ export class SectionDockManager extends BaseManager {
},
onFitToModel: () => {
this.engineComponent?.scaleSectionBox();
},
defaultFill: this.engineComponent?.getFillCutFace() ?? false,
onFillToggle: (isFilled) => {
this.engineComponent?.setFillCutFace(isFilled);
}
});
this.panel.init();

View File

@@ -53,6 +53,7 @@ export class SectionPlaneDialogManager extends BaseDialogManager {
protected createContent(): HTMLElement {
this.panel = new SectionPlanePanel({
defaultHidden: false,
defaultFill: this.engineComponent?.getFillCutFace() ?? false,
onHideToggle: (isHidden) => {
console.log('[SectionPlaneDialogManager] 隐藏切换:', isHidden);
if (isHidden) {
@@ -61,6 +62,9 @@ export class SectionPlaneDialogManager extends BaseDialogManager {
this.engineComponent?.recoverSection();
}
},
onFillToggle: (isFilled) => {
this.engineComponent?.setFillCutFace(isFilled);
},
onReverse: () => {
this.engineComponent?.reverseSection();
},

View File

@@ -10,6 +10,10 @@ export interface EngineEvents {
'engine:model-loading-completed': {};
'engine:object-clicked': { objectId: string; position: { x: number; y: number; z: number } };
'encoding:start': { data?: any };
'encoding:complete': { data?: any };
'encoding:error': { data?: any };
// 树组件事件
'ui:tree-node-check': { id: string; checked: boolean; node: any };
'ui:tree-node-select': { id: string; selected: boolean; node: any };

View File

@@ -27,6 +27,7 @@ const ICONS: Record<string, string> = {
: '<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="currentColor"><g transform="translate(-84.667 -84.667)"><path d="M87.667,97V87.667H97V85.333H86.733a1.4,1.4,0,0,0-1.4,1.4V97Zm0,23.333H85.333V130.6a1.4,1.4,0,0,0,1.4,1.4H97v-2.333H87.667v-9.333Zm32.667,9.333V132H130.6a1.4,1.4,0,0,0,1.4-1.4V120.333h-2.333v9.333h-9.333ZM129.667,97H132V86.733a1.4,1.4,0,0,0-1.4-1.4H120.333v2.333h9.333Z"/><path d="M270.857,243.5l11.3-6.652V223.387l-11.3,6.361V243.5Zm-1.8,0v-13.75l-11.3-6.361v13.456l11.3,6.652Zm-10.278-21.621,11.177,6.284,11.177-6.284L269.958,215.3l-11.177,6.573Zm11.622-8.426,13.1,7.709a.919.919,0,0,1,.448.793v15.419a.926.926,0,0,1-.448.793l-13.1,7.709a.887.887,0,0,1-.9,0l-13.1-7.709a.905.905,0,0,1-.448-.793V221.954a.919.919,0,0,1,.448-.793l13.1-7.709A.887.887,0,0,1,270.4,213.452Z" transform="translate(-161.292 -120.997)"/></g></svg>',
: '<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="currentColor"><path d="M61.274,76.178l0-.007-.007,0a32.458,32.458,0,0,0-7.014-9.981L51.53,68.91a28.382,28.382,0,0,1,6.124,8.635c-4.472,9.25-10.809,13.636-19.472,13.636A20.615,20.615,0,0,1,30.6,89.838l-2.937,2.933a23.916,23.916,0,0,0,10.514,2.277q15.472,0,23.088-16.116a3.229,3.229,0,0,0,0-2.755ZM57.839,58.351l-2.274-2.277a.429.429,0,0,0-.608,0l-6.265,6.262a23.775,23.775,0,0,0-10.51-2.281q-15.472,0-23.088,16.116v.007a3.236,3.236,0,0,0,0,2.765,32.617,32.617,0,0,0,7.014,9.985L16.7,94.32a.429.429,0,0,0,0,.608l2.274,2.277a.429.429,0,0,0,.608,0L57.839,58.949a.426.426,0,0,0,0-.6ZM32.113,78.915a6.011,6.011,0,0,1,7.22-7.22l-7.223,7.22Zm9.9-9.9A9.452,9.452,0,0,0,29.43,81.6l-4.6,4.6a28.424,28.424,0,0,1-6.124-8.635c4.475-9.25,10.816-13.636,19.472-13.636a20.615,20.615,0,0,1,7.577,1.343Zm-4.046,14.55a5.752,5.752,0,0,1-1.01-.086l-2.744,2.741A9.446,9.446,0,0,0,46.638,73.794L43.9,76.538a6.018,6.018,0,0,1-5.932,7.024Z" transform="translate(-14.182 -52.64)"/></svg>',
: '<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="currentColor"><path d="M32.936,32.658a15.914,15.914,0,0,1-9.6,3.064c-.451-.006-.9-.042-1.342-.077-.182-.018-.357-.047-.539-.077-.352-.047-.7-.089-1.037-.164-.211-.036-.422-.1-.621-.148-.334-.077-.668-.154-.99-.254-.153-.059-.3-.11-.457-.177-.381-.124-.755-.26-1.113-.414-.082-.036-.162-.071-.24-.1-.422-.2-.844-.408-1.248-.627-.017-.012-.035-.018-.054-.03a16.485,16.485,0,0,1-3.732-2.9c-.017-.018-.035-.042-.054-.059-.34-.355-.668-.721-.979-1.118-.065-.083-.123-.16-.194-.254A17.286,17.286,0,0,1,7.149,18.751h3.98L6.318,9.076,0,18.874H3.942a20.809,20.809,0,0,0,3.5,11.57.871.871,0,0,0,.075.14c.225.34.486.648.724.961.1.113.177.232.274.362.354.443.746.869,1.133,1.285.043.043.075.076.108.113a19.778,19.778,0,0,0,4.436,3.446c.043.027.08.043.128.076.467.259.949.5,1.432.719.124.055.242.113.359.164.419.184.847.335,1.277.493.2.076.4.14.611.21.378.113.762.21,1.153.3.257.065.509.129.772.184a2.252,2.252,0,0,0,.316.076c.37.07.735.1,1.1.151.134.027.27.049.4.065.66.065,1.314.11,1.974.11,4.006,0,8.852-1.3,11.85-4.177a1.908,1.908,0,0,0,.333-2.484,2.228,2.228,0,0,0-2.952.023ZM44.053,20.424a20.728,20.728,0,0,0-3.47-11.537c-.032-.055-.054-.113-.08-.164-.284-.405-.574-.778-.869-1.161a1.289,1.289,0,0,1-.1-.14A19.86,19.86,0,0,0,32.168,1.7c-.086-.032-.155-.076-.242-.11-.456-.195-.922-.362-1.394-.53C30.37,1,30.2.937,30.033.883,29.62.754,29.212.651,28.792.548c-.231-.055-.467-.113-.7-.164-.113-.022-.22-.055-.338-.081-.311-.055-.617-.081-.933-.124C26.6.151,26.392.119,26.18.1,25.655.042,25.135.021,24.616.016c-.1,0-.188-.016-.284-.016-.016,0-.032.005-.049.005A19.124,19.124,0,0,0,13.02,3.695a1.682,1.682,0,0,0-.466,2.515,1.777,1.777,0,0,0,2.309.3A15.251,15.251,0,0,1,24.374,3.45c.486.006.971.03,1.441.077.147.012.287.036.432.059a11.3,11.3,0,0,1,1.16.189c.162.03.334.077.493.11.381.089.744.178,1.107.3a2.577,2.577,0,0,1,.34.124c.422.142.832.29,1.23.467.047.012.082.047.123.059a16.492,16.492,0,0,1,6.182,4.8.367.367,0,0,0,.024.036A17.308,17.308,0,0,1,40.629,20.43H36.645l5.048,9.787L48,20.424Zm0,0" transform="translate(0 4.349)"/></svg>',
: '<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="currentColor"><path d="M24 6L10 22h10v18h8V22h10L24 6z" opacity="0.35"/><path d="M8 24h32v4H8z"/></svg>',
// ========== 测量相关图标 (32x32) ==========
: '<svg width="32" height="32" viewBox="0 0 32 32"><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>',