feat: 迁移BimEngine到radial+dock并完善测量/剖切/漫游面板
This commit is contained in:
@@ -136,11 +136,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3. 工具栏操作 (CusBimEngine 中已移除 ToolbarManager) -->
|
||||
<div class="control-group" style="opacity: 0.6;">
|
||||
<h2>🛠️ 工具栏 (Toolbar) <span style="color: #999; font-size: 0.75rem;">[CusBimEngine 已移除]</span></h2>
|
||||
<h2>🛠️ 工具栏 (Toolbar) <span style="color: #999; font-size: 0.75rem;">[当前使用 Radial Toolbar]</span></h2>
|
||||
<div style="font-size: 0.8rem; color: #888; margin-bottom: 8px;">
|
||||
当前使用 CusBimEngine,不包含工具栏功能
|
||||
当前演示默认使用 Radial Toolbar,以下旧 Toolbar API 按钮已停用
|
||||
</div>
|
||||
<div class="btn-container">
|
||||
<button disabled>显隐工具栏</button>
|
||||
@@ -585,7 +584,7 @@
|
||||
|
||||
/**
|
||||
* 初始化 3D 引擎(独立实例,销毁其他引擎类型)
|
||||
* 使用 CusBimEngine(移除了按钮组和构件树)
|
||||
* 使用 BimEngine
|
||||
*/
|
||||
function initEngine3D() {
|
||||
destroyAllEngines();
|
||||
@@ -596,7 +595,7 @@
|
||||
document.getElementById('btn-load720').disabled = true;
|
||||
|
||||
try {
|
||||
engine = new IflowEngine.CusBimEngine('app', { locale: currentLocale });
|
||||
engine = new IflowEngine.BimEngine('app', { locale: currentLocale });
|
||||
var success = engine.engine.initialize({
|
||||
backgroundColor: 0x333333,
|
||||
version: 'v2',
|
||||
|
||||
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
@@ -123,14 +123,33 @@ interface EngineSettingPreset {
|
||||
settings: EngineSettings; // 预设包含的设置
|
||||
readonly?: boolean; // 是否只读
|
||||
source?: 'sdk-default' | 'external' | 'user'; // 预设来源
|
||||
allowModify?: boolean; // 是否允许修改(保存/删除),默认 true
|
||||
}
|
||||
```
|
||||
|
||||
**字段说明:**
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `id` | `string` | 预设唯一标识 |
|
||||
| `presetName` | `string` | 预设显示名称 |
|
||||
| `isDefault` | `boolean` | 是否为默认预设,打开面板时自动选中 |
|
||||
| `settings` | `EngineSettings` | 预设包含的完整设置 |
|
||||
| `readonly` | `boolean` | 是否只读,只读预设用户无法修改设置值(但仍可保存为新预设) |
|
||||
| `source` | `'sdk-default' \| 'external' \| 'user'` | 预设来源 |
|
||||
| `allowModify` | `boolean` | **是否允许修改当前预设**,`false` 时隐藏"保存"和"删除"按钮,默认 `true` |
|
||||
|
||||
**source 字段说明:**
|
||||
- `'sdk-default'`: SDK 内置的默认预设
|
||||
- `'sdk-default'`: SDK 内置的默认预设(`allowModify: false`)
|
||||
- `'external'`: 第三方注入的预设
|
||||
- `'user'`: 用户自行保存的预设
|
||||
|
||||
**allowModify 字段说明:**
|
||||
- `true`(默认): 显示所有操作按钮("保存预设"、"存为新预设"、"删除")
|
||||
- `false`: 隐藏"保存当前预设"和"删除"按钮,但保留"另存为新预设"按钮
|
||||
|
||||
> **注意:** "另存为新预设"始终可用,因为创建新预设不是在修改当前预设。下拉框宽度始终保持固定。
|
||||
|
||||
### 2.3 设置补丁 (局部更新)
|
||||
|
||||
```typescript
|
||||
@@ -532,7 +551,45 @@ bimEngine.setting?.setPresetList(externalPresets);
|
||||
- 若有多个 `isDefault: true`,取第一个
|
||||
- 若没有指定默认,选中内置默认预设
|
||||
|
||||
### 5.3 动态更新预设
|
||||
### 5.3 禁止修改预设
|
||||
|
||||
使用 `allowModify: false` 可以禁止用户保存或删除某个预设:
|
||||
|
||||
```typescript
|
||||
bimEngine.setting?.setPresetList([
|
||||
{
|
||||
id: 'vendor-locked',
|
||||
presetName: '供应商锁定预设',
|
||||
isDefault: false,
|
||||
source: 'external',
|
||||
allowModify: false, // ← 禁止修改
|
||||
settings: {
|
||||
render: { mode: 'advanced', contrast: 60, ... },
|
||||
display: { showEdge: true, ... },
|
||||
environment: { type: 'hdr', hdrId: 'hdr-01', ... }
|
||||
}
|
||||
}
|
||||
]);
|
||||
```
|
||||
|
||||
**效果:**
|
||||
- ❌ 不显示"保存当前预设"按钮(无法覆盖原预设)
|
||||
- ❌ 不显示删除按钮
|
||||
- ✅ 保留"另存为新预设"按钮(可基于此预设创建新预设)
|
||||
- ✅ 预设选择下拉框宽度固定不变
|
||||
- ✅ 用户仍可修改设置值(如需禁止修改设置值,使用 `readonly: true`)
|
||||
|
||||
**适用场景:**
|
||||
- 供应商提供的标准预设,不希望用户覆盖或删除,但允许基于此创建新预设
|
||||
- 项目规定的固定展示模式
|
||||
- 内置默认预设(SDK 默认 `allowModify: false`)
|
||||
|
||||
> **为什么保留"另存为新预设"?**
|
||||
>
|
||||
> "另存为新预设"是创建一个新的预设,不是在修改当前预设。即使用户不能修改供应商预设,也应该可以基于它创建自己的版本。>
|
||||
> 如果你希望完全禁止用户基于此预设创建新预设,需要额外的前端逻辑控制。
|
||||
|
||||
### 5.4 动态更新预设
|
||||
|
||||
```typescript
|
||||
// 随时可以更新预设列表
|
||||
@@ -550,7 +607,7 @@ bimEngine.setting?.setPresetList([
|
||||
const presets = bimEngine.setting?.getPresetList();
|
||||
```
|
||||
|
||||
### 5.4 完整示例
|
||||
### 5.5 完整示例
|
||||
|
||||
```typescript
|
||||
import { BimEngine } from 'iflow-engine';
|
||||
@@ -881,21 +938,55 @@ await bimEngine.engine?.setSettings({
|
||||
});
|
||||
```
|
||||
|
||||
### Q3: 第三方预设会被用户删除吗?
|
||||
### Q3: 如何禁止用户修改(保存/删除)某个预设?
|
||||
|
||||
不会。`readonly: true` 的预设不会显示删除按钮。建议第三方预设设置 `readonly: true`:
|
||||
使用 `allowModify: false` 可以禁止用户覆盖或删除某个预设:
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: 'vendor-locked-preset',
|
||||
presetName: '供应商锁定预设',
|
||||
source: 'external',
|
||||
allowModify: false, // 禁止修改当前预设
|
||||
settings: { ... }
|
||||
}
|
||||
```
|
||||
|
||||
**效果:**
|
||||
- ❌ 隐藏"保存当前预设"按钮(无法覆盖原预设)
|
||||
- ❌ 隐藏"删除"按钮
|
||||
- ✅ 保留"另存为新预设"按钮(可基于此创建新预设)
|
||||
- ✅ 下拉框宽度保持不变
|
||||
|
||||
**注意:**
|
||||
- `allowModify: false` 时,用户仍可以切换到这个预设
|
||||
- 用户仍可以修改设置值,只是无法保存到当前预设
|
||||
- 如需完全禁止修改设置值,需配合 `readonly: true`
|
||||
|
||||
### Q4: `readonly` 和 `allowModify` 有什么区别?
|
||||
|
||||
| 字段 | 作用 | 对用户的影响 |
|
||||
|------|------|-------------|
|
||||
| `readonly` | 禁止修改设置值 | 禁用所有设置控件(滑块、开关等) |
|
||||
| `allowModify` | 禁止覆盖/删除当前预设 | 隐藏"保存当前预设"和"删除"按钮,但保留"另存为新预设" |
|
||||
|
||||
内置默认预设(`__internal-default-preset__`)的 `allowModify: false`。
|
||||
|
||||
### Q5: 第三方预设会被用户删除吗?
|
||||
|
||||
如果设置了 `allowModify: false`,用户无法删除该预设。建议第三方预设设置 `allowModify: false`:
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: 'vendor-preset',
|
||||
presetName: '供应商预设',
|
||||
source: 'external',
|
||||
readonly: true, // 用户不可删除
|
||||
allowModify: false, // 用户不可删除/保存该预设
|
||||
settings: { ... }
|
||||
}
|
||||
```
|
||||
|
||||
### Q4: 如何清空所有第三方预设?
|
||||
### Q6: 如何清空所有第三方预设?
|
||||
|
||||
```typescript
|
||||
// 获取当前列表
|
||||
@@ -908,7 +999,7 @@ const filteredList = currentList.filter(p => p.source !== 'external');
|
||||
bimEngine.setting?.setPresetList(filteredList);
|
||||
```
|
||||
|
||||
### Q5: 设置修改后立即生效吗?
|
||||
### Q7: 设置修改后立即生效吗?
|
||||
|
||||
`setSettings` 返回 Promise,设置应用完成后 resolve:
|
||||
|
||||
@@ -921,6 +1012,6 @@ await bimEngine.engine?.setSettings({
|
||||
|
||||
---
|
||||
|
||||
**文档版本**: 1.0.0
|
||||
**文档版本**: 1.1.0
|
||||
**更新时间**: 2026-03-30
|
||||
**适用 SDK 版本**: iflow-engine >= 2.2.0
|
||||
**适用 SDK 版本**: iflow-engine >= 2.2.1
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "iflow-engine",
|
||||
"version": "2.4.8",
|
||||
"version": "2.5.2",
|
||||
"description": "iFlow Engine SDK for Vue2, Vue3, React and HTML",
|
||||
"main": "./dist/iflow-engine.umd.js",
|
||||
"module": "./dist/iflow-engine.es.js",
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
declare const __APP_VERSION__: string;
|
||||
import './bim-engine.css';
|
||||
import { ToolbarManager } from './managers/toolbar-manager';
|
||||
import { ButtonGroupManager } from './managers/button-group-manager';
|
||||
import { DialogManager } from './managers/dialog-manager';
|
||||
import { EngineManager } from './managers/engine-manager';
|
||||
import { RightKeyManager } from './managers/right-key-manager';
|
||||
import { ConstructTreeManagerBtn } from './managers/construct-tree-manager-btn';
|
||||
import { RadialToolbarManager } from './managers/radial-toolbar-manager';
|
||||
import { BottomDockManager } from './managers/bottom-dock-manager';
|
||||
import { MeasureDockManager } from './managers/measure-dock-manager';
|
||||
import { SectionDockManager } from './managers/section-dock-manager';
|
||||
import { WalkDockManager } from './managers/walk-dock-manager';
|
||||
|
||||
import { MeasureDialogManager } from './managers/measure-dialog-manager';
|
||||
import { SectionPlaneDialogManager } from './managers/section-plane-dialog-manager';
|
||||
@@ -16,6 +18,7 @@ import { EngineInfoDialogManager } from './managers/engine-info-dialog-manager';
|
||||
import { SettingDialogManager } from './managers/setting-dialog-manager';
|
||||
import { ComponentDetailManager } from './managers/component-detail-manager';
|
||||
import { AiChatManager } from './managers/ai-chat-manager';
|
||||
import { ConstructTreeManagerBtn } from './managers/construct-tree-manager-btn';
|
||||
import type { EngineOptions, ModelLoadOptions } from './components/engine';
|
||||
import { localeManager } from './services/locale';
|
||||
import { themeManager } from './services/theme';
|
||||
@@ -35,12 +38,14 @@ export class BimEngine {
|
||||
private lastSyncedHeight = -1;
|
||||
private registry: ManagerRegistry;
|
||||
|
||||
public toolbar: ToolbarManager | null = null;
|
||||
public constructTreeBtn: ConstructTreeManagerBtn | null = null;
|
||||
public buttonGroup: ButtonGroupManager | null = null;
|
||||
public dialog: DialogManager | null = null;
|
||||
public engine: EngineManager | null = null;
|
||||
public rightKey: RightKeyManager | null = null;
|
||||
public radialToolbar: RadialToolbarManager | null = null;
|
||||
public bottomDock: BottomDockManager | null = null;
|
||||
public measureDock: MeasureDockManager | null = null;
|
||||
public sectionDock: SectionDockManager | null = null;
|
||||
public walkDock: WalkDockManager | null = null;
|
||||
|
||||
public measure: MeasureDialogManager | null = null;
|
||||
public sectionPlane: SectionPlaneDialogManager | null = null;
|
||||
@@ -51,6 +56,7 @@ export class BimEngine {
|
||||
public componentDetail: ComponentDetailManager | null = null;
|
||||
public aiChat: AiChatManager | null = null;
|
||||
public setting: SettingDialogManager | null = null;
|
||||
public constructTreeBtn: ConstructTreeManagerBtn | null = null;
|
||||
|
||||
private readonly handleWindowResize = () => {
|
||||
this.updateClientSizeDisplay();
|
||||
@@ -133,10 +139,23 @@ export class BimEngine {
|
||||
|
||||
this.engine = new EngineManager(this.wrapper, this.registry);
|
||||
this.dialog = new DialogManager(this.wrapper, this.registry);
|
||||
this.toolbar = new ToolbarManager(this.wrapper, this.registry);
|
||||
this.buttonGroup = new ButtonGroupManager(this.wrapper, this.registry);
|
||||
this.rightKey = new RightKeyManager(this.wrapper, this.registry);
|
||||
this.constructTreeBtn = new ConstructTreeManagerBtn(this.wrapper, this.registry);
|
||||
this.bottomDock = new BottomDockManager(this.wrapper, this.registry);
|
||||
this.registry.bottomDock = this.bottomDock;
|
||||
|
||||
this.measureDock = new MeasureDockManager(this.registry);
|
||||
this.registry.measureDock = this.measureDock;
|
||||
this.measureDock.init();
|
||||
|
||||
this.sectionDock = new SectionDockManager(this.registry);
|
||||
this.registry.sectionDock = this.sectionDock;
|
||||
this.sectionDock.init();
|
||||
|
||||
this.walkDock = new WalkDockManager(this.registry);
|
||||
this.registry.walkDock = this.walkDock;
|
||||
this.walkDock.init();
|
||||
|
||||
this.radialToolbar = new RadialToolbarManager(this.wrapper, this.registry);
|
||||
|
||||
this.measure = new MeasureDialogManager(this.registry);
|
||||
this.sectionPlane = new SectionPlaneDialogManager(this.registry);
|
||||
@@ -149,10 +168,8 @@ export class BimEngine {
|
||||
|
||||
this.registry.engine3d = this.engine;
|
||||
this.registry.dialog = this.dialog;
|
||||
this.registry.toolbar = this.toolbar;
|
||||
this.registry.buttonGroup = this.buttonGroup;
|
||||
this.registry.rightKey = this.rightKey;
|
||||
this.registry.constructTree = this.constructTreeBtn;
|
||||
this.registry.radialToolbar = this.radialToolbar;
|
||||
|
||||
this.registry.measure = this.measure;
|
||||
this.registry.sectionPlane = this.sectionPlane;
|
||||
@@ -170,6 +187,9 @@ export class BimEngine {
|
||||
this.registry.aiChat = this.aiChat;
|
||||
this.aiChat.init();
|
||||
|
||||
this.constructTreeBtn = new ConstructTreeManagerBtn(this.wrapper, this.registry);
|
||||
this.registry.constructTree = this.constructTreeBtn;
|
||||
|
||||
this.setting = new SettingDialogManager(this.registry);
|
||||
this.registry.setting = this.setting;
|
||||
this.setting.init();
|
||||
@@ -236,8 +256,11 @@ export class BimEngine {
|
||||
public destroy() {
|
||||
this.unbindSizeObserver();
|
||||
|
||||
this.toolbar?.destroy();
|
||||
this.buttonGroup?.destroy();
|
||||
this.radialToolbar?.destroy();
|
||||
this.measureDock?.destroy();
|
||||
this.sectionDock?.destroy();
|
||||
this.walkDock?.destroy();
|
||||
this.bottomDock?.destroy();
|
||||
this.engine?.destroy();
|
||||
this.dialog?.destroy();
|
||||
this.rightKey?.destroy();
|
||||
@@ -247,6 +270,7 @@ export class BimEngine {
|
||||
this.sectionAxis?.destroy();
|
||||
this.sectionBox?.destroy();
|
||||
this.walkControl?.destroy();
|
||||
this.constructTreeBtn?.destroy();
|
||||
this.aiChat?.destroy();
|
||||
this.setting?.destroy();
|
||||
|
||||
|
||||
@@ -23,6 +23,12 @@
|
||||
box-shadow: var(--bd-shadow, 0 2px 8px rgba(15, 23, 42, 0.1));
|
||||
transition: transform 220ms ease, opacity 200ms ease;
|
||||
overflow: visible;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.bottom-dock-panel:hover,
|
||||
.bottom-dock-panel:focus-within {
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.bottom-dock-panel.is-entering {
|
||||
|
||||
@@ -94,6 +94,7 @@ export interface EngineSettingPreset {
|
||||
settings: EngineSettings;
|
||||
readonly?: boolean;
|
||||
source?: 'sdk-default' | 'external' | 'user';
|
||||
allowModify?: boolean;
|
||||
}
|
||||
|
||||
export interface PresetListItem {
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
padding: 0;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
background: color-mix(in srgb, var(--bim-bg-elevated, #e8ecf2) 92%, #ffffff 8%);
|
||||
box-sizing: border-box;
|
||||
color: var(--bim-text-secondary, #475569);
|
||||
font-size: 13px;
|
||||
@@ -187,7 +186,6 @@
|
||||
|
||||
.measure-dock-panel-mode-btn.is-active {
|
||||
border-color: color-mix(in srgb, var(--bim-primary, #4f88ff) 70%, #9db9ff 30%);
|
||||
background: color-mix(in srgb, var(--bim-primary-subtle, rgba(96, 140, 255, 0.18)) 72%, #ffffff 28%);
|
||||
color: color-mix(in srgb, var(--bim-primary, #4f88ff) 78%, #6f9dff 22%);
|
||||
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--bim-primary, #4f88ff) 35%, transparent 65%);
|
||||
}
|
||||
@@ -332,4 +330,4 @@
|
||||
.measure-dock-panel-action-expand.is-expanded {
|
||||
height: 74px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -400,23 +400,21 @@ export class RadialToolbar implements IBimComponent {
|
||||
style.setProperty('--bim-icon-inverse', theme.iconInverse);
|
||||
style.setProperty('--bim-shadow-glow', theme.shadowGlow);
|
||||
|
||||
const isDark = theme.name === 'dark';
|
||||
style.setProperty('--rt-main-bg', theme.bgBase);
|
||||
style.setProperty('--rt-main-bg-hover', theme.bgBase);
|
||||
style.setProperty('--rt-main-border', theme.floatingBtnBorder);
|
||||
style.setProperty('--rt-main-shadow', theme.floatingBtnShadow);
|
||||
style.setProperty('--rt-main-shadow-hover', theme.floatingBtnShadowHover);
|
||||
style.setProperty('--rt-main-icon', theme.floatingIconColor);
|
||||
style.setProperty('--rt-main-icon-hover', theme.floatingIconColorHover);
|
||||
|
||||
style.setProperty('--rt-main-bg', isDark ? 'rgba(55, 68, 86, 0.92)' : theme.floatingBtnBg);
|
||||
style.setProperty('--rt-main-bg-hover', isDark ? 'rgba(66, 82, 104, 0.96)' : theme.floatingBtnBgHover);
|
||||
style.setProperty('--rt-main-border', isDark ? 'rgba(117, 133, 154, 0.56)' : theme.floatingBtnBorder);
|
||||
style.setProperty('--rt-main-shadow', isDark ? '0 2px 8px rgba(15, 23, 42, 0.32), 0 4px 12px rgba(15, 23, 42, 0.24)' : theme.floatingBtnShadow);
|
||||
style.setProperty('--rt-main-shadow-hover', isDark ? '0 4px 12px rgba(15, 23, 42, 0.38), 0 6px 20px rgba(15, 23, 42, 0.3)' : theme.floatingBtnShadowHover);
|
||||
style.setProperty('--rt-main-icon', isDark ? '#e2e8f0' : theme.floatingIconColor);
|
||||
style.setProperty('--rt-main-icon-hover', isDark ? '#f8fafc' : theme.floatingIconColorHover);
|
||||
|
||||
style.setProperty('--rt-sub-bg', isDark ? 'rgba(55, 68, 86, 0.92)' : theme.floatingBtnBg);
|
||||
style.setProperty('--rt-sub-bg-hover', isDark ? 'rgba(66, 82, 104, 0.96)' : theme.floatingBtnBgHover);
|
||||
style.setProperty('--rt-sub-border', isDark ? 'rgba(117, 133, 154, 0.56)' : theme.floatingBtnBorder);
|
||||
style.setProperty('--rt-sub-shadow', isDark ? '0 2px 8px rgba(15, 23, 42, 0.32), 0 4px 12px rgba(15, 23, 42, 0.24)' : theme.floatingBtnShadow);
|
||||
style.setProperty('--rt-sub-shadow-hover', isDark ? '0 4px 12px rgba(15, 23, 42, 0.38), 0 6px 20px rgba(15, 23, 42, 0.3)' : theme.floatingBtnShadowHover);
|
||||
style.setProperty('--rt-sub-icon', isDark ? '#e2e8f0' : theme.floatingIconColor);
|
||||
style.setProperty('--rt-sub-icon-hover', isDark ? '#f8fafc' : theme.floatingIconColorHover);
|
||||
style.setProperty('--rt-sub-bg', theme.bgBase);
|
||||
style.setProperty('--rt-sub-bg-hover', theme.primaryHover);
|
||||
style.setProperty('--rt-sub-border', theme.floatingBtnBorder);
|
||||
style.setProperty('--rt-sub-shadow', theme.floatingBtnShadow);
|
||||
style.setProperty('--rt-sub-shadow-hover', theme.floatingBtnShadowHover);
|
||||
style.setProperty('--rt-sub-icon', theme.floatingIconColor);
|
||||
style.setProperty('--rt-sub-icon-hover', theme.iconHover);
|
||||
}
|
||||
|
||||
public setItemActive(id: string, active: boolean): void {
|
||||
|
||||
142
src/components/section-dock-panel/index.css
Normal file
142
src/components/section-dock-panel/index.css
Normal file
@@ -0,0 +1,142 @@
|
||||
.section-dock-panel {
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
color: var(--bim-text-secondary, #475569);
|
||||
}
|
||||
|
||||
.section-dock-axis-panel {
|
||||
display: none;
|
||||
width: fit-content;
|
||||
border-radius: 8px;
|
||||
padding: 0;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.section-dock-axis-panel.is-visible {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.section-dock-axis-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.28);
|
||||
background: color-mix(in srgb, var(--bim-bg-inset, #edf1f6) 92%, #ffffff 8%);
|
||||
color: color-mix(in srgb, var(--bim-text-secondary, #64748b) 94%, #475569 6%);
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.section-dock-axis-btn:hover {
|
||||
border-color: rgba(148, 163, 184, 0.5);
|
||||
background: color-mix(in srgb, var(--bim-component-bg-hover, #dce5f2) 64%, #ffffff 36%);
|
||||
}
|
||||
|
||||
.section-dock-axis-btn.is-active {
|
||||
border-color: color-mix(in srgb, var(--bim-primary, #4f88ff) 70%, #9db9ff 30%);
|
||||
background: color-mix(in srgb, var(--bim-primary-subtle, rgba(96, 140, 255, 0.18)) 72%, #ffffff 28%);
|
||||
color: color-mix(in srgb, var(--bim-primary, #4f88ff) 78%, #6f9dff 22%);
|
||||
}
|
||||
|
||||
.section-dock-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.section-dock-types,
|
||||
.section-dock-tools {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.section-dock-divider {
|
||||
width: 1px;
|
||||
height: 32px;
|
||||
background: color-mix(in srgb, var(--bim-border-default, #cbd5e1) 84%, transparent 16%);
|
||||
}
|
||||
|
||||
.section-dock-type-btn,
|
||||
.section-dock-tool-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.28);
|
||||
border-radius: 8px;
|
||||
background: color-mix(in srgb, var(--bim-bg-inset, #edf1f6) 92%, #ffffff 8%);
|
||||
color: color-mix(in srgb, var(--bim-text-secondary, #64748b) 94%, #475569 6%);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.section-dock-type-btn:hover,
|
||||
.section-dock-tool-btn:hover {
|
||||
border-color: var(--bim-border-strong, rgba(100, 116, 139, 0.6));
|
||||
background: color-mix(in srgb, var(--bim-component-bg-hover, #dce5f2) 64%, #ffffff 36%);
|
||||
}
|
||||
|
||||
.section-dock-type-btn.is-active,
|
||||
.section-dock-tool-btn.is-active {
|
||||
border-color: color-mix(in srgb, var(--bim-primary, #4f88ff) 70%, #9db9ff 30%);
|
||||
color: color-mix(in srgb, var(--bim-primary, #4f88ff) 78%, #6f9dff 22%);
|
||||
}
|
||||
|
||||
.section-dock-type-icon,
|
||||
.section-dock-tool-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.section-dock-type-icon svg,
|
||||
.section-dock-tool-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.section-dock-panel [data-tooltip] {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.section-dock-panel [data-tooltip]::after {
|
||||
content: attr(data-tooltip);
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: calc(100% + 6px);
|
||||
transform: translateX(-50%);
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
background: rgba(15, 23, 42, 0.92);
|
||||
color: #f8fafc;
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
z-index: 8;
|
||||
transition: opacity 60ms ease;
|
||||
}
|
||||
|
||||
.section-dock-panel [data-tooltip]:hover::after,
|
||||
.section-dock-panel [data-tooltip]:focus-visible::after {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
330
src/components/section-dock-panel/index.ts
Normal file
330
src/components/section-dock-panel/index.ts
Normal file
@@ -0,0 +1,330 @@
|
||||
import './index.css';
|
||||
import type { ThemeConfig } from '../../themes/types';
|
||||
import { IBimComponent } from '../../types/component';
|
||||
import { t } from '../../services/locale';
|
||||
import { getIcon } from '../../utils/icon-manager';
|
||||
|
||||
export type SectionDockType = 'face' | 'axis' | 'box';
|
||||
export type SectionDockAxis = 'x' | 'y' | 'z';
|
||||
|
||||
export interface SectionDockPanelOptions {
|
||||
defaultType?: SectionDockType;
|
||||
defaultAxis?: SectionDockAxis;
|
||||
defaultHidden?: 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;
|
||||
}
|
||||
|
||||
interface ToolDefinition {
|
||||
key: 'hide' | 'reverse' | 'reset' | 'fit';
|
||||
iconName: string;
|
||||
textKey: string;
|
||||
onClick: () => void;
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
export class SectionDockPanel implements IBimComponent {
|
||||
public readonly element: HTMLElement;
|
||||
private readonly options: SectionDockPanelOptions;
|
||||
|
||||
private readonly typeButtons: Map<SectionDockType, HTMLButtonElement> = new Map();
|
||||
private readonly axisButtons: Map<SectionDockAxis, HTMLButtonElement> = new Map();
|
||||
private readonly toolContainer: HTMLElement;
|
||||
private readonly axisPanel: HTMLElement;
|
||||
|
||||
private activeType: SectionDockType;
|
||||
private activeAxis: SectionDockAxis;
|
||||
private isHidden: boolean;
|
||||
|
||||
constructor(options: SectionDockPanelOptions = {}) {
|
||||
this.options = options;
|
||||
this.activeType = options.defaultType ?? 'face';
|
||||
this.activeAxis = options.defaultAxis ?? 'x';
|
||||
this.isHidden = options.defaultHidden ?? false;
|
||||
|
||||
const { root, toolContainer, axisPanel } = this.createDom();
|
||||
this.element = root;
|
||||
this.toolContainer = toolContainer;
|
||||
this.axisPanel = axisPanel;
|
||||
}
|
||||
|
||||
public init(): void {
|
||||
this.applyTypeState();
|
||||
this.applyAxisState();
|
||||
this.renderTools();
|
||||
this.setLocales();
|
||||
}
|
||||
|
||||
public setTheme(theme: ThemeConfig): void {
|
||||
const style = this.element.style;
|
||||
style.setProperty('--bim-text-primary', theme.textPrimary);
|
||||
style.setProperty('--bim-text-secondary', theme.textSecondary);
|
||||
style.setProperty('--bim-border-default', theme.borderDefault);
|
||||
style.setProperty('--bim-border-strong', theme.borderStrong);
|
||||
style.setProperty('--bim-bg-inset', theme.bgInset);
|
||||
style.setProperty('--bim-bg-elevated', theme.bgElevated);
|
||||
style.setProperty('--bim-primary', theme.primary);
|
||||
style.setProperty('--bim-primary-subtle', theme.primarySubtle);
|
||||
style.setProperty('--bim-component-bg-hover', theme.componentBgHover);
|
||||
}
|
||||
|
||||
public setLocales(): void {
|
||||
this.typeButtons.get('face')!.dataset.tooltip = t('toolbar.sectionPlane');
|
||||
this.typeButtons.get('axis')!.dataset.tooltip = t('toolbar.sectionAxis');
|
||||
this.typeButtons.get('box')!.dataset.tooltip = t('toolbar.sectionBox');
|
||||
|
||||
this.axisButtons.get('x')!.dataset.label = 'X';
|
||||
this.axisButtons.get('y')!.dataset.label = 'Y';
|
||||
this.axisButtons.get('z')!.dataset.label = 'Z';
|
||||
|
||||
this.typeButtons.forEach((button) => {
|
||||
button.setAttribute('aria-label', button.dataset.tooltip ?? '');
|
||||
});
|
||||
this.axisButtons.forEach((button) => {
|
||||
button.setAttribute('aria-label', button.dataset.label ?? '');
|
||||
});
|
||||
|
||||
this.renderTools();
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
this.element.remove();
|
||||
}
|
||||
|
||||
public resetForOpen(): void {
|
||||
this.activeType = 'face';
|
||||
this.isHidden = false;
|
||||
this.applyTypeState();
|
||||
this.renderTools();
|
||||
this.options.onTypeChange?.(this.activeType, this.activeAxis);
|
||||
this.options.onHideToggle?.(false);
|
||||
}
|
||||
|
||||
public getActiveType(): SectionDockType {
|
||||
return this.activeType;
|
||||
}
|
||||
|
||||
public getActiveAxis(): SectionDockAxis {
|
||||
return this.activeAxis;
|
||||
}
|
||||
|
||||
private createDom(): { root: HTMLElement; toolContainer: HTMLElement; axisPanel: HTMLElement } {
|
||||
const root = document.createElement('div');
|
||||
root.className = 'section-dock-panel';
|
||||
|
||||
const axisPanel = document.createElement('div');
|
||||
axisPanel.className = 'section-dock-axis-panel';
|
||||
|
||||
(['x', 'y', 'z'] as SectionDockAxis[]).forEach((axis) => {
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.className = 'section-dock-axis-btn';
|
||||
button.textContent = axis.toUpperCase();
|
||||
button.addEventListener('click', () => {
|
||||
this.handleAxisChange(axis);
|
||||
});
|
||||
this.axisButtons.set(axis, button);
|
||||
axisPanel.appendChild(button);
|
||||
});
|
||||
|
||||
const main = document.createElement('div');
|
||||
main.className = 'section-dock-main';
|
||||
|
||||
const left = document.createElement('div');
|
||||
left.className = 'section-dock-types';
|
||||
|
||||
const faceBtn = this.createTypeButton('face', getIcon('拾曲面剖切'));
|
||||
const axisBtn = this.createTypeButton('axis', getIcon('轴向剖切'));
|
||||
const boxBtn = this.createTypeButton('box', getIcon('剖切盒'));
|
||||
|
||||
left.appendChild(faceBtn);
|
||||
left.appendChild(axisBtn);
|
||||
left.appendChild(boxBtn);
|
||||
|
||||
const divider = document.createElement('div');
|
||||
divider.className = 'section-dock-divider';
|
||||
|
||||
const toolContainer = document.createElement('div');
|
||||
toolContainer.className = 'section-dock-tools';
|
||||
|
||||
main.appendChild(left);
|
||||
main.appendChild(divider);
|
||||
main.appendChild(toolContainer);
|
||||
|
||||
root.appendChild(axisPanel);
|
||||
root.appendChild(main);
|
||||
|
||||
return { root, toolContainer, axisPanel };
|
||||
}
|
||||
|
||||
private createTypeButton(type: SectionDockType, iconSvg: string): HTMLButtonElement {
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.className = 'section-dock-type-btn';
|
||||
|
||||
const icon = document.createElement('span');
|
||||
icon.className = 'section-dock-type-icon';
|
||||
icon.innerHTML = iconSvg;
|
||||
|
||||
button.appendChild(icon);
|
||||
button.addEventListener('click', () => {
|
||||
this.handleTypeChange(type);
|
||||
});
|
||||
|
||||
this.typeButtons.set(type, button);
|
||||
return button;
|
||||
}
|
||||
|
||||
private handleTypeChange(type: SectionDockType): void {
|
||||
if (this.activeType === type) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.activeType = type;
|
||||
this.applyTypeState();
|
||||
this.renderTools();
|
||||
|
||||
if (this.isHidden) {
|
||||
this.isHidden = false;
|
||||
this.options.onHideToggle?.(false);
|
||||
}
|
||||
|
||||
this.options.onTypeChange?.(type, this.activeAxis);
|
||||
}
|
||||
|
||||
private handleAxisChange(axis: SectionDockAxis): void {
|
||||
if (this.activeAxis === axis) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.activeAxis = axis;
|
||||
this.applyAxisState();
|
||||
|
||||
if (this.activeType === 'axis') {
|
||||
this.options.onAxisChange?.(axis);
|
||||
}
|
||||
}
|
||||
|
||||
private applyTypeState(): void {
|
||||
this.typeButtons.forEach((button, type) => {
|
||||
button.classList.toggle('is-active', type === this.activeType);
|
||||
});
|
||||
|
||||
const axisVisible = this.activeType === 'axis';
|
||||
this.axisPanel.classList.toggle('is-visible', axisVisible);
|
||||
}
|
||||
|
||||
private applyAxisState(): void {
|
||||
this.axisButtons.forEach((button, axis) => {
|
||||
button.classList.toggle('is-active', axis === this.activeAxis);
|
||||
});
|
||||
}
|
||||
|
||||
private renderTools(): void {
|
||||
this.toolContainer.innerHTML = '';
|
||||
|
||||
this.getToolsForType(this.activeType).forEach((tool) => {
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.className = 'section-dock-tool-btn';
|
||||
if (tool.isActive) {
|
||||
button.classList.add('is-active');
|
||||
}
|
||||
|
||||
const icon = document.createElement('span');
|
||||
icon.className = 'section-dock-tool-icon';
|
||||
icon.innerHTML = getIcon(tool.iconName);
|
||||
|
||||
button.appendChild(icon);
|
||||
const text = t(tool.textKey);
|
||||
button.dataset.tooltip = text;
|
||||
button.setAttribute('aria-label', text);
|
||||
button.addEventListener('click', tool.onClick);
|
||||
|
||||
this.toolContainer.appendChild(button);
|
||||
});
|
||||
}
|
||||
|
||||
private getToolsForType(type: SectionDockType): ToolDefinition[] {
|
||||
const hideTool: ToolDefinition = {
|
||||
key: 'hide',
|
||||
iconName: '隐藏',
|
||||
textKey: type === 'box' ? 'sectionBox.actions.hide' : type === 'axis' ? 'sectionAxis.actions.hide' : 'sectionPlane.actions.hide',
|
||||
isActive: this.isHidden,
|
||||
onClick: () => {
|
||||
this.isHidden = !this.isHidden;
|
||||
this.options.onHideToggle?.(this.isHidden);
|
||||
this.renderTools();
|
||||
}
|
||||
};
|
||||
|
||||
const reverseTool: ToolDefinition = {
|
||||
key: 'reverse',
|
||||
iconName: '反向',
|
||||
textKey: type === 'axis' ? 'sectionAxis.actions.reverse' : type === 'box' ? 'sectionBox.actions.reverse' : 'sectionPlane.actions.reverse',
|
||||
onClick: () => {
|
||||
this.options.onReverse?.();
|
||||
}
|
||||
};
|
||||
|
||||
if (type === 'axis') {
|
||||
return [
|
||||
hideTool,
|
||||
reverseTool,
|
||||
{
|
||||
key: 'reset',
|
||||
iconName: '重置',
|
||||
textKey: 'sectionAxis.actions.reset',
|
||||
onClick: () => {
|
||||
this.isHidden = false;
|
||||
this.options.onReset?.(this.activeType, this.activeAxis);
|
||||
this.renderTools();
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
if (type === 'box') {
|
||||
return [
|
||||
hideTool,
|
||||
{
|
||||
key: 'fit',
|
||||
iconName: '适应到模型',
|
||||
textKey: 'sectionBox.actions.fitToModel',
|
||||
onClick: () => {
|
||||
this.options.onFitToModel?.();
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'reset',
|
||||
iconName: '重置',
|
||||
textKey: 'sectionBox.actions.reset',
|
||||
onClick: () => {
|
||||
this.isHidden = false;
|
||||
this.options.onReset?.(this.activeType, this.activeAxis);
|
||||
this.renderTools();
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
hideTool,
|
||||
reverseTool,
|
||||
{
|
||||
key: 'reset',
|
||||
iconName: '重置',
|
||||
textKey: 'sectionPlane.actions.reset',
|
||||
onClick: () => {
|
||||
this.isHidden = false;
|
||||
this.options.onReset?.(this.activeType, this.activeAxis);
|
||||
this.renderTools();
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
107
src/components/walk-dock-panel/index.css
Normal file
107
src/components/walk-dock-panel/index.css
Normal file
@@ -0,0 +1,107 @@
|
||||
.walk-control-panel.walk-dock-panel {
|
||||
gap: 10px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
box-shadow: none;
|
||||
color: var(--bim-text-secondary, #64748b);
|
||||
}
|
||||
|
||||
.walk-control-panel.walk-dock-panel .walk-divider {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.walk-control-panel.walk-dock-panel .walk-control-left,
|
||||
.walk-control-panel.walk-dock-panel .walk-control-settings {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.walk-control-panel.walk-dock-panel .walk-icon-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.28);
|
||||
background: color-mix(in srgb, var(--bim-bg-inset, #edf1f6) 92%, #ffffff 8%);
|
||||
color: color-mix(in srgb, var(--bim-text-secondary, #64748b) 94%, #475569 6%);
|
||||
}
|
||||
|
||||
.walk-control-panel.walk-dock-panel .walk-icon-btn:hover {
|
||||
border-color: rgba(148, 163, 184, 0.5);
|
||||
background: color-mix(in srgb, var(--bim-component-bg-hover, #dce5f2) 64%, #ffffff 36%);
|
||||
}
|
||||
|
||||
.walk-control-panel.walk-dock-panel .walk-icon-btn.active {
|
||||
border-color: color-mix(in srgb, var(--bim-primary, #4f88ff) 70%, #9db9ff 30%);
|
||||
background: color-mix(in srgb, var(--bim-primary-subtle, rgba(96, 140, 255, 0.18)) 72%, #ffffff 28%);
|
||||
color: color-mix(in srgb, var(--bim-primary, #4f88ff) 78%, #6f9dff 22%);
|
||||
}
|
||||
|
||||
.walk-control-panel.walk-dock-panel .walk-icon-btn svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.walk-control-panel.walk-dock-panel .walk-icon-btn.active svg {
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.walk-control-panel.walk-dock-panel .walk-speed-control {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.walk-control-panel.walk-dock-panel .walk-speed-label,
|
||||
.walk-control-panel.walk-dock-panel .walk-checkbox-label,
|
||||
.walk-control-panel.walk-dock-panel .walk-select-label {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.walk-control-panel.walk-dock-panel .walk-speed-group {
|
||||
padding: 2px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.28);
|
||||
background: color-mix(in srgb, var(--bim-bg-inset, #edf1f6) 92%, #ffffff 8%);
|
||||
}
|
||||
|
||||
.walk-control-panel.walk-dock-panel .walk-speed-btn {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
padding: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid rgba(148, 163, 184, 0.28);
|
||||
background: color-mix(in srgb, var(--bim-bg-elevated, #f8fafc) 88%, #ffffff 12%);
|
||||
color: var(--bim-text-primary, #0f172a);
|
||||
}
|
||||
|
||||
.walk-control-panel.walk-dock-panel .walk-speed-display {
|
||||
min-width: 30px;
|
||||
font-size: 12px;
|
||||
color: var(--bim-text-primary, #0f172a);
|
||||
}
|
||||
|
||||
.walk-control-panel.walk-dock-panel .walk-checkbox {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
accent-color: var(--bim-primary, #3b82f6);
|
||||
}
|
||||
|
||||
.walk-control-panel.walk-dock-panel .walk-select {
|
||||
min-width: 90px;
|
||||
height: 24px;
|
||||
padding: 2px 8px;
|
||||
font-size: 12px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.28);
|
||||
background: color-mix(in srgb, var(--bim-bg-inset, #edf1f6) 92%, #ffffff 8%);
|
||||
color: var(--bim-text-primary, #0f172a);
|
||||
}
|
||||
|
||||
.walk-control-panel.walk-dock-panel .walk-exit-btn {
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
39
src/components/walk-dock-panel/index.ts
Normal file
39
src/components/walk-dock-panel/index.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import './index.css';
|
||||
import { IBimComponent } from '../../types/component';
|
||||
import { WalkControlPanel } from '../walk-control-panel';
|
||||
import type { WalkControlPanelOptions, WalkControlState } from '../walk-control-panel/types';
|
||||
import type { ThemeConfig } from '../../themes/types';
|
||||
|
||||
export class WalkDockPanel implements IBimComponent {
|
||||
public readonly element: HTMLElement;
|
||||
private readonly panel: WalkControlPanel;
|
||||
|
||||
constructor(options: WalkControlPanelOptions = {}) {
|
||||
this.panel = new WalkControlPanel(options);
|
||||
this.panel.init();
|
||||
this.element = this.panel.element;
|
||||
this.element.classList.add('walk-dock-panel');
|
||||
}
|
||||
|
||||
public init(): void {}
|
||||
|
||||
public setPlanViewActive(active: boolean): void {
|
||||
this.panel.setPlanViewActive(active);
|
||||
}
|
||||
|
||||
public setLocales(): void {
|
||||
this.panel.setLocales();
|
||||
}
|
||||
|
||||
public setTheme(theme: ThemeConfig): void {
|
||||
this.panel.setTheme(theme);
|
||||
}
|
||||
|
||||
public getState(): WalkControlState {
|
||||
return this.panel.getState();
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
this.panel.destroy();
|
||||
}
|
||||
}
|
||||
@@ -79,7 +79,7 @@ export class WalkPathPanel implements IBimComponent {
|
||||
*/
|
||||
private render(): void {
|
||||
this.element.innerHTML = '';
|
||||
|
||||
|
||||
// 渲染路径设置区域
|
||||
const settings = this.createSettingsSection();
|
||||
this.element.appendChild(settings);
|
||||
@@ -104,13 +104,13 @@ export class WalkPathPanel implements IBimComponent {
|
||||
// ===== 漫游时间 =====
|
||||
const durationGroup = document.createElement('div');
|
||||
durationGroup.className = 'walk-path-form-group';
|
||||
|
||||
|
||||
const durationLabel = document.createElement('label');
|
||||
durationLabel.textContent = t('walkControl.path.duration');
|
||||
|
||||
|
||||
const durationWrapper = document.createElement('div');
|
||||
durationWrapper.className = 'walk-path-input-wrapper';
|
||||
|
||||
|
||||
const durationInput = document.createElement('input');
|
||||
durationInput.type = 'number';
|
||||
durationInput.className = 'walk-path-input';
|
||||
@@ -121,11 +121,11 @@ export class WalkPathPanel implements IBimComponent {
|
||||
const val = parseInt((e.target as HTMLInputElement).value) || 1;
|
||||
this.duration = val * 1000;
|
||||
};
|
||||
|
||||
|
||||
const durationUnit = document.createElement('span');
|
||||
durationUnit.className = 'walk-path-unit';
|
||||
durationUnit.textContent = t('walkControl.path.durationUnit');
|
||||
|
||||
|
||||
durationWrapper.appendChild(durationInput);
|
||||
durationWrapper.appendChild(durationUnit);
|
||||
durationGroup.appendChild(durationLabel);
|
||||
@@ -134,7 +134,7 @@ export class WalkPathPanel implements IBimComponent {
|
||||
// ===== 循环播放 =====
|
||||
const loopGroup = document.createElement('div');
|
||||
loopGroup.className = 'walk-path-form-group walk-path-form-group-inline';
|
||||
|
||||
|
||||
const loopCheckbox = document.createElement('input');
|
||||
loopCheckbox.type = 'checkbox';
|
||||
loopCheckbox.id = 'walk-path-loop-checkbox';
|
||||
@@ -144,11 +144,11 @@ export class WalkPathPanel implements IBimComponent {
|
||||
// 更新循环播放状态
|
||||
this.loop = (e.target as HTMLInputElement).checked;
|
||||
};
|
||||
|
||||
|
||||
const loopLabel = document.createElement('label');
|
||||
loopLabel.htmlFor = 'walk-path-loop-checkbox';
|
||||
loopLabel.textContent = t('walkControl.path.loop');
|
||||
|
||||
|
||||
loopGroup.appendChild(loopCheckbox);
|
||||
loopGroup.appendChild(loopLabel);
|
||||
|
||||
@@ -390,7 +390,7 @@ export class WalkPathPanel implements IBimComponent {
|
||||
if (!this.element) return;
|
||||
// 设置 CSS 变量
|
||||
this.element.style.setProperty('--bim-text-primary', theme.textPrimary ?? '#fff');
|
||||
this.element.style.setProperty('--bim-text-secondary', theme.textSecondary ?? '#94a3b8');
|
||||
this.element.style.setProperty('--bim-text-secondary', theme.textPrimary ?? '#fff');
|
||||
this.element.style.setProperty('--bim-bg-elevated', theme.bgElevated ?? '#1f2d3e');
|
||||
this.element.style.setProperty('--bim-border-default', theme.borderDefault ?? '#334155');
|
||||
this.element.style.setProperty('--bim-primary', theme.primary ?? '#3b82f6');
|
||||
|
||||
@@ -26,6 +26,8 @@ import type { AiChatManager } from '../managers/ai-chat-manager';
|
||||
import type { RadialToolbarManager } from '../managers/radial-toolbar-manager';
|
||||
import type { BottomDockManager } from '../managers/bottom-dock-manager';
|
||||
import type { MeasureDockManager } from '../managers/measure-dock-manager';
|
||||
import type { SectionDockManager } from '../managers/section-dock-manager';
|
||||
import type { WalkDockManager } from '../managers/walk-dock-manager';
|
||||
|
||||
/**
|
||||
* Manager 注册表 - 实例模式
|
||||
@@ -79,6 +81,8 @@ export class ManagerRegistry {
|
||||
public radialToolbar: RadialToolbarManager | null = null;
|
||||
public bottomDock: BottomDockManager | null = null;
|
||||
public measureDock: MeasureDockManager | null = null;
|
||||
public sectionDock: SectionDockManager | null = null;
|
||||
public walkDock: WalkDockManager | null = null;
|
||||
|
||||
constructor() {}
|
||||
|
||||
@@ -107,6 +111,8 @@ export class ManagerRegistry {
|
||||
this.radialToolbar = null;
|
||||
this.bottomDock = null;
|
||||
this.measureDock = null;
|
||||
this.sectionDock = null;
|
||||
this.walkDock = null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,6 +6,8 @@ import { RightKeyManager } from './managers/right-key-manager';
|
||||
import { RadialToolbarManager } from './managers/radial-toolbar-manager';
|
||||
import { BottomDockManager } from './managers/bottom-dock-manager';
|
||||
import { MeasureDockManager } from './managers/measure-dock-manager';
|
||||
import { SectionDockManager } from './managers/section-dock-manager';
|
||||
import { WalkDockManager } from './managers/walk-dock-manager';
|
||||
|
||||
import { MeasureDialogManager } from './managers/measure-dialog-manager';
|
||||
import { SectionPlaneDialogManager } from './managers/section-plane-dialog-manager';
|
||||
@@ -46,6 +48,8 @@ export class CusBimEngine {
|
||||
public radialToolbar: RadialToolbarManager | null = null;
|
||||
public bottomDock: BottomDockManager | null = null;
|
||||
public measureDock: MeasureDockManager | null = null;
|
||||
public sectionDock: SectionDockManager | null = null;
|
||||
public walkDock: WalkDockManager | null = null;
|
||||
|
||||
public measure: MeasureDialogManager | null = null;
|
||||
public sectionPlane: SectionPlaneDialogManager | null = null;
|
||||
@@ -150,6 +154,14 @@ export class CusBimEngine {
|
||||
this.registry.measureDock = this.measureDock;
|
||||
this.measureDock.init();
|
||||
|
||||
this.sectionDock = new SectionDockManager(this.registry);
|
||||
this.registry.sectionDock = this.sectionDock;
|
||||
this.sectionDock.init();
|
||||
|
||||
this.walkDock = new WalkDockManager(this.registry);
|
||||
this.registry.walkDock = this.walkDock;
|
||||
this.walkDock.init();
|
||||
|
||||
this.radialToolbar = new RadialToolbarManager(this.wrapper, this.registry);
|
||||
|
||||
this.measure = new MeasureDialogManager(this.registry);
|
||||
@@ -252,6 +264,8 @@ export class CusBimEngine {
|
||||
|
||||
this.radialToolbar?.destroy();
|
||||
this.measureDock?.destroy();
|
||||
this.sectionDock?.destroy();
|
||||
this.walkDock?.destroy();
|
||||
this.bottomDock?.destroy();
|
||||
this.engine?.destroy();
|
||||
this.dialog?.destroy();
|
||||
|
||||
@@ -143,6 +143,7 @@ export const enUS: TranslationDictionary = {
|
||||
actions: {
|
||||
hide: 'Hide',
|
||||
reverse: 'Reverse',
|
||||
reset: 'Reset',
|
||||
axisX: 'X',
|
||||
axisY: 'Y',
|
||||
axisZ: 'Z'
|
||||
|
||||
@@ -154,6 +154,7 @@ export interface TranslationDictionary {
|
||||
actions: {
|
||||
hide: string;
|
||||
reverse: string;
|
||||
reset: string;
|
||||
axisX: string;
|
||||
axisY: string;
|
||||
axisZ: string;
|
||||
|
||||
@@ -143,6 +143,7 @@ export const zhCN: TranslationDictionary = {
|
||||
actions: {
|
||||
hide: '隐藏',
|
||||
reverse: '反向',
|
||||
reset: '重置',
|
||||
axisX: 'X',
|
||||
axisY: 'Y',
|
||||
axisZ: 'Z'
|
||||
|
||||
@@ -143,6 +143,7 @@ export const zhTW: TranslationDictionary = {
|
||||
actions: {
|
||||
hide: '隱藏',
|
||||
reverse: '反向',
|
||||
reset: '重設',
|
||||
axisX: 'X',
|
||||
axisY: 'Y',
|
||||
axisZ: 'Z'
|
||||
|
||||
@@ -144,13 +144,25 @@ export class BottomDockManager extends BaseManager {
|
||||
this.register({
|
||||
id: 'section',
|
||||
title: '剖切',
|
||||
createContent: () => this.stack.createPlaceholderContent('剖切面板占位')
|
||||
createContent: () => {
|
||||
const sectionPanel = this.registry.sectionDock?.getPanelElement();
|
||||
if (sectionPanel) {
|
||||
return sectionPanel;
|
||||
}
|
||||
return this.stack.createPlaceholderContent('剖切面板占位');
|
||||
}
|
||||
});
|
||||
|
||||
this.register({
|
||||
id: 'walk',
|
||||
title: '漫游',
|
||||
createContent: () => this.stack.createPlaceholderContent('漫游面板占位')
|
||||
createContent: () => {
|
||||
const walkPanel = this.registry.walkDock?.getPanelElement();
|
||||
if (walkPanel) {
|
||||
return walkPanel;
|
||||
}
|
||||
return this.stack.createPlaceholderContent('漫游面板占位');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
123
src/managers/section-dock-manager.ts
Normal file
123
src/managers/section-dock-manager.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import { BaseManager } from '../core/base-manager';
|
||||
import { ManagerRegistry } from '../core/manager-registry';
|
||||
import { SectionDockPanel, type SectionDockAxis, type SectionDockType } from '../components/section-dock-panel';
|
||||
import { themeManager } from '../services/theme';
|
||||
import { localeManager } from '../services/locale';
|
||||
|
||||
export class SectionDockManager extends BaseManager {
|
||||
private panel: SectionDockPanel | null = null;
|
||||
private unsubscribeTheme: (() => void) | null = null;
|
||||
private unsubscribeLocale: (() => void) | null = null;
|
||||
private unsubscribeDockState: (() => void) | null = null;
|
||||
|
||||
constructor(registry: ManagerRegistry) {
|
||||
super(registry);
|
||||
}
|
||||
|
||||
public init(): void {
|
||||
if (!this.unsubscribeTheme) {
|
||||
this.unsubscribeTheme = themeManager.subscribe(() => {
|
||||
this.applyPresentation();
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.unsubscribeLocale) {
|
||||
this.unsubscribeLocale = localeManager.subscribe(() => {
|
||||
this.applyPresentation();
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.unsubscribeDockState) {
|
||||
this.unsubscribeDockState = this.registry.bottomDock?.onStateChange((state) => {
|
||||
if (state.id !== 'section') {
|
||||
return;
|
||||
}
|
||||
if (!state.open) {
|
||||
console.log("deactivateSection")
|
||||
this.engineComponent?.deactivateSection();
|
||||
return;
|
||||
}
|
||||
this.panel?.resetForOpen();
|
||||
}) ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
if (this.unsubscribeTheme) {
|
||||
this.unsubscribeTheme();
|
||||
this.unsubscribeTheme = null;
|
||||
}
|
||||
if (this.unsubscribeLocale) {
|
||||
this.unsubscribeLocale();
|
||||
this.unsubscribeLocale = null;
|
||||
}
|
||||
if (this.unsubscribeDockState) {
|
||||
this.unsubscribeDockState();
|
||||
this.unsubscribeDockState = null;
|
||||
}
|
||||
|
||||
this.panel?.destroy();
|
||||
this.panel = null;
|
||||
super.destroy();
|
||||
}
|
||||
|
||||
public getPanelElement(): HTMLElement {
|
||||
if (!this.panel) {
|
||||
this.panel = new SectionDockPanel({
|
||||
defaultType: 'face',
|
||||
defaultAxis: 'x',
|
||||
defaultHidden: false,
|
||||
onTypeChange: (type, axis) => {
|
||||
this.activateByType(type, axis);
|
||||
},
|
||||
onAxisChange: (axis) => {
|
||||
if (this.panel?.getActiveType() === 'axis') {
|
||||
this.engineComponent?.activeSection(axis);
|
||||
}
|
||||
},
|
||||
onHideToggle: (hidden) => {
|
||||
if (hidden) {
|
||||
this.engineComponent?.hideSection();
|
||||
return;
|
||||
}
|
||||
this.engineComponent?.recoverSection();
|
||||
},
|
||||
onReverse: () => {
|
||||
this.engineComponent?.reverseSection();
|
||||
},
|
||||
onReset: (type, axis) => {
|
||||
this.engineComponent?.deactivateSection();
|
||||
this.activateByType(type, axis);
|
||||
},
|
||||
onFitToModel: () => {
|
||||
this.engineComponent?.scaleSectionBox();
|
||||
}
|
||||
});
|
||||
this.panel.init();
|
||||
}
|
||||
|
||||
this.panel.resetForOpen();
|
||||
this.applyPresentation();
|
||||
return this.panel.element;
|
||||
}
|
||||
|
||||
private activateByType(type: SectionDockType, axis: SectionDockAxis): void {
|
||||
if (type === 'face') {
|
||||
this.engineComponent?.activeSection('face');
|
||||
return;
|
||||
}
|
||||
if (type === 'box') {
|
||||
this.engineComponent?.activeSection('box');
|
||||
return;
|
||||
}
|
||||
this.engineComponent?.activeSection(axis);
|
||||
}
|
||||
|
||||
private applyPresentation(): void {
|
||||
if (!this.panel) {
|
||||
return;
|
||||
}
|
||||
this.panel.setTheme(themeManager.getTheme());
|
||||
this.panel.setLocales();
|
||||
}
|
||||
}
|
||||
@@ -100,8 +100,9 @@ export class SettingDialogManager extends BaseDialogManager {
|
||||
}
|
||||
|
||||
private ensurePresetListWithInternalDefault(): void {
|
||||
const builtin = this.createInternalDefaultPreset();
|
||||
const withoutInternal = this.presetList.filter((item) => item.id !== SettingDialogManager.INTERNAL_DEFAULT_PRESET_ID);
|
||||
const hasCustomDefault = withoutInternal.some((item) => item.isDefault);
|
||||
const builtin = this.createInternalDefaultPreset(!hasCustomDefault);
|
||||
this.presetList = [...withoutInternal, builtin];
|
||||
if (!this.selectedPresetId || !this.presetList.some((item) => item.id === this.selectedPresetId)) {
|
||||
this.selectedPresetId = builtin.id;
|
||||
@@ -118,21 +119,24 @@ export class SettingDialogManager extends BaseDialogManager {
|
||||
this.ensurePresetListWithInternalDefault();
|
||||
}
|
||||
|
||||
private createInternalDefaultPreset(): EngineSettingPreset {
|
||||
private createInternalDefaultPreset(isDefault: boolean = true): EngineSettingPreset {
|
||||
return {
|
||||
id: SettingDialogManager.INTERNAL_DEFAULT_PRESET_ID,
|
||||
presetName: t('setting.defaultPresetLabel'),
|
||||
isDefault: true,
|
||||
isDefault,
|
||||
settings: structuredClone(DEFAULT_SETTINGS),
|
||||
source: 'sdk-default',
|
||||
readonly: false,
|
||||
allowModify: false,
|
||||
};
|
||||
}
|
||||
|
||||
public setPresetList(presets: EngineSettingPreset[]): void {
|
||||
this.presetList = this.normalizePresetList(presets);
|
||||
this.ensurePresetListWithInternalDefault();
|
||||
|
||||
this.selectedPresetId = this.resolveDefaultPresetId(this.presetList);
|
||||
|
||||
if (!this.selectedPresetId && this.presetList.length > 0) {
|
||||
this.selectedPresetId = this.presetList[0].id;
|
||||
}
|
||||
@@ -169,11 +173,11 @@ export class SettingDialogManager extends BaseDialogManager {
|
||||
this.ensureSliderStyle();
|
||||
|
||||
const content = document.createElement('div');
|
||||
content.style.cssText = 'max-height: 520px; display: flex; flex-direction: column;';
|
||||
content.style.cssText = 'height: 600px; display: flex; flex-direction: column;';
|
||||
|
||||
const scrollBody = document.createElement('div');
|
||||
scrollBody.className = 'setting-scroll-body';
|
||||
scrollBody.style.cssText = 'padding: 14px 16px 12px 16px; overflow-y: auto; display: flex; flex-direction: column; gap: 6px;';
|
||||
scrollBody.style.cssText = 'padding: 14px 16px 12px 16px; overflow-y: auto; display: flex; flex-direction: column; gap: 6px; flex: 1; min-height: 0;';
|
||||
|
||||
scrollBody.appendChild(this.createRenderModeSection());
|
||||
scrollBody.appendChild(this.createSliderSection(
|
||||
@@ -228,7 +232,11 @@ export class SettingDialogManager extends BaseDialogManager {
|
||||
const engine = this.engineComponent;
|
||||
if (!engine) return;
|
||||
|
||||
const savedSelectedId = this.selectedPresetId;
|
||||
this.ensurePresetListWithInternalDefault();
|
||||
if (savedSelectedId && this.presetList.some((item) => item.id === savedSelectedId)) {
|
||||
this.selectedPresetId = savedSelectedId;
|
||||
}
|
||||
this.presetLists = engine.getPresetLists();
|
||||
this.settings = engine.getSettings();
|
||||
this.isDirty = this.getCurrentSettingsDirtyState(this.settings);
|
||||
@@ -314,23 +322,32 @@ export class SettingDialogManager extends BaseDialogManager {
|
||||
|
||||
const actions = document.createElement('div');
|
||||
const canDelete = this.isSelectedPresetDeletable();
|
||||
actions.style.cssText = canDelete
|
||||
? 'display: grid; grid-template-columns: minmax(0, 1.55fr) 40px minmax(0, 0.7fr) minmax(0, 0.95fr); gap: 8px; align-items: center;'
|
||||
: 'display: grid; grid-template-columns: minmax(0, 1.55fr) minmax(0, 0.7fr) minmax(0, 0.95fr); gap: 8px; align-items: center;';
|
||||
const allowModify = this.isSelectedPresetAllowModify();
|
||||
|
||||
actions.style.cssText = 'display: grid; grid-template-columns: minmax(0, 1.55fr) 40px minmax(0, 0.7fr) minmax(0, 0.95fr); gap: 8px; align-items: center;';
|
||||
|
||||
actions.appendChild(this.createBottomPresetSelect());
|
||||
|
||||
if (canDelete) {
|
||||
if (allowModify && canDelete) {
|
||||
actions.appendChild(this.createDeleteIconButton());
|
||||
} else {
|
||||
const placeholder = document.createElement('div');
|
||||
placeholder.style.cssText = 'width: 40px; height: 36px;';
|
||||
actions.appendChild(placeholder);
|
||||
}
|
||||
|
||||
actions.appendChild(this.createFooterActionButton(t('setting.savePreset'), {
|
||||
background: PRIMARY,
|
||||
color: TEXT_INVERSE,
|
||||
border: 'none',
|
||||
onClick: () => this.handleSaveCurrentPreset(),
|
||||
compact: true,
|
||||
}));
|
||||
if (allowModify) {
|
||||
actions.appendChild(this.createFooterActionButton(t('setting.savePreset'), {
|
||||
background: PRIMARY,
|
||||
color: TEXT_INVERSE,
|
||||
border: 'none',
|
||||
onClick: () => this.handleSaveCurrentPreset(),
|
||||
compact: true,
|
||||
}));
|
||||
} else {
|
||||
const placeholder = document.createElement('div');
|
||||
actions.appendChild(placeholder);
|
||||
}
|
||||
|
||||
actions.appendChild(this.createFooterActionButton(t('setting.saveAsNewPreset'), {
|
||||
background: PRIMARY,
|
||||
@@ -371,9 +388,17 @@ export class SettingDialogManager extends BaseDialogManager {
|
||||
private isSelectedPresetDeletable(): boolean {
|
||||
if (!this.selectedPresetId) return false;
|
||||
if (this.selectedPresetId === SettingDialogManager.INTERNAL_DEFAULT_PRESET_ID) return false;
|
||||
const preset = this.presetList.find((item) => item.id === this.selectedPresetId);
|
||||
if (preset?.allowModify === false) return false;
|
||||
return this.presetList.some((item) => item.id === this.selectedPresetId);
|
||||
}
|
||||
|
||||
private isSelectedPresetAllowModify(): boolean {
|
||||
if (!this.selectedPresetId) return true;
|
||||
const preset = this.presetList.find((item) => item.id === this.selectedPresetId);
|
||||
return preset?.allowModify !== false;
|
||||
}
|
||||
|
||||
private createBottomPresetSelect(): HTMLElement {
|
||||
const options: SelectOption[] = [];
|
||||
if (this.presetList.length === 0) {
|
||||
@@ -875,6 +900,9 @@ export class SettingDialogManager extends BaseDialogManager {
|
||||
}
|
||||
|
||||
private handleSaveCurrentPreset(): void {
|
||||
if (!this.isSelectedPresetAllowModify()) {
|
||||
return;
|
||||
}
|
||||
if (!this.selectedPresetId) {
|
||||
window.alert(t('setting.selectPresetFirst'));
|
||||
return;
|
||||
@@ -886,14 +914,20 @@ export class SettingDialogManager extends BaseDialogManager {
|
||||
return;
|
||||
}
|
||||
|
||||
const savedPresetId = this.selectedPresetId;
|
||||
const currentSettings = this.engineComponent?.getSettings() ?? structuredClone(this.settings);
|
||||
const updatedPreset: EngineSettingPreset = {
|
||||
...this.presetList[index],
|
||||
settings: structuredClone(currentSettings),
|
||||
isDefault: true,
|
||||
};
|
||||
|
||||
this.presetList = this.presetList.map((item, i) => (i === index ? updatedPreset : item));
|
||||
this.presetList = this.presetList.map((item, i) => ({
|
||||
...(i === index ? updatedPreset : item),
|
||||
isDefault: i === index,
|
||||
}));
|
||||
this.ensurePresetListWithInternalDefault();
|
||||
this.selectedPresetId = savedPresetId;
|
||||
this.settings = structuredClone(currentSettings);
|
||||
this.isDirty = false;
|
||||
this.updateUndoRestoreVisibility();
|
||||
@@ -922,14 +956,14 @@ export class SettingDialogManager extends BaseDialogManager {
|
||||
const preset: EngineSettingPreset = {
|
||||
id: `user-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||
presetName: trimmedName,
|
||||
isDefault: false,
|
||||
isDefault: true,
|
||||
settings: structuredClone(currentSettings),
|
||||
source: 'user',
|
||||
readonly: false,
|
||||
};
|
||||
|
||||
const withoutInternal = this.presetList.filter((item) => item.id !== SettingDialogManager.INTERNAL_DEFAULT_PRESET_ID);
|
||||
this.presetList = [...withoutInternal, preset];
|
||||
this.presetList = [...withoutInternal.map((item) => ({ ...item, isDefault: false })), preset];
|
||||
this.ensurePresetListWithInternalDefault();
|
||||
this.selectedPresetId = preset.id;
|
||||
this.settings = structuredClone(currentSettings);
|
||||
@@ -945,7 +979,7 @@ export class SettingDialogManager extends BaseDialogManager {
|
||||
}
|
||||
|
||||
private openDeletePresetConfirmModal(): void {
|
||||
if (!this.isSelectedPresetDeletable()) {
|
||||
if (!this.isSelectedPresetAllowModify() || !this.isSelectedPresetDeletable()) {
|
||||
return;
|
||||
}
|
||||
this.closeDeletePresetConfirmModal();
|
||||
|
||||
125
src/managers/walk-dock-manager.ts
Normal file
125
src/managers/walk-dock-manager.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { BaseManager } from '../core/base-manager';
|
||||
import { ManagerRegistry } from '../core/manager-registry';
|
||||
import { WalkDockPanel } from '../components/walk-dock-panel';
|
||||
import { WalkPathDialogManager } from './walk-path-dialog-manager';
|
||||
|
||||
export class WalkDockManager extends BaseManager {
|
||||
private panel: WalkDockPanel | null = null;
|
||||
private pathManager: WalkPathDialogManager | null = null;
|
||||
private unsubscribeDockState: (() => void) | null = null;
|
||||
|
||||
constructor(registry: ManagerRegistry) {
|
||||
super(registry);
|
||||
}
|
||||
|
||||
public init(): void {
|
||||
this.pathManager = new WalkPathDialogManager(this.registry);
|
||||
this.pathManager.init();
|
||||
|
||||
if (!this.unsubscribeDockState) {
|
||||
this.unsubscribeDockState = this.registry.bottomDock?.onStateChange((state) => {
|
||||
if (state.id !== 'walk') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.open) {
|
||||
this.onOpen();
|
||||
return;
|
||||
}
|
||||
|
||||
this.onClose();
|
||||
}) ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
if (this.unsubscribeDockState) {
|
||||
this.unsubscribeDockState();
|
||||
this.unsubscribeDockState = null;
|
||||
}
|
||||
|
||||
this.onClose();
|
||||
this.pathManager?.destroy();
|
||||
this.pathManager = null;
|
||||
super.destroy();
|
||||
}
|
||||
|
||||
public getPanelElement(): HTMLElement {
|
||||
if (!this.panel) {
|
||||
this.panel = new WalkDockPanel({
|
||||
onPlanViewToggle: (isActive) => {
|
||||
this.engineComponent?.toggleMiniMap();
|
||||
this.registry.toolbar?.setBtnActive('map', isActive);
|
||||
this.emit('walk:plan-view-toggle', { isActive });
|
||||
},
|
||||
onPathModeToggle: (isActive) => {
|
||||
if (isActive) {
|
||||
this.pathManager?.show();
|
||||
} else {
|
||||
this.pathManager?.hide();
|
||||
}
|
||||
this.emit('walk:path-mode-toggle', { isActive });
|
||||
},
|
||||
onWalkModeToggle: (isActive) => {
|
||||
if (isActive) {
|
||||
this.pathManager?.hide();
|
||||
}
|
||||
this.emit('walk:walk-mode-toggle', { isActive });
|
||||
},
|
||||
onSpeedChange: (speed) => {
|
||||
const engineSpeed = speed * 0.1;
|
||||
this.engineComponent?.setWalkSpeed(engineSpeed);
|
||||
this.emit('walk:speed-change', { speed });
|
||||
},
|
||||
onGravityToggle: (enabled) => {
|
||||
this.engineComponent?.setWalkGravity(enabled);
|
||||
this.emit('walk:gravity-toggle', { enabled });
|
||||
},
|
||||
onCollisionToggle: (enabled) => {
|
||||
this.engineComponent?.setWalkCollision(enabled);
|
||||
this.emit('walk:collision-toggle', { enabled });
|
||||
},
|
||||
onCharacterModelChange: (model) => {
|
||||
void model;
|
||||
},
|
||||
onWalkModeChange: (mode) => {
|
||||
void mode;
|
||||
},
|
||||
onExit: () => {
|
||||
this.registry.bottomDock?.close('walk');
|
||||
}
|
||||
});
|
||||
this.panel.init();
|
||||
}
|
||||
|
||||
this.syncMapState();
|
||||
return this.panel.element;
|
||||
}
|
||||
|
||||
private onOpen(): void {
|
||||
this.engineComponent?.activateFirstPersonMode();
|
||||
this.syncMapState();
|
||||
}
|
||||
|
||||
private onClose(): void {
|
||||
this.pathManager?.hide();
|
||||
|
||||
if (this.engineComponent?.getMiniMapState()) {
|
||||
this.engineComponent.toggleMiniMap();
|
||||
}
|
||||
|
||||
this.engineComponent?.deactivateFirstPersonMode();
|
||||
|
||||
if (this.panel) {
|
||||
this.panel.destroy();
|
||||
this.panel = null;
|
||||
}
|
||||
|
||||
this.registry.toolbar?.setBtnActive('map', false);
|
||||
}
|
||||
|
||||
private syncMapState(): void {
|
||||
const mapState = this.engineComponent?.getMiniMapState() ?? false;
|
||||
this.panel?.setPlanViewActive(mapState);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user