refactor: sync managers and section box actions

Wire section box scale/reverse/reset to clipping APIs and sync demo artifacts.
This commit is contained in:
yuding
2026-02-04 18:20:30 +08:00
parent b12940f49c
commit 191c571f40
64 changed files with 10569 additions and 7065 deletions

View File

@@ -1,9 +1,8 @@
{ {
"active_plan": null, "active_plan": "/Users/yuding/WORK/LYZ/project/bimEngine/engine/.sisyphus/plans/refactor-model-operations.md",
"completed_at": "2026-02-02T09:45:00.000Z", "started_at": "2026-02-04T07:23:30.594Z",
"last_plan": "clipping-api-migration",
"session_ids": [ "session_ids": [
"ses_3e2bc84f9ffeHmiDS2pkiLtX2n" "ses_3dec861d2ffekLv6QMzLySFf7p"
], ],
"status": "completed" "plan_name": "refactor-model-operations"
} }

View File

@@ -0,0 +1,5 @@
# Decisions - Path Roaming Implementation
This notepad tracks architectural and design decisions made during implementation.
---

View File

@@ -0,0 +1,5 @@
# Issues - Path Roaming Implementation
This notepad tracks problems, gotchas, and issues encountered during implementation.
---

View File

@@ -0,0 +1,199 @@
# Learnings - Path Roaming Implementation
This notepad tracks conventions, patterns, and accumulated wisdom from the path-roaming feature implementation.
---
## [2026-02-03T07:48:40] Codebase Exploration - Locale and Component Patterns
### Locale Files Structure
- **Location**: `src/locales/{types.ts, zh-CN.ts, en-US.ts}`
- **Pattern**: Nested objects matching TypeScript interface
- **Existing `walkControl.path` field**: Already contains `dialogTitle` property
- **Line ranges**:
- types.ts: lines 158-176
- zh-CN.ts: lines 151-168
- en-US.ts: lines 151-168
### Engine/EngineManager Method Patterns
- **Engine class** (`src/components/engine/index.ts`):
- Use guard clauses: `if (!this._isInitialized || !this.engine?.xxx)`
- Console warnings for uninitialized state: `console.warn('[Engine] ...')`
- Return `null` explicitly (not `undefined`)
- JSDoc comments required
- Type assertions where needed
- **EngineManager class** (`src/managers/engine-manager.ts`):
- Proxy pattern: `this.engineInstance?.methodName() ?? null`
- Optional chaining with nullish coalescing
- Consistent return types with Engine class
- **getEngineInfo() locations**:
- Engine: line 691-697
- EngineManager: line 426-428
### Component Implementation Patterns
- **WalkPathPanel status**: Exists as stub (55 lines), missing CSS and types files
- **Reference implementations**: MeasurePanel (888 lines), WalkControlPanel (468 lines)
- **Core interface**: `IBimComponent` with init(), destroy(), setTheme(), setLocales()
- **Subscriptions**: localeManager and themeManager subscriptions in init()
- **CSS variables**: Prefix with `--bim-*` for theming
- **Class naming**: `bim-[panel-name]-[element]` convention
- **Import pattern**: CSS at top, services/types imports follow
## [2026-02-03T07:50:15] Task 1 Complete - I18n Text Added
### Changes Made
- **types.ts**: Expanded `walkControl.path` from 1 field to 9 fields (dialogTitle + 8 new fields)
- **zh-CN.ts**: Added Chinese translations for all 9 fields
- **en-US.ts**: Added English translations for all 9 fields
### Fields Added
1. duration - 漫游时间 / Duration
2. durationUnit - 秒 / s
3. loop - 循环播放 / Loop
4. addPoint - 添加漫游点 / Add Point
5. deleteAll - 删除全部 / Delete All
6. point - 漫游点 / Point
7. play - 播放漫游 / Play
8. noPoints - 暂无漫游点,请添加 / No points yet
### Verification
- ✅ Build succeeded: `npm run build` passed with no errors
- ✅ All three files modified with exact code from plan
- ✅ Type safety maintained (TypeScript interface matches implementation)
### Notes
- Chinese JSDoc comments in types.ts are intentional per plan specification
- Subagent failed with JSON parse error, completed directly as trivial edit
## [2026-02-03T07:51:30] Task 2 Complete - Types.ts Created
### File Created
- **Location**: `src/components/walk-path-panel/types.ts`
- **Size**: 24 lines
### Interfaces Defined
1. **RoamingPoint**: Represents a single roaming point with `index: number`
2. **PlayOptions**: Configuration for playback with 4 optional fields:
- duration?: number (milliseconds)
- loop?: boolean
- onComplete?: () => void
- onPointComplete?: (pointIndex: number) => void
### Documentation
- All interfaces have Chinese JSDoc comments (per plan requirement)
- All fields have inline comments explaining purpose
- Public API documentation complete
### Verification
- ✅ Build succeeded: `npm run build` passed
- ✅ File created with exact code from plan
- ✅ Type definitions ready for component implementation
## [2026-02-03T07:52:30] Task 3 Complete - Engine pathRoaming Methods Added
### Changes Made
- **File**: `src/components/engine/index.ts`
- **Location**: After `getEngineInfo()` method (line 697)
- **Lines added**: ~90 lines of code
### Methods Added
1. **pathRoamingAddPoint()** - void - Add current camera position as roaming point
2. **pathRoamingRemovePoint(index)** - void - Remove point by index
3. **pathRoamingClearPoints()** - void - Clear all points
4. **pathRoamingGetPoints()** - any[] - Get all points array
5. **pathRoamingJumpToPoint(index)** - void - Jump to specific point
6. **pathRoamingPlay(options)** - void - Play roaming with callbacks
### Pattern Compliance
- ✅ Guard clauses: `if (!this._isInitialized || !this.engine?.pathRoaming)`
- ✅ Console warnings: `console.warn('[Engine] pathRoaming not available')`
- ✅ JSDoc comments for all public methods
- ✅ Internal comments explaining logic flow
- ✅ Section markers for code organization
- ✅ Consistent with existing Engine class patterns
### Verification
- ✅ Build succeeded: `npm run build` passed
- ✅ All 6 methods added with exact signatures from plan
- ✅ Ready for EngineManager proxy methods
## [2026-02-03T07:53:15] Task 4 Complete - EngineManager Proxy Methods Added
### Changes Made
- **File**: `src/managers/engine-manager.ts`
- **Location**: After `getEngineInfo()` method (line 428)
- **Lines added**: ~60 lines of code
### Proxy Methods Added
All 6 methods delegate to `this.engineInstance`:
1. **pathRoamingAddPoint()** - Proxies to engineInstance
2. **pathRoamingRemovePoint(index)** - Proxies with parameter
3. **pathRoamingClearPoints()** - Proxies to engineInstance
4. **pathRoamingGetPoints()** - Returns array with nullish coalescing
5. **pathRoamingJumpToPoint(index)** - Proxies with parameter
6. **pathRoamingPlay(options)** - Proxies with options object
### Pattern Compliance
- ✅ Proxy pattern: `this.engineInstance?.methodName()`
- ✅ Optional chaining for safety
- ✅ Nullish coalescing for getPoints: `?? []`
- ✅ JSDoc comments matching Engine class
- ✅ Section markers for code organization
- ✅ Consistent with existing EngineManager patterns
### Verification
- ✅ Build succeeded: `npm run build` passed
- ✅ All 6 proxy methods match Engine class signatures
- ✅ Ready for WalkPathPanel component implementation
## [2026-02-03T07:55:00] Tasks 5-7 Complete - WalkPathPanel & CSS Implemented
### Task 5: WalkPathPanel Component
- **File**: `src/components/walk-path-panel/index.ts`
- **Lines**: 377 lines of TypeScript
- **Complexity**: Complex UI component with state management
#### Features Implemented
1. **Initialization**: Load existing points from engine on panel open
2. **Settings**: Duration input (seconds → milliseconds) and loop checkbox
3. **Point Management**: Add, delete, delete all operations
4. **Point List**: Dynamic rendering with empty state
5. **Playback**: Play with callbacks, highlight current point during playback
6. **Lifecycle**: init(), setLocales(), setTheme(), destroy()
#### Key Patterns
- Render-based updates: Full re-render on state changes
- Callback integration: onPointComplete for highlighting, onComplete for cleanup
- Theme application: CSS variables via setProperty
- I18n integration: Subscribe to localeManager, use t() function
### Task 6: CSS Styling
- **File**: `src/components/walk-path-panel/index.css`
- **Lines**: 233 lines of CSS
- **Sections**: Buttons, Forms, Points Section, Point Items
#### Style Features
1. **Theming**: All colors use CSS variables with fallbacks
2. **Highlight Effect**: `.walk-path-point-item-active` for playing point
3. **Hover Effects**: Button hover states, action button visibility on hover
4. **Responsive**: Flexbox layouts, overflow handling
5. **Transitions**: Smooth hover and state changes
### Task 7: Build Verification
-`npm run build` succeeded
- ✅ Bundle size: 2,078 KB (ES), 1,370 KB (UMD)
- ✅ No TypeScript errors
- ✅ All 7 tasks complete
### Total Implementation
- **Files created**: 2 (types.ts, index.css)
- **Files modified**: 5 (types.ts, zh-CN.ts, en-US.ts, Engine.ts, EngineManager.ts)
- **Files replaced**: 1 (walk-path-panel/index.ts)
- **Total lines added**: ~1,200 lines
- **Build status**: ✅ SUCCESSFUL

View File

@@ -0,0 +1,29 @@
# Problems - Path Roaming Implementation
This notepad tracks unresolved blockers and critical problems requiring attention.
---
## [2026-02-03T07:52:00] Delegation System Failure
### Issue
Subagent delegation consistently failing with JSON parse error:
```
SyntaxError: JSON Parse error: Unexpected EOF
```
### Impact
- Cannot use delegate_task() for any category
- All 3 delegation attempts failed (Tasks 1, 2, 3)
- Forced to complete tasks directly as orchestrator
### Workaround
Completing tasks directly while maintaining:
- Plan specification compliance
- Build verification
- Notepad documentation
### Action Required
- Investigate delegation system JSON serialization
- May need to report to framework maintainers

View File

@@ -0,0 +1,109 @@
## [2026-02-03T09:55] Task 1: I18n Files Update
### Pattern Observed
- All menu i18n keys use JSDoc comments with Chinese descriptions in types.ts
- Format: `/** 中文描述 */\n{key}: string;`
- This is a codebase convention for bilingual documentation
### Implementation
- Added 4 fields to menu object: quickSelect, selectSameType, selectSameLevel, selectSameLevelType
- Maintained alphabetical ordering after existing fields
- Chinese: 快速选择, 选择同类模型, 选择同层模型, 选择同层同类模型
- English: Quick Select, Select Same Type, Select Same Level, Select Same Level & Type
### Verification
- tsc --noEmit passed with no errors
- All three files updated consistently
## [2026-02-03T09:58] Task 2: Engine Layer Methods
### Implementation
- Added 8 public methods to Engine class between lines 792-903
- Pattern: check initialization → get highlightModels → call engine.modelToolModule API
- Section markers: `// ==================== 构件操作 ====================`
- Each method has JSDoc with Chinese description (SDK convention)
### API Mapping
```
hideSelectedModels() → modelToolModule.hideModel(models)
translucentSelectedModels() → modelToolModule.translucentModel(models)
isolateSelectedModels() → modelToolModule.isolateModel(models)
translucentOtherModels() → modelToolModule.translucentOtherModel(models)
showAllModels() → modelToolModule.showAllModels()
batchSelectSameTypeModel() → modelToolModule.batchSelectSameTypeModel(models)
batchSelectSameLevelModel() → modelToolModule.batchSelectSameLevelModel(models)
batchSelectSameLevelTypeModel() → modelToolModule.batchSelectSameLevelTypeModel(models)
```
### Verification
- TypeScript compilation: PASSED
- Methods follow existing patterns (fitSectionBoxToModel as reference)
- Positioned correctly before destroy() method
## [2026-02-03T10:00] Task 3: EngineManager Proxy Methods
### Implementation
- Added 8 proxy methods to EngineManager class (lines 494-555)
- Pattern: Simple delegation to engineInstance methods using optional chaining
- Format: `this.engineInstance?.{methodName}();`
- Each method has JSDoc with Chinese description
### Verification
- TypeScript compilation: PASSED
- All 8 methods correctly delegate to Engine layer
- Positioned between path roaming and destroy() method
## [2026-02-03T10:03] Task 4: Right-Click Menu Handler Updates
### Implementation
- Replaced all console.log placeholders with actual method calls
- Added "快速选择" submenu with 3 options (order: 5)
- Updated menu item ordering:
- isolateSelected: order 4 (unchanged)
- quickSelect: order 5 (NEW)
- fitSectionBox: order 5 → 6
- showAll: order 6 → 7
### Menu Structure
```
1. 构件详情 (componentDetail)
2. 隐藏选中构件 (hideSelected) → hideSelectedModels()
3. 半透明选中构件 (transparentSelected) → translucentSelectedModels()
4. 隔离选中构件 (isolateSelected) → SUBMENU
- 其他构件隐藏 (hideOthers) → isolateSelectedModels()
- 其他构件半透明 (transparentOthers) → translucentOtherModels()
5. 快速选择 (quickSelect) → SUBMENU [NEW]
- 选择同类模型 (selectSameType) → batchSelectSameTypeModel()
- 选择同层模型 (selectSameLevel) → batchSelectSameLevelModel()
- 选择同层同类模型 (selectSameLevelType) → batchSelectSameLevelTypeModel()
6. 剖切盒适应 (fitSectionBox) → fitSectionBoxToModel()
7. 显示全部 (showAll) → showAllModels() [always visible]
```
### Verification
- TypeScript compilation: PASSED
- All console.log placeholders replaced
- All i18n keys properly linked to translations
## [2026-02-03T10:05] Task 5: Build & Commit
### Build Verification
- Command: `npm run build`
- Result: SUCCESS
- Output size: 2,084.68 kB (ES), 1,374.53 kB (UMD)
- Build time: 4.06s
### Commit
- Hash: 89783d0
- Message: "feat(menu): implement right-click menu functions for model operations"
- Files staged: 5 (types.ts, zh-CN.ts, en-US.ts, engine/index.ts, engine-manager.ts)
- Stats: 551 insertions(+), 118 deletions(-)
### Summary
All 5 tasks completed successfully:
1. ✅ I18n files updated (4 new fields)
2. ✅ Engine layer: 8 methods added
3. ✅ EngineManager: 8 proxy methods added
4. ✅ Right-click menu handler: All functions connected + quick select submenu
5. ✅ Build verification: PASSED

View File

@@ -1,420 +0,0 @@
# 构件详情弹窗 Bug 修复
## TL;DR
> **Quick Summary**: 修复构件详情弹窗的 5 个问题:删除重复的 PropertyPanelManager统一使用 ComponentDetailManager修复 CSS 样式(背景色、左边距);修复选中切换时内容不更新的问题。
>
> **Deliverables**:
> - 删除 PropertyPanelManager 及所有引用
> - 工具栏按钮改用 ComponentDetailManager
> - 折叠面板样式修复
> - 选中切换功能正常工作
>
> **Estimated Effort**: Medium (2-3 小时)
> **Parallel Execution**: YES - 2 waves
> **Critical Path**: Task 1 → Task 2 → Task 4 → Task 5
---
## Context
### Original Request
用户在测试时发现 5 个问题:
1. 双弹窗 - 底部面板和右键菜单各打开一个弹窗
2. 无背景色 - 折叠面板头部没有背景色
3. 左边距不足 - 头部需要增加左侧 padding
4. Mock 数据 - 工具栏按钮打开的是 mock 数据
5. 选中切换无效 - 点击不同构件内容不更新
### Interview Summary
**Key Discussions**:
- 确认完全删除 PropertyPanelManager不是废弃
- 手动测试策略(非 TDD
- 统一使用 ComponentDetailManager 作为唯一实现
**Research Findings**:
- 发现两套独立实现ComponentDetailManager 和 PropertyPanelManager
- PropertyPanelManager 硬编码了 mock 数据
- Toolbar 按钮调用的是错误的 Manager
- CSS 问题ghost 模式下背景色被设为 transparent
### Root Cause Analysis
| 问题 | 根因 |
|------|------|
| 双弹窗 | 两个 Manager 创建两个不同的 DialogID 不同) |
| 无背景色 | `collapse/index.css` 第 19 行:`.is-ghost .bim-collapse-header { background-color: transparent }` |
| Mock 数据 | `property/index.ts` 调用 `propertyPanel?.show()` 而非 `componentDetail?.show()` |
| 选中切换 | 事件流正确,但需要验证 init() 是否被正确调用 |
---
## Work Objectives
### Core Objective
统一构件详情功能为单一实现ComponentDetailManager修复所有样式和功能问题。
### Concrete Deliverables
- 删除文件:`src/managers/property-panel-manager.ts``.recycle/`
- 修改文件:共 5 个文件需要修改
### Definition of Done
- [ ] `bun run build` 成功,无编译错误
- [ ] 底部工具栏"构件详情"按钮调用 ComponentDetailManager
- [ ] 只能打开一个构件详情弹窗
- [ ] 折叠面板头部有背景色(亮色/暗色模式)
- [ ] 头部有适当的左边距
- [ ] 选中不同构件时弹窗内容自动更新
### Must Have
- 单一弹窗实例
- 背景色在所有主题下可见
- 真实数据加载(非 mock
### Must NOT Have (Guardrails)
- 不得保留 PropertyPanelManager 的任何代码
- 不得在 ComponentDetailManager 中使用 mock 数据
- 不得破坏现有的右键菜单功能
- 不得影响其他 Manager 的功能
---
## Verification Strategy (MANDATORY)
### Test Decision
- **Infrastructure exists**: NO (无自动化测试)
- **User wants tests**: Manual-only
- **Framework**: none
### Manual QA Procedures
**For each TODO, verification is done via playground:**
1. **启动环境**: `bun run dev:demo`
2. **测试工具**: 浏览器开发者工具 + 手动操作
3. **证据收集**: 截图 + 控制台日志
---
## Execution Strategy
### Parallel Execution Waves
```
Wave 1 (Start Immediately):
├── Task 1: 删除 PropertyPanelManager
├── Task 2: 修改 Toolbar 按钮
└── Task 3: 修复 CSS 样式
Wave 2 (After Wave 1):
├── Task 4: 清理 BimEngine 和 Registry 引用
└── Task 5: 验证选中切换功能
Wave 3 (Final):
└── Task 6: 完整回归测试
```
### Dependency Matrix
| Task | Depends On | Blocks | Can Parallelize With |
|------|------------|--------|---------------------|
| 1 | None | 4 | 2, 3 |
| 2 | None | 5 | 1, 3 |
| 3 | None | 5 | 1, 2 |
| 4 | 1 | 5 | None |
| 5 | 2, 3, 4 | 6 | None |
| 6 | 5 | None | None (final) |
---
## TODOs
### Wave 1: Core Changes (可并行)
- [ ] 1. 删除 PropertyPanelManager
**What to do**:
-`src/managers/property-panel-manager.ts` 移动到 `.recycle/YYYY-MM-DD/` 目录
- 记录删除原因
**Must NOT do**:
- 不得直接 `rm` 删除文件
- 不得保留任何代码片段
**Recommended Agent Profile**:
- **Category**: `quick`
- **Skills**: [`git-master`]
- `git-master`: 文件移动和版本控制
**Parallelization**:
- **Can Run In Parallel**: YES
- **Parallel Group**: Wave 1 (with Tasks 2, 3)
- **Blocks**: Task 4
- **Blocked By**: None
**References**:
- `src/managers/property-panel-manager.ts` - 要删除的文件223 行)
- `.recycle/` - 目标回收目录(按 AI_COLLABORATION.md 规范)
**Acceptance Criteria**:
- [ ] 文件已移动到 `.recycle/YYYY-MM-DD/src/managers/property-panel-manager.ts`
- [ ] 创建 `.recycle/YYYY-MM-DD/README.md` 记录删除原因
- [ ] 原位置文件不存在
**Commit**: NO (groups with Task 4)
---
- [ ] 2. 修改 Toolbar 按钮指向 ComponentDetailManager
**What to do**:
- 修改 `src/components/button-group/toolbar/buttons/property/index.ts`
-`registry.propertyPanel?.show()` 改为 `registry.componentDetail?.show()`
**Must NOT do**:
- 不得改变按钮的其他属性id, label, icon
- 不得添加额外逻辑
**Recommended Agent Profile**:
- **Category**: `quick`
- **Skills**: [`coding-standards`]
- `coding-standards`: 确保代码风格一致
**Parallelization**:
- **Can Run In Parallel**: YES
- **Parallel Group**: Wave 1 (with Tasks 1, 3)
- **Blocks**: Task 5
- **Blocked By**: None
**References**:
- `src/components/button-group/toolbar/buttons/property/index.ts:13-17` - 当前实现(调用 propertyPanel
- `src/managers/component-detail-manager.ts:35-50` - ComponentDetailManager.show() 方法
**Acceptance Criteria**:
- [ ] 第 16 行改为 `registry.componentDetail?.show()`
- [ ] 保留 console.log 用于调试
- [ ] 无 TypeScript 错误
**Commit**: NO (groups with Task 4)
---
- [ ] 3. 修复折叠面板 CSS 样式
**What to do**:
- 修改 `src/components/collapse/index.css`
-`.is-ghost .bim-collapse-header` 添加背景色
- 增加左侧 padding
- 移除 ComponentDetailManager 中的内联样式 hack可选
**Must NOT do**:
- 不得破坏非 ghost 模式的样式
- 不得使用 `!important`(除非绝对必要)
**Recommended Agent Profile**:
- **Category**: `visual-engineering`
- **Skills**: [`frontend-ui-ux`]
- `frontend-ui-ux`: CSS 样式专家
**Parallelization**:
- **Can Run In Parallel**: YES
- **Parallel Group**: Wave 1 (with Tasks 1, 2)
- **Blocks**: Task 5
- **Blocked By**: None
**References**:
- `src/components/collapse/index.css:18-22` - 当前 ghost 模式样式background: transparent
- `src/components/collapse/index.css:42-54` - 标准 header 样式(有 background-color
- `src/managers/component-detail-manager.ts:159-167` - 当前的样式 hack可移除
- `src/themes/presets.ts` - 主题变量定义
**Acceptance Criteria**:
- [ ] `.is-ghost .bim-collapse-header``background-color: var(--bim-component-bg)`
- [ ] `.is-ghost .bim-collapse-header``padding-left: 12px` 或类似值
- [ ] 暗色模式下背景可见
- [ ] 亮色模式下背景可见
- [ ] hover 状态仍有不同背景色
**Commit**: NO (groups with Task 4)
---
### Wave 2: Cleanup & Verification
- [ ] 4. 清理 BimEngine 和 ManagerRegistry 中的 PropertyPanelManager 引用
**What to do**:
- 修改 `src/bim-engine.ts`
- 删除 import 语句(第 8 行)
- 删除属性声明(第 37 行)
- 删除实例化代码(第 110 行)
- 删除 registry 注册(第 126 行)
- 删除 destroy 调用(第 156 行)
- 修改 `src/core/manager-registry.ts`
- 删除 import 语句(第 14 行)
- 删除属性声明(第 52-53 行)
**Must NOT do**:
- 不得删除 ComponentDetailManager 相关代码
- 不得改变初始化顺序
**Recommended Agent Profile**:
- **Category**: `quick`
- **Skills**: [`coding-standards`]
- `coding-standards`: TypeScript 代码清理
**Parallelization**:
- **Can Run In Parallel**: NO
- **Parallel Group**: Sequential
- **Blocks**: Task 5
- **Blocked By**: Task 1
**References**:
- `src/bim-engine.ts:8` - import PropertyPanelManager
- `src/bim-engine.ts:37` - propertyPanel 属性声明
- `src/bim-engine.ts:110` - new PropertyPanelManager()
- `src/bim-engine.ts:126` - registry.propertyPanel = ...
- `src/bim-engine.ts:156` - propertyPanel?.destroy()
- `src/core/manager-registry.ts:14` - import type
- `src/core/manager-registry.ts:52-53` - propertyPanel 属性
**Acceptance Criteria**:
- [ ] `bun run build` 成功
- [ ] 无 PropertyPanelManager 相关代码
- [ ] 无未使用的 import 警告
**Commit**: YES
- Message: `refactor(component-detail): 移除 PropertyPanelManager统一使用 ComponentDetailManager`
- Files:
- `.recycle/YYYY-MM-DD/src/managers/property-panel-manager.ts`
- `src/components/button-group/toolbar/buttons/property/index.ts`
- `src/components/collapse/index.css`
- `src/bim-engine.ts`
- `src/core/manager-registry.ts`
- Pre-commit: `bun run build`
---
- [ ] 5. 验证并修复选中切换功能
**What to do**:
- 在 playground 中测试选中切换
- 如果不工作,检查以下几点:
1. `component:selected` 事件是否正确发射Engine 组件)
2. `ComponentDetailManager.init()` 是否被调用
3. `isOpen()` 返回值是否正确
- 根据发现的问题进行修复
**Must NOT do**:
- 不得添加不必要的 console.log调试后删除
- 不得改变事件名称或 payload 结构
**Recommended Agent Profile**:
- **Category**: `unspecified-low`
- **Skills**: [`coding-standards`]
- `coding-standards`: 调试和代码修复
**Parallelization**:
- **Can Run In Parallel**: NO
- **Parallel Group**: Sequential (after Wave 1)
- **Blocks**: Task 6
- **Blocked By**: Tasks 2, 3, 4
**References**:
- `src/components/engine/index.ts:131-145` - 事件发射代码
- `src/managers/component-detail-manager.ts:19-33` - 事件监听代码
- `src/managers/component-detail-manager.ts:68-81` - loadAndRenderContent 方法
**Acceptance Criteria**:
**Manual Execution Verification:**
- [ ] 启动 `bun run dev:demo`
- [ ] 选中构件 A右键打开构件详情
- [ ] 验证:弹窗显示构件 A 的属性
- [ ] 选中构件 B不关闭弹窗
- [ ] 验证:弹窗自动更新为构件 B 的属性
- [ ] 控制台打印 `[Engine] 构件选中:` 日志
**Commit**: YES (if changes made)
- Message: `fix(component-detail): 修复选中切换时内容不更新的问题`
- Files: depends on what needs fixing
- Pre-commit: `bun run build`
---
### Wave 3: Final Verification
- [ ] 6. 完整回归测试
**What to do**:
- 运行 `bun run build` 确保编译通过
- 在 playground 中完整测试所有功能
**Must NOT do**:
- 无代码修改(仅测试)
**Recommended Agent Profile**:
- **Category**: `quick`
- **Skills**: [`playwright`]
- `playwright`: 浏览器自动化测试(可选)
**Parallelization**:
- **Can Run In Parallel**: NO
- **Parallel Group**: Final
- **Blocks**: None (final task)
- **Blocked By**: Task 5
**References**:
- 无(纯测试任务)
**Acceptance Criteria**:
**Manual Execution Verification (COMPLETE REGRESSION):**
**构建验证:**
- [ ] `bun run build` → 成功,无错误
- [ ] `bun run dev:demo` → 启动成功
**问题 1 - 单一弹窗验证:**
- [ ] 点击底部工具栏"构件详情" → 打开弹窗
- [ ] 右键点击"构件详情" → 同一个弹窗(不是新弹窗)
- [ ] 弹窗 ID 在 DevTools 中为 `component-detail-dialog`
**问题 2 - 背景色验证:**
- [ ] 暗色模式:折叠面板头部有可见背景色
- [ ] 切换到亮色模式(如果支持):头部背景仍可见
- [ ] hover 时背景色有变化
**问题 3 - 左边距验证:**
- [ ] 折叠面板标题有适当的左侧间距(不贴边)
**问题 4 - 真实数据验证:**
- [ ] 选中构件后打开弹窗 → 显示真实属性数据
- [ ] 不显示 mock 数据(如 "1f8d-4a2e-9c", "Generic - 200mm"
**问题 5 - 选中切换验证:**
- [ ] 弹窗打开状态下,选中不同构件 → 内容自动刷新
- [ ] 取消选中 → 显示"请先选中构件"
**Commit**: NO (无代码修改)
---
## Commit Strategy
| After Task | Message | Files | Verification |
|------------|---------|-------|--------------|
| 4 | `refactor(component-detail): 移除 PropertyPanelManager统一使用 ComponentDetailManager` | 6 files | `bun run build` |
| 5 | `fix(component-detail): 修复选中切换时内容不更新的问题` (if needed) | TBD | `bun run build` |
---
## Success Criteria
### Verification Commands
```bash
bun run build # Expected: Build success, no errors
```
### Final Checklist
- [ ] 所有 "Must Have" 功能正常
- [ ] 所有 "Must NOT Have" 未出现
- [ ] 构建通过
- [ ] 5 个原始问题全部解决

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,547 @@
# 右键菜单功能对接
## TL;DR
> **Quick Summary**: 对接右键菜单的构件操作功能,包括隐藏、半透明、隔离、显示全部,以及新增"快速选择"子菜单。
>
> **Deliverables**:
> - 国际化文件更新types.ts, zh-CN.ts, en-US.ts
> - Engine 层新增 8 个方法
> - EngineManager 层新增 8 个代理方法
> - 右键菜单 handler 实现功能调用
>
> **Estimated Effort**: Short (约 30 分钟)
> **Parallel Execution**: NO - sequential
> **Critical Path**: Task 1 → Task 2 → Task 3 → Task 4
---
## Context
### Original Request
对接右键菜单的以下功能:
1. 隐藏选中构件:`engine.modelToolModule.hideModel(engine.engineStatus.highlightModels)`
2. 半透明选中构件:`engine.modelToolModule.translucentModel(engine.engineStatus.highlightModels)`
3. 隔离/隐藏其他构件:`engine.modelToolModule.isolateModel(engine.engineStatus.highlightModels)`
4. 隔离/半透明其他构件:`engine.modelToolModule.translucentOtherModel(engine.engineStatus.highlightModels)`
5. 显示所有模型:`engine.modelToolModule.showAllModels()`
新增快速选择子菜单:
6. 选择同类模型:`engine.modelToolModule.batchSelectSameTypeModel(engine.engineStatus.highlightModels)`
7. 选择同层模型:`engine.modelToolModule.batchSelectSameLevelModel(engine.engineStatus.highlightModels)`
8. 选择同层同类模型:`engine.modelToolModule.batchSelectSameLevelTypeModel(engine.engineStatus.highlightModels)`
### 底层 API 模式
```typescript
// 获取选中构件
const models = engine.engineStatus.highlightModels;
// 调用 modelToolModule 方法
engine.modelToolModule.hideModel(models);
engine.modelToolModule.translucentModel(models);
engine.modelToolModule.isolateModel(models);
engine.modelToolModule.translucentOtherModel(models);
engine.modelToolModule.showAllModels();
engine.modelToolModule.batchSelectSameTypeModel(models);
engine.modelToolModule.batchSelectSameLevelModel(models);
engine.modelToolModule.batchSelectSameLevelTypeModel(models);
```
---
## Work Objectives
### Core Objective
将底层引擎的构件操作 API 对接到右键菜单,实现完整的构件隐藏、半透明、隔离和快速选择功能。
### Concrete Deliverables
- `src/locales/types.ts` - 添加 4 个新字段
- `src/locales/zh-CN.ts` - 添加中文翻译
- `src/locales/en-US.ts` - 添加英文翻译
- `src/components/engine/index.ts` - 添加 8 个方法
- `src/managers/engine-manager.ts` - 添加 8 个代理方法 + 更新右键菜单 handler
### Definition of Done
- [x] 右键选中构件时,所有菜单功能可正常调用
- [x] 快速选择子菜单正确显示三个选项
- [x] 构建成功:`npm run build`
### Must Have
- 所有 8 个底层 API 都要对接
- 国际化支持中英文
- 保持现有代码风格
### Must NOT Have (Guardrails)
- 不要修改底层引擎 API
- 不要添加额外的 UI 组件
- 不要修改菜单的显示逻辑
---
## Verification Strategy
### Test Decision
- **Infrastructure exists**: NO
- **User wants tests**: Manual-only
- **QA approach**: Manual verification
---
## TODOs
- [x] 1. 更新国际化文件
**What to do**:
1.`src/locales/types.ts``menu` 对象中添加:
```typescript
quickSelect: string;
selectSameType: string;
selectSameLevel: string;
selectSameLevelType: string;
```
2. 在 `src/locales/zh-CN.ts` 的 `menu` 对象中添加:
```typescript
quickSelect: '快速选择',
selectSameType: '选择同类模型',
selectSameLevel: '选择同层模型',
selectSameLevelType: '选择同层同类模型'
```
3. 在 `src/locales/en-US.ts` 的 `menu` 对象中添加:
```typescript
quickSelect: 'Quick Select',
selectSameType: 'Select Same Type',
selectSameLevel: 'Select Same Level',
selectSameLevelType: 'Select Same Level & Type'
```
**Must NOT do**:
- 不要修改其他现有字段
**Recommended Agent Profile**:
- **Category**: `quick`
- **Skills**: [`coding-standards`]
**Parallelization**:
- **Can Run In Parallel**: NO
- **Parallel Group**: Sequential
- **Blocks**: Task 2, 3, 4
- **Blocked By**: None
**References**:
- `src/locales/types.ts:53-64` - 现有 menu 类型定义
- `src/locales/zh-CN.ts:34-45` - 现有中文翻译
- `src/locales/en-US.ts:34-45` - 现有英文翻译
**Acceptance Criteria**:
- [ ] TypeScript 无类型错误
- [ ] 三个文件都已更新
**Commit**: NO (与 Task 2-4 一起提交)
---
- [x] 2. Engine 层添加构件操作方法
**What to do**:
在 `src/components/engine/index.ts` 中添加以下 8 个公共方法在路径漫游区域之后、destroy 之前):
```typescript
// ==================== 构件操作 ====================
/**
* 隐藏选中构件
*/
public hideSelectedModels(): void {
if (!this._isInitialized || !this.engine?.modelToolModule) {
console.warn('[Engine] Cannot hide models: engine not initialized.');
return;
}
const models = this.engine.engineStatus?.highlightModels;
if (models) {
this.engine.modelToolModule.hideModel(models);
}
}
/**
* 半透明选中构件
*/
public translucentSelectedModels(): void {
if (!this._isInitialized || !this.engine?.modelToolModule) {
console.warn('[Engine] Cannot translucent models: engine not initialized.');
return;
}
const models = this.engine.engineStatus?.highlightModels;
if (models) {
this.engine.modelToolModule.translucentModel(models);
}
}
/**
* 隔离选中构件(隐藏其他)
*/
public isolateSelectedModels(): void {
if (!this._isInitialized || !this.engine?.modelToolModule) {
console.warn('[Engine] Cannot isolate models: engine not initialized.');
return;
}
const models = this.engine.engineStatus?.highlightModels;
if (models) {
this.engine.modelToolModule.isolateModel(models);
}
}
/**
* 半透明其他构件
*/
public translucentOtherModels(): void {
if (!this._isInitialized || !this.engine?.modelToolModule) {
console.warn('[Engine] Cannot translucent other models: engine not initialized.');
return;
}
const models = this.engine.engineStatus?.highlightModels;
if (models) {
this.engine.modelToolModule.translucentOtherModel(models);
}
}
/**
* 显示所有模型
*/
public showAllModels(): void {
if (!this._isInitialized || !this.engine?.modelToolModule) {
console.warn('[Engine] Cannot show all models: engine not initialized.');
return;
}
this.engine.modelToolModule.showAllModels();
}
/**
* 批量选择同类模型
*/
public batchSelectSameTypeModel(): void {
if (!this._isInitialized || !this.engine?.modelToolModule) {
console.warn('[Engine] Cannot batch select: engine not initialized.');
return;
}
const models = this.engine.engineStatus?.highlightModels;
if (models) {
this.engine.modelToolModule.batchSelectSameTypeModel(models);
}
}
/**
* 批量选择同层模型
*/
public batchSelectSameLevelModel(): void {
if (!this._isInitialized || !this.engine?.modelToolModule) {
console.warn('[Engine] Cannot batch select: engine not initialized.');
return;
}
const models = this.engine.engineStatus?.highlightModels;
if (models) {
this.engine.modelToolModule.batchSelectSameLevelModel(models);
}
}
/**
* 批量选择同层同类模型
*/
public batchSelectSameLevelTypeModel(): void {
if (!this._isInitialized || !this.engine?.modelToolModule) {
console.warn('[Engine] Cannot batch select: engine not initialized.');
return;
}
const models = this.engine.engineStatus?.highlightModels;
if (models) {
this.engine.modelToolModule.batchSelectSameLevelTypeModel(models);
}
}
// ==================== 结束:构件操作 ====================
```
**Must NOT do**:
- 不要修改现有方法
- 不要修改 `fitSectionBoxToModel` 方法
**Recommended Agent Profile**:
- **Category**: `quick`
- **Skills**: [`coding-standards`]
**Parallelization**:
- **Can Run In Parallel**: NO
- **Parallel Group**: Sequential
- **Blocks**: Task 3
- **Blocked By**: Task 1
**References**:
- `src/components/engine/index.ts:496-506` - `fitSectionBoxToModel` 参考模式(使用 highlightModels
- `src/components/engine/index.ts:431-491` - 路径漫游方法区域(在此之后添加)
**Acceptance Criteria**:
- [ ] 8 个方法都已添加
- [ ] 方法有 JSDoc 注释
- [ ] 使用项目规范的区域注释标记
**Commit**: NO (与其他 Task 一起提交)
---
- [x] 3. EngineManager 添加代理方法
**What to do**:
在 `src/managers/engine-manager.ts` 中添加 8 个代理方法在路径漫游区域之后、destroy 之前):
```typescript
// ==================== 构件操作 ====================
/**
* 隐藏选中构件
*/
public hideSelectedModels(): void {
this.engineInstance?.hideSelectedModels();
}
/**
* 半透明选中构件
*/
public translucentSelectedModels(): void {
this.engineInstance?.translucentSelectedModels();
}
/**
* 隔离选中构件(隐藏其他)
*/
public isolateSelectedModels(): void {
this.engineInstance?.isolateSelectedModels();
}
/**
* 半透明其他构件
*/
public translucentOtherModels(): void {
this.engineInstance?.translucentOtherModels();
}
/**
* 显示所有模型
*/
public showAllModels(): void {
this.engineInstance?.showAllModels();
}
/**
* 批量选择同类模型
*/
public batchSelectSameTypeModel(): void {
this.engineInstance?.batchSelectSameTypeModel();
}
/**
* 批量选择同层模型
*/
public batchSelectSameLevelModel(): void {
this.engineInstance?.batchSelectSameLevelModel();
}
/**
* 批量选择同层同类模型
*/
public batchSelectSameLevelTypeModel(): void {
this.engineInstance?.batchSelectSameLevelTypeModel();
}
// ==================== 结束:构件操作 ====================
```
**Must NOT do**:
- 不要修改现有方法
**Recommended Agent Profile**:
- **Category**: `quick`
- **Skills**: [`coding-standards`]
**Parallelization**:
- **Can Run In Parallel**: NO
- **Parallel Group**: Sequential
- **Blocks**: Task 4
- **Blocked By**: Task 2
**References**:
- `src/managers/engine-manager.ts:431-492` - 路径漫游代理方法区域
**Acceptance Criteria**:
- [ ] 8 个代理方法都已添加
- [ ] 方法有 JSDoc 注释
**Commit**: NO (与其他 Task 一起提交)
---
- [x] 4. 更新右键菜单 Handler
**What to do**:
在 `src/managers/engine-manager.ts` 的 `registerHandler` 回调中:
1. **替换隐藏选中构件的 onClick**(约第 78-81 行):
```typescript
onClick: () => {
this.hideSelectedModels();
this.rightKey?.hide();
}
```
2. **替换半透明选中构件的 onClick**(约第 90-93 行):
```typescript
onClick: () => {
this.translucentSelectedModels();
this.rightKey?.hide();
}
```
3. **替换隔离-隐藏其他的 onClick**(约第 107-110 行):
```typescript
onClick: () => {
this.isolateSelectedModels();
this.rightKey?.hide();
}
```
4. **替换隔离-半透明其他的 onClick**(约第 114-117 行):
```typescript
onClick: () => {
this.translucentOtherModels();
this.rightKey?.hide();
}
```
5. **替换显示全部的 onClick**(约第 142-144 行):
```typescript
onClick: () => {
this.showAllModels();
this.rightKey?.hide();
}
```
6. **在隔离选中构件菜单项之后order: 4 之后),添加快速选择子菜单**order: 5将剖切盒适应改为 order: 6显示全部改为 order: 7
```typescript
// 5. 快速选择(带子菜单)
items.push({
id: 'quickSelect',
label: 'menu.quickSelect',
group: 'component',
order: 5,
children: [
{
id: 'selectSameType',
label: 'menu.selectSameType',
onClick: () => {
this.batchSelectSameTypeModel();
this.rightKey?.hide();
}
},
{
id: 'selectSameLevel',
label: 'menu.selectSameLevel',
onClick: () => {
this.batchSelectSameLevelModel();
this.rightKey?.hide();
}
},
{
id: 'selectSameLevelType',
label: 'menu.selectSameLevelType',
onClick: () => {
this.batchSelectSameLevelTypeModel();
this.rightKey?.hide();
}
}
]
});
```
7. **更新剖切盒适应的 order 为 6**
8. **更新显示全部的 order 为 7**
**Must NOT do**:
- 不要修改菜单项的 id 和 label除了新增的
- 不要修改 group 分组逻辑
**Recommended Agent Profile**:
- **Category**: `quick`
- **Skills**: [`coding-standards`]
**Parallelization**:
- **Can Run In Parallel**: NO
- **Parallel Group**: Sequential (final task)
- **Blocks**: None
- **Blocked By**: Task 3
**References**:
- `src/managers/engine-manager.ts:53-149` - 现有右键菜单 handler
- `src/managers/engine-manager.ts:96-121` - 隔离子菜单结构参考
**Acceptance Criteria**:
- [ ] 所有 console.log 占位符已替换为实际方法调用
- [ ] 快速选择子菜单正确添加
- [ ] 菜单项 order 正确排序
**Commit**: YES
- Message: `feat(menu): implement right-click menu functions for model operations`
- Files:
- `src/locales/types.ts`
- `src/locales/zh-CN.ts`
- `src/locales/en-US.ts`
- `src/components/engine/index.ts`
- `src/managers/engine-manager.ts`
- Pre-commit: `npm run build`
---
- [x] 5. 构建验证
**What to do**:
- 运行 `npm run build` 确保构建成功
**Recommended Agent Profile**:
- **Category**: `quick`
- **Skills**: []
**Parallelization**:
- **Can Run In Parallel**: NO
- **Blocks**: None
- **Blocked By**: Task 4
**Acceptance Criteria**:
```bash
npm run build
# 预期: 构建成功,无错误
```
**Commit**: NO
---
## Commit Strategy
| After Task | Message | Files | Verification |
|------------|---------|-------|--------------|
| 4 | `feat(menu): implement right-click menu functions for model operations` | types.ts, zh-CN.ts, en-US.ts, engine/index.ts, engine-manager.ts | npm run build |
---
## Success Criteria
### Verification Commands
```bash
npm run build # Expected: Build successful
```
### Final Checklist
- [x] 国际化文件已更新3 个文件)
- [x] Engine 层添加 8 个方法
- [x] EngineManager 层添加 8 个代理方法
- [x] 右键菜单 handler 已更新
- [x] 快速选择子菜单已添加
- [x] 构建成功

View File

@@ -538,6 +538,19 @@ const dialog = engine.dialog.create({
| `ConstructTreeManagerBtn` | `src/managers/construct-tree-manager-btn.ts` | 目录树按钮/弹窗管理器 | `BimComponent` | | `ConstructTreeManagerBtn` | `src/managers/construct-tree-manager-btn.ts` | 目录树按钮/弹窗管理器 | `BimComponent` |
| `ComponentDetailManager` | `src/managers/component-detail-manager.ts` | 构件详情弹窗管理器 | `BimComponent` | | `ComponentDetailManager` | `src/managers/component-detail-manager.ts` | 构件详情弹窗管理器 | `BimComponent` |
#### 4.1.1 剖切盒SectionBox对接说明
- 相关文件:
- `src/managers/section-box-dialog-manager.ts`:剖切盒弹窗事件/回调对接
- `src/components/section-box-panel/index.ts`:剖切盒 UI 状态管理(隐藏/反向/滑块范围/重置)
- `src/components/engine/index.ts`:底层 3D 引擎能力封装
- `src/managers/engine-manager.ts`:对外暴露的 3D 引擎管理器 APIUI 通过此层调用)
- 关键交互约定:
- “适应”按钮:调用 `EngineManager.scaleSectionBox()`,对接底层 `engine.clipping.scaleBox()`
- “反向”按钮:调用 `EngineManager.reverseSection()`,对接底层 `engine.clipping.reverse()`(该接口为“切换一次”语义)
- “重置”按钮UI 强制恢复到默认状态(滑块 0-100、隐藏/反向按钮均为关闭),并通过 `deactivateSection()` → `activeSection('box')` 实现“关闭剖切再打开”
### 4.2 组件类清单 ### 4.2 组件类清单
| 类名 | 文件路径 | 功能 | 实现接口 | | 类名 | 文件路径 | 功能 | 实现接口 |

BIN
demo/assets/hdr/001.hdr Normal file

Binary file not shown.

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1767704478351" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5751" width="32" height="32" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M341.333333 608L202.666667 469.333333H298.666667c10.666667-239.36 101.973333-426.666667 213.333333-426.666666 85.333333 0 160.853333 112.64 194.133333 275.2C868.693333 351.146667 981.333333 426.666667 981.333333 512c0 78.08-92.586667 146.346667-230.4 183.466667l12.373334-86.613334C844.8 585.386667 896 550.826667 896 512c0-45.226667-70.4-85.333333-176.213333-106.666667 3.413333 33.706667 5.546667 69.546667 5.546666 106.666667 0 259.413333-95.573333 469.333333-213.333333 469.333333-78.08 0-146.346667-92.586667-183.466667-230.4l86.613334 12.373334C438.613333 844.8 473.173333 896 512 896c70.826667 0 128-171.946667 128-384 0-42.666667-2.133333-83.2-6.4-121.6C595.2 386.133333 554.666667 384 512 384l-79.36 2.56 12.373333-85.76L512 298.666667c37.12 0 72.96 2.133333 106.666667 5.546666C597.333333 198.4 557.226667 128 512 128c-65.706667 0-120.32 149.333333-128 341.333333h96L341.333333 608M608 682.666667L469.333333 821.333333V725.333333c-239.36-10.666667-426.666667-101.973333-426.666666-213.333333 0-78.08 92.586667-146.346667 230.4-183.466667l-12.373334 86.613334C179.2 438.613333 128 473.173333 128 512c0 65.706667 149.333333 120.32 341.333333 128v-96L608 682.666667z" p-id="5752" fill="#1afa29"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

739
demo/draco/DRACOLoader.js Normal file
View File

@@ -0,0 +1,739 @@
import {
BufferAttribute,
BufferGeometry,
Color,
ColorManagement,
FileLoader,
Loader,
LinearSRGBColorSpace,
SRGBColorSpace,
InterleavedBuffer,
InterleavedBufferAttribute
} from 'three';
const _taskCache = new WeakMap();
/**
* A loader for the Draco format.
*
* [Draco](https://google.github.io/draco/) is an open source library for compressing
* and decompressing 3D meshes and point clouds. Compressed geometry can be significantly smaller,
* at the cost of additional decoding time on the client device.
*
* Standalone Draco files have a `.drc` extension, and contain vertex positions, normals, colors,
* and other attributes. Draco files do not contain materials, textures, animation, or node hierarchies
* to use these features, embed Draco geometry inside of a glTF file. A normal glTF file can be converted
* to a Draco-compressed glTF file using [glTF-Pipeline](https://github.com/CesiumGS/gltf-pipeline).
* When using Draco with glTF, an instance of `DRACOLoader` will be used internally by {@link GLTFLoader}.
*
* It is recommended to create one DRACOLoader instance and reuse it to avoid loading and creating
* multiple decoder instances.
*
* `DRACOLoader` will automatically use either the JS or the WASM decoding library, based on
* browser capabilities.
*
* ```js
* const loader = new DRACOLoader();
* loader.setDecoderPath( '/examples/jsm/libs/draco/' );
*
* const geometry = await dracoLoader.loadAsync( 'models/draco/bunny.drc' );
* geometry.computeVertexNormals(); // optional
*
* dracoLoader.dispose();
* ```
*
* @augments Loader
* @three_import import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
*/
class DRACOLoader extends Loader {
/**
* Constructs a new Draco loader.
*
* @param {LoadingManager} [manager] - The loading manager.
*/
constructor( manager ) {
super( manager );
this.decoderPath = '';
this.decoderConfig = {};
this.decoderBinary = null;
this.decoderPending = null;
this.workerLimit = 4;
this.workerPool = [];
this.workerNextTaskID = 1;
this.workerSourceURL = '';
this.defaultAttributeIDs = {
position: 'POSITION',
normal: 'NORMAL',
color: 'COLOR',
uv: 'TEX_COORD'
};
this.defaultAttributeTypes = {
position: 'Float32Array',
normal: 'Float32Array',
color: 'Float32Array',
uv: 'Float32Array'
};
}
/**
* Provides configuration for the decoder libraries. Configuration cannot be changed after decoding begins.
*
* @param {string} path - The decoder path.
* @return {DRACOLoader} A reference to this loader.
*/
setDecoderPath( path ) {
this.decoderPath = path;
return this;
}
/**
* Provides configuration for the decoder libraries. Configuration cannot be changed after decoding begins.
*
* @param {{type:('js'|'wasm')}} config - The decoder config.
* @return {DRACOLoader} A reference to this loader.
*/
setDecoderConfig( config ) {
this.decoderConfig = config;
return this;
}
/**
* Sets the maximum number of Web Workers to be used during decoding.
* A lower limit may be preferable if workers are also for other tasks in the application.
*
* @param {number} workerLimit - The worker limit.
* @return {DRACOLoader} A reference to this loader.
*/
setWorkerLimit( workerLimit ) {
this.workerLimit = workerLimit;
return this;
}
/**
* Starts loading from the given URL and passes the loaded Draco asset
* to the `onLoad()` callback.
*
* @param {string} url - The path/URL of the file to be loaded. This can also be a data URI.
* @param {function(BufferGeometry)} onLoad - Executed when the loading process has been finished.
* @param {onProgressCallback} onProgress - Executed while the loading is in progress.
* @param {onErrorCallback} onError - Executed when errors occur.
*/
load( url, onLoad, onProgress, onError ) {
const loader = new FileLoader( this.manager );
loader.setPath( this.path );
loader.setResponseType( 'arraybuffer' );
loader.setRequestHeader( this.requestHeader );
loader.setWithCredentials( this.withCredentials );
loader.load( url, ( buffer ) => {
this.parse( buffer, onLoad, onError );
}, onProgress, onError );
}
/**
* Parses the given Draco data.
*
* @param {ArrayBuffer} buffer - The raw Draco data as an array buffer.
* @param {function(BufferGeometry)} onLoad - Executed when the loading/parsing process has been finished.
* @param {onErrorCallback} onError - Executed when errors occur.
*/
parse( buffer, onLoad, onError = ()=>{} ) {
this.decodeDracoFile( buffer, onLoad, null, null, SRGBColorSpace, onError ).catch( onError );
}
//
decodeDracoFile( buffer, callback, attributeIDs, attributeTypes, vertexColorSpace = LinearSRGBColorSpace, onError = () => {} ) {
const taskConfig = {
attributeIDs: attributeIDs || this.defaultAttributeIDs,
attributeTypes: attributeTypes || this.defaultAttributeTypes,
useUniqueIDs: !! attributeIDs,
vertexColorSpace: vertexColorSpace,
};
return this.decodeGeometry( buffer, taskConfig ).then( callback ).catch( onError );
}
decodeGeometry( buffer, taskConfig ) {
const taskKey = JSON.stringify( taskConfig );
// Check for an existing task using this buffer. A transferred buffer cannot be transferred
// again from this thread.
if ( _taskCache.has( buffer ) ) {
const cachedTask = _taskCache.get( buffer );
if ( cachedTask.key === taskKey ) {
return cachedTask.promise;
} else if ( buffer.byteLength === 0 ) {
// Technically, it would be possible to wait for the previous task to complete,
// transfer the buffer back, and decode again with the second configuration. That
// is complex, and I don't know of any reason to decode a Draco buffer twice in
// different ways, so this is left unimplemented.
throw new Error(
'THREE.DRACOLoader: Unable to re-decode a buffer with different ' +
'settings. Buffer has already been transferred.'
);
}
}
//
let worker;
const taskID = this.workerNextTaskID ++;
const taskCost = buffer.byteLength;
// Obtain a worker and assign a task, and construct a geometry instance
// when the task completes.
const geometryPending = this._getWorker( taskID, taskCost )
.then( ( _worker ) => {
worker = _worker;
return new Promise( ( resolve, reject ) => {
worker._callbacks[ taskID ] = { resolve, reject };
worker.postMessage( { type: 'decode', id: taskID, taskConfig, buffer }, [ buffer ] );
// this.debug();
} );
} )
.then( ( message ) => this._createGeometry( message.geometry ) );
// Remove task from the task list.
// Note: replaced '.finally()' with '.catch().then()' block - iOS 11 support (#19416)
geometryPending
.catch( () => true )
.then( () => {
if ( worker && taskID ) {
this._releaseTask( worker, taskID );
// this.debug();
}
} );
// Cache the task result.
_taskCache.set( buffer, {
key: taskKey,
promise: geometryPending
} );
return geometryPending;
}
_createGeometry( geometryData ) {
const geometry = new BufferGeometry();
if ( geometryData.index ) {
geometry.setIndex( new BufferAttribute( geometryData.index.array, 1 ) );
}
for ( let i = 0; i < geometryData.attributes.length; i ++ ) {
const { name, array, itemSize, stride, vertexColorSpace } = geometryData.attributes[ i ];
let attribute;
if ( itemSize === stride ) {
attribute = new BufferAttribute( array, itemSize );
} else {
const buffer = new InterleavedBuffer( array, stride );
attribute = new InterleavedBufferAttribute( buffer, itemSize, 0 );
}
if ( name === 'color' ) {
this._assignVertexColorSpace( attribute, vertexColorSpace );
attribute.normalized = ( array instanceof Float32Array ) === false;
}
geometry.setAttribute( name, attribute );
}
return geometry;
}
_assignVertexColorSpace( attribute, inputColorSpace ) {
// While .drc files do not specify colorspace, the only 'official' tooling
// is PLY and OBJ converters, which use sRGB. We'll assume sRGB when a .drc
// file is passed into .load() or .parse(). GLTFLoader uses internal APIs
// to decode geometry, and vertex colors are already Linear-sRGB in there.
if ( inputColorSpace !== SRGBColorSpace ) return;
const _color = new Color();
for ( let i = 0, il = attribute.count; i < il; i ++ ) {
_color.fromBufferAttribute( attribute, i );
ColorManagement.colorSpaceToWorking( _color, SRGBColorSpace );
attribute.setXYZ( i, _color.r, _color.g, _color.b );
}
}
_loadLibrary( url, responseType ) {
const loader = new FileLoader( this.manager );
loader.setPath( this.decoderPath );
loader.setResponseType( responseType );
loader.setWithCredentials( this.withCredentials );
return new Promise( ( resolve, reject ) => {
loader.load( url, resolve, undefined, reject );
} );
}
preload() {
this._initDecoder();
return this;
}
_initDecoder() {
if ( this.decoderPending ) return this.decoderPending;
const useJS = typeof WebAssembly !== 'object' || this.decoderConfig.type === 'js';
const librariesPending = [];
if ( useJS ) {
librariesPending.push( this._loadLibrary( 'draco_decoder.js', 'text' ) );
} else {
librariesPending.push( this._loadLibrary( 'draco_wasm_wrapper.js', 'text' ) );
librariesPending.push( this._loadLibrary( 'draco_decoder.wasm', 'arraybuffer' ) );
}
this.decoderPending = Promise.all( librariesPending )
.then( ( libraries ) => {
const jsContent = libraries[ 0 ];
if ( ! useJS ) {
this.decoderConfig.wasmBinary = libraries[ 1 ];
}
const fn = DRACOWorker.toString();
const body = [
'/* draco decoder */',
jsContent,
'',
'/* worker */',
fn.substring( fn.indexOf( '{' ) + 1, fn.lastIndexOf( '}' ) )
].join( '\n' );
this.workerSourceURL = URL.createObjectURL( new Blob( [ body ] ) );
} );
return this.decoderPending;
}
_getWorker( taskID, taskCost ) {
return this._initDecoder().then( () => {
if ( this.workerPool.length < this.workerLimit ) {
const worker = new Worker( this.workerSourceURL );
worker._callbacks = {};
worker._taskCosts = {};
worker._taskLoad = 0;
worker.postMessage( { type: 'init', decoderConfig: this.decoderConfig } );
worker.onmessage = function ( e ) {
const message = e.data;
switch ( message.type ) {
case 'decode':
worker._callbacks[ message.id ].resolve( message );
break;
case 'error':
worker._callbacks[ message.id ].reject( message );
break;
default:
console.error( 'THREE.DRACOLoader: Unexpected message, "' + message.type + '"' );
}
};
this.workerPool.push( worker );
} else {
this.workerPool.sort( function ( a, b ) {
return a._taskLoad > b._taskLoad ? - 1 : 1;
} );
}
const worker = this.workerPool[ this.workerPool.length - 1 ];
worker._taskCosts[ taskID ] = taskCost;
worker._taskLoad += taskCost;
return worker;
} );
}
_releaseTask( worker, taskID ) {
worker._taskLoad -= worker._taskCosts[ taskID ];
delete worker._callbacks[ taskID ];
delete worker._taskCosts[ taskID ];
}
debug() {
console.log( 'Task load: ', this.workerPool.map( ( worker ) => worker._taskLoad ) );
}
dispose() {
for ( let i = 0; i < this.workerPool.length; ++ i ) {
this.workerPool[ i ].terminate();
}
this.workerPool.length = 0;
if ( this.workerSourceURL !== '' ) {
URL.revokeObjectURL( this.workerSourceURL );
}
return this;
}
}
/* WEB WORKER */
function DRACOWorker() {
let decoderConfig;
let decoderPending;
onmessage = function ( e ) {
const message = e.data;
switch ( message.type ) {
case 'init':
decoderConfig = message.decoderConfig;
decoderPending = new Promise( function ( resolve/*, reject*/ ) {
decoderConfig.onModuleLoaded = function ( draco ) {
// Module is Promise-like. Wrap before resolving to avoid loop.
resolve( { draco: draco } );
};
DracoDecoderModule( decoderConfig ); // eslint-disable-line no-undef
} );
break;
case 'decode':
const buffer = message.buffer;
const taskConfig = message.taskConfig;
decoderPending.then( ( module ) => {
const draco = module.draco;
const decoder = new draco.Decoder();
try {
const geometry = decodeGeometry( draco, decoder, new Int8Array( buffer ), taskConfig );
const buffers = geometry.attributes.map( ( attr ) => attr.array.buffer );
if ( geometry.index ) buffers.push( geometry.index.array.buffer );
self.postMessage( { type: 'decode', id: message.id, geometry }, buffers );
} catch ( error ) {
console.error( error );
self.postMessage( { type: 'error', id: message.id, error: error.message } );
} finally {
draco.destroy( decoder );
}
} );
break;
}
};
function decodeGeometry( draco, decoder, array, taskConfig ) {
const attributeIDs = taskConfig.attributeIDs;
const attributeTypes = taskConfig.attributeTypes;
let dracoGeometry;
let decodingStatus;
const geometryType = decoder.GetEncodedGeometryType( array );
if ( geometryType === draco.TRIANGULAR_MESH ) {
dracoGeometry = new draco.Mesh();
decodingStatus = decoder.DecodeArrayToMesh( array, array.byteLength, dracoGeometry );
} else if ( geometryType === draco.POINT_CLOUD ) {
dracoGeometry = new draco.PointCloud();
decodingStatus = decoder.DecodeArrayToPointCloud( array, array.byteLength, dracoGeometry );
} else {
throw new Error( 'THREE.DRACOLoader: Unexpected geometry type.' );
}
if ( ! decodingStatus.ok() || dracoGeometry.ptr === 0 ) {
throw new Error( 'THREE.DRACOLoader: Decoding failed: ' + decodingStatus.error_msg() );
}
const geometry = { index: null, attributes: [] };
// Gather all vertex attributes.
for ( const attributeName in attributeIDs ) {
const attributeType = self[ attributeTypes[ attributeName ] ];
let attribute;
let attributeID;
// A Draco file may be created with default vertex attributes, whose attribute IDs
// are mapped 1:1 from their semantic name (POSITION, NORMAL, ...). Alternatively,
// a Draco file may contain a custom set of attributes, identified by known unique
// IDs. glTF files always do the latter, and `.drc` files typically do the former.
if ( taskConfig.useUniqueIDs ) {
attributeID = attributeIDs[ attributeName ];
attribute = decoder.GetAttributeByUniqueId( dracoGeometry, attributeID );
} else {
attributeID = decoder.GetAttributeId( dracoGeometry, draco[ attributeIDs[ attributeName ] ] );
if ( attributeID === - 1 ) continue;
attribute = decoder.GetAttribute( dracoGeometry, attributeID );
}
const attributeResult = decodeAttribute( draco, decoder, dracoGeometry, attributeName, attributeType, attribute );
if ( attributeName === 'color' ) {
attributeResult.vertexColorSpace = taskConfig.vertexColorSpace;
}
geometry.attributes.push( attributeResult );
}
// Add index.
if ( geometryType === draco.TRIANGULAR_MESH ) {
geometry.index = decodeIndex( draco, decoder, dracoGeometry );
}
draco.destroy( dracoGeometry );
return geometry;
}
function decodeIndex( draco, decoder, dracoGeometry ) {
const numFaces = dracoGeometry.num_faces();
const numIndices = numFaces * 3;
const byteLength = numIndices * 4;
const ptr = draco._malloc( byteLength );
decoder.GetTrianglesUInt32Array( dracoGeometry, byteLength, ptr );
const index = new Uint32Array( draco.HEAPF32.buffer, ptr, numIndices ).slice();
draco._free( ptr );
return { array: index, itemSize: 1 };
}
function decodeAttribute( draco, decoder, dracoGeometry, attributeName, TypedArray, attribute ) {
const count = dracoGeometry.num_points();
const itemSize = attribute.num_components();
const dracoDataType = getDracoDataType( draco, TypedArray );
// Reference: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#data-alignment
const srcByteStride = itemSize * TypedArray.BYTES_PER_ELEMENT;
const dstByteStride = Math.ceil( srcByteStride / 4 ) * 4;
const dstStride = dstByteStride / TypedArray.BYTES_PER_ELEMENT
const srcByteLength = count * srcByteStride;
const dstByteLength = count * dstByteStride;
const ptr = draco._malloc( srcByteLength );
decoder.GetAttributeDataArrayForAllPoints( dracoGeometry, attribute, dracoDataType, srcByteLength, ptr );
const srcArray = new TypedArray( draco.HEAPF32.buffer, ptr, srcByteLength / TypedArray.BYTES_PER_ELEMENT );
let dstArray;
if ( srcByteStride === dstByteStride ) {
// THREE.BufferAttribute
dstArray = srcArray.slice();
} else {
// THREE.InterleavedBufferAttribute
dstArray = new TypedArray( dstByteLength / TypedArray.BYTES_PER_ELEMENT );
let dstOffset = 0
for ( let i = 0, il = srcArray.length; i < il; i++ ) {
for ( let j = 0; j < itemSize; j++ ) {
dstArray[ dstOffset + j ] = srcArray[ i * itemSize + j ]
}
dstOffset += dstStride;
}
}
draco._free( ptr );
return {
name: attributeName,
count: count,
itemSize: itemSize,
array: dstArray,
stride: dstStride
};
}
function getDracoDataType( draco, TypedArray ) {
switch ( TypedArray ) {
case Float32Array: return draco.DT_FLOAT32;
case Int8Array: return draco.DT_INT8;
case Int16Array: return draco.DT_INT16;
case Int32Array: return draco.DT_INT32;
case Uint8Array: return draco.DT_UINT8;
case Uint16Array: return draco.DT_UINT16;
case Uint32Array: return draco.DT_UINT32;
}
}
}
export { DRACOLoader };

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,104 @@
var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.arrayIteratorImpl=function(f){var m=0;return function(){return m<f.length?{done:!1,value:f[m++]}:{done:!0}}};$jscomp.arrayIterator=function(f){return{next:$jscomp.arrayIteratorImpl(f)}};$jscomp.makeIterator=function(f){var m="undefined"!=typeof Symbol&&Symbol.iterator&&f[Symbol.iterator];return m?m.call(f):$jscomp.arrayIterator(f)};
$jscomp.getGlobal=function(f){return"undefined"!=typeof window&&window===f?f:"undefined"!=typeof global&&null!=global?global:f};$jscomp.global=$jscomp.getGlobal(this);$jscomp.ASSUME_ES5=!1;$jscomp.ASSUME_NO_NATIVE_MAP=!1;$jscomp.ASSUME_NO_NATIVE_SET=!1;$jscomp.SIMPLE_FROUND_POLYFILL=!1;$jscomp.defineProperty=$jscomp.ASSUME_ES5||"function"==typeof Object.defineProperties?Object.defineProperty:function(f,m,v){f!=Array.prototype&&f!=Object.prototype&&(f[m]=v.value)};
$jscomp.polyfill=function(f,m,v,t){if(m){v=$jscomp.global;f=f.split(".");for(t=0;t<f.length-1;t++){var h=f[t];h in v||(v[h]={});v=v[h]}f=f[f.length-1];t=v[f];m=m(t);m!=t&&null!=m&&$jscomp.defineProperty(v,f,{configurable:!0,writable:!0,value:m})}};$jscomp.FORCE_POLYFILL_PROMISE=!1;
$jscomp.polyfill("Promise",function(f){function m(){this.batch_=null}function v(e){return e instanceof h?e:new h(function(l,f){l(e)})}if(f&&!$jscomp.FORCE_POLYFILL_PROMISE)return f;m.prototype.asyncExecute=function(e){if(null==this.batch_){this.batch_=[];var l=this;this.asyncExecuteFunction(function(){l.executeBatch_()})}this.batch_.push(e)};var t=$jscomp.global.setTimeout;m.prototype.asyncExecuteFunction=function(e){t(e,0)};m.prototype.executeBatch_=function(){for(;this.batch_&&this.batch_.length;){var e=
this.batch_;this.batch_=[];for(var l=0;l<e.length;++l){var f=e[l];e[l]=null;try{f()}catch(z){this.asyncThrow_(z)}}}this.batch_=null};m.prototype.asyncThrow_=function(e){this.asyncExecuteFunction(function(){throw e;})};var h=function(e){this.state_=0;this.result_=void 0;this.onSettledCallbacks_=[];var l=this.createResolveAndReject_();try{e(l.resolve,l.reject)}catch(S){l.reject(S)}};h.prototype.createResolveAndReject_=function(){function e(e){return function(h){f||(f=!0,e.call(l,h))}}var l=this,f=!1;
return{resolve:e(this.resolveTo_),reject:e(this.reject_)}};h.prototype.resolveTo_=function(e){if(e===this)this.reject_(new TypeError("A Promise cannot resolve to itself"));else if(e instanceof h)this.settleSameAsPromise_(e);else{a:switch(typeof e){case "object":var l=null!=e;break a;case "function":l=!0;break a;default:l=!1}l?this.resolveToNonPromiseObj_(e):this.fulfill_(e)}};h.prototype.resolveToNonPromiseObj_=function(e){var l=void 0;try{l=e.then}catch(S){this.reject_(S);return}"function"==typeof l?
this.settleSameAsThenable_(l,e):this.fulfill_(e)};h.prototype.reject_=function(e){this.settle_(2,e)};h.prototype.fulfill_=function(e){this.settle_(1,e)};h.prototype.settle_=function(e,l){if(0!=this.state_)throw Error("Cannot settle("+e+", "+l+"): Promise already settled in state"+this.state_);this.state_=e;this.result_=l;this.executeOnSettledCallbacks_()};h.prototype.executeOnSettledCallbacks_=function(){if(null!=this.onSettledCallbacks_){for(var e=0;e<this.onSettledCallbacks_.length;++e)X.asyncExecute(this.onSettledCallbacks_[e]);
this.onSettledCallbacks_=null}};var X=new m;h.prototype.settleSameAsPromise_=function(e){var l=this.createResolveAndReject_();e.callWhenSettled_(l.resolve,l.reject)};h.prototype.settleSameAsThenable_=function(e,l){var f=this.createResolveAndReject_();try{e.call(l,f.resolve,f.reject)}catch(z){f.reject(z)}};h.prototype.then=function(e,f){function l(e,f){return"function"==typeof e?function(f){try{m(e(f))}catch(p){v(p)}}:f}var m,v,t=new h(function(e,f){m=e;v=f});this.callWhenSettled_(l(e,m),l(f,v));return t};
h.prototype.catch=function(e){return this.then(void 0,e)};h.prototype.callWhenSettled_=function(e,f){function l(){switch(h.state_){case 1:e(h.result_);break;case 2:f(h.result_);break;default:throw Error("Unexpected state: "+h.state_);}}var h=this;null==this.onSettledCallbacks_?X.asyncExecute(l):this.onSettledCallbacks_.push(l)};h.resolve=v;h.reject=function(e){return new h(function(f,h){h(e)})};h.race=function(e){return new h(function(f,h){for(var l=$jscomp.makeIterator(e),m=l.next();!m.done;m=l.next())v(m.value).callWhenSettled_(f,
h)})};h.all=function(e){var f=$jscomp.makeIterator(e),m=f.next();return m.done?v([]):new h(function(e,h){function l(f){return function(h){t[f]=h;z--;0==z&&e(t)}}var t=[],z=0;do t.push(void 0),z++,v(m.value).callWhenSettled_(l(t.length-1),h),m=f.next();while(!m.done)})};return h},"es6","es3");
var DracoDecoderModule=function(){var f="undefined"!==typeof document&&document.currentScript?document.currentScript.src:void 0;"undefined"!==typeof __filename&&(f=f||__filename);return function(m){function v(k){return a.locateFile?a.locateFile(k,M):M+k}function t(a,c){a||z("Assertion failed: "+c)}function h(a,c,b){var d=c+b;for(b=c;a[b]&&!(b>=d);)++b;if(16<b-c&&a.subarray&&xa)return xa.decode(a.subarray(c,b));for(d="";c<b;){var k=a[c++];if(k&128){var e=a[c++]&63;if(192==(k&224))d+=String.fromCharCode((k&
31)<<6|e);else{var f=a[c++]&63;k=224==(k&240)?(k&15)<<12|e<<6|f:(k&7)<<18|e<<12|f<<6|a[c++]&63;65536>k?d+=String.fromCharCode(k):(k-=65536,d+=String.fromCharCode(55296|k>>10,56320|k&1023))}}else d+=String.fromCharCode(k)}return d}function X(a,c){return a?h(ca,a,c):""}function e(a,c){0<a%c&&(a+=c-a%c);return a}function l(k){ka=k;a.HEAP8=T=new Int8Array(k);a.HEAP16=new Int16Array(k);a.HEAP32=P=new Int32Array(k);a.HEAPU8=ca=new Uint8Array(k);a.HEAPU16=new Uint16Array(k);a.HEAPU32=new Uint32Array(k);
a.HEAPF32=new Float32Array(k);a.HEAPF64=new Float64Array(k)}function S(k){for(;0<k.length;){var c=k.shift();if("function"==typeof c)c();else{var b=c.func;"number"===typeof b?void 0===c.arg?a.dynCall_v(b):a.dynCall_vi(b,c.arg):b(void 0===c.arg?null:c.arg)}}}function z(k){if(a.onAbort)a.onAbort(k);k+="";ya(k);Y(k);za=!0;throw new WebAssembly.RuntimeError("abort("+k+"). Build with -s ASSERTIONS=1 for more info.");}function va(a){return String.prototype.startsWith?a.startsWith("data:application/octet-stream;base64,"):
0===a.indexOf("data:application/octet-stream;base64,")}function wa(){try{if(da)return new Uint8Array(da);if(la)return la(U);throw"both async and sync fetching of the wasm failed";}catch(k){z(k)}}function Ma(){return da||!ea&&!Z||"function"!==typeof fetch?new Promise(function(a,c){a(wa())}):fetch(U,{credentials:"same-origin"}).then(function(a){if(!a.ok)throw"failed to load wasm binary file at '"+U+"'";return a.arrayBuffer()}).catch(function(){return wa()})}function ba(){if(!ba.strings){var a={USER:"web_user",
LOGNAME:"web_user",PATH:"/",PWD:"/",HOME:"/home/web_user",LANG:("object"===typeof navigator&&navigator.languages&&navigator.languages[0]||"C").replace("-","_")+".UTF-8",_:na},c;for(c in Aa)a[c]=Aa[c];var b=[];for(c in a)b.push(c+"="+a[c]);ba.strings=b}return ba.strings}function ma(k){function c(){if(!fa&&(fa=!0,!za)){Ba=!0;S(Ca);S(Da);if(a.onRuntimeInitialized)a.onRuntimeInitialized();if(a.postRun)for("function"==typeof a.postRun&&(a.postRun=[a.postRun]);a.postRun.length;)Ea.unshift(a.postRun.shift());
S(Ea)}}if(!(0<aa)){if(a.preRun)for("function"==typeof a.preRun&&(a.preRun=[a.preRun]);a.preRun.length;)Fa.unshift(a.preRun.shift());S(Fa);0<aa||(a.setStatus?(a.setStatus("Running..."),setTimeout(function(){setTimeout(function(){a.setStatus("")},1);c()},1)):c())}}function p(){}function u(a){return(a||p).__cache__}function N(a,c){var b=u(c),d=b[a];if(d)return d;d=Object.create((c||p).prototype);d.ptr=a;return b[a]=d}function V(a){if("string"===typeof a){for(var c=0,b=0;b<a.length;++b){var d=a.charCodeAt(b);
55296<=d&&57343>=d&&(d=65536+((d&1023)<<10)|a.charCodeAt(++b)&1023);127>=d?++c:c=2047>=d?c+2:65535>=d?c+3:c+4}c=Array(c+1);b=0;d=c.length;if(0<d){d=b+d-1;for(var k=0;k<a.length;++k){var e=a.charCodeAt(k);if(55296<=e&&57343>=e){var f=a.charCodeAt(++k);e=65536+((e&1023)<<10)|f&1023}if(127>=e){if(b>=d)break;c[b++]=e}else{if(2047>=e){if(b+1>=d)break;c[b++]=192|e>>6}else{if(65535>=e){if(b+2>=d)break;c[b++]=224|e>>12}else{if(b+3>=d)break;c[b++]=240|e>>18;c[b++]=128|e>>12&63}c[b++]=128|e>>6&63}c[b++]=128|
e&63}}c[b]=0}a=n.alloc(c,T);n.copy(c,T,a)}return a}function x(){throw"cannot construct a Status, no constructor in IDL";}function A(){this.ptr=Oa();u(A)[this.ptr]=this}function B(){this.ptr=Pa();u(B)[this.ptr]=this}function C(){this.ptr=Qa();u(C)[this.ptr]=this}function D(){this.ptr=Ra();u(D)[this.ptr]=this}function E(){this.ptr=Sa();u(E)[this.ptr]=this}function q(){this.ptr=Ta();u(q)[this.ptr]=this}function J(){this.ptr=Ua();u(J)[this.ptr]=this}function w(){this.ptr=Va();u(w)[this.ptr]=this}function F(){this.ptr=
Wa();u(F)[this.ptr]=this}function r(){this.ptr=Xa();u(r)[this.ptr]=this}function G(){this.ptr=Ya();u(G)[this.ptr]=this}function H(){this.ptr=Za();u(H)[this.ptr]=this}function O(){this.ptr=$a();u(O)[this.ptr]=this}function K(){this.ptr=ab();u(K)[this.ptr]=this}function g(){this.ptr=bb();u(g)[this.ptr]=this}function y(){this.ptr=cb();u(y)[this.ptr]=this}function Q(){throw"cannot construct a VoidPtr, no constructor in IDL";}function I(){this.ptr=db();u(I)[this.ptr]=this}function L(){this.ptr=eb();u(L)[this.ptr]=
this}m=m||{};var a="undefined"!==typeof m?m:{},Ga=!1,Ha=!1;a.onRuntimeInitialized=function(){Ga=!0;if(Ha&&"function"===typeof a.onModuleLoaded)a.onModuleLoaded(a)};a.onModuleParsed=function(){Ha=!0;if(Ga&&"function"===typeof a.onModuleLoaded)a.onModuleLoaded(a)};a.isVersionSupported=function(a){if("string"!==typeof a)return!1;a=a.split(".");return 2>a.length||3<a.length?!1:1==a[0]&&0<=a[1]&&3>=a[1]?!0:0!=a[0]||10<a[1]?!1:!0};var ha={},W;for(W in a)a.hasOwnProperty(W)&&(ha[W]=a[W]);var na="./this.program",
ea=!1,Z=!1,oa=!1,fb=!1,Ia=!1;ea="object"===typeof window;Z="function"===typeof importScripts;oa=(fb="object"===typeof process&&"object"===typeof process.versions&&"string"===typeof process.versions.node)&&!ea&&!Z;Ia=!ea&&!oa&&!Z;var M="",pa,qa;if(oa){M=__dirname+"/";var ra=function(a,c){pa||(pa=require("fs"));qa||(qa=require("path"));a=qa.normalize(a);return pa.readFileSync(a,c?null:"utf8")};var la=function(a){a=ra(a,!0);a.buffer||(a=new Uint8Array(a));t(a.buffer);return a};1<process.argv.length&&
(na=process.argv[1].replace(/\\/g,"/"));process.argv.slice(2);process.on("uncaughtException",function(a){throw a;});process.on("unhandledRejection",z);a.inspect=function(){return"[Emscripten Module object]"}}else if(Ia)"undefined"!=typeof read&&(ra=function(a){return read(a)}),la=function(a){if("function"===typeof readbuffer)return new Uint8Array(readbuffer(a));a=read(a,"binary");t("object"===typeof a);return a},"undefined"!==typeof print&&("undefined"===typeof console&&(console={}),console.log=print,
console.warn=console.error="undefined"!==typeof printErr?printErr:print);else if(ea||Z)Z?M=self.location.href:document.currentScript&&(M=document.currentScript.src),f&&(M=f),M=0!==M.indexOf("blob:")?M.substr(0,M.lastIndexOf("/")+1):"",ra=function(a){var c=new XMLHttpRequest;c.open("GET",a,!1);c.send(null);return c.responseText},Z&&(la=function(a){var c=new XMLHttpRequest;c.open("GET",a,!1);c.responseType="arraybuffer";c.send(null);return new Uint8Array(c.response)});var ya=a.print||console.log.bind(console),
Y=a.printErr||console.warn.bind(console);for(W in ha)ha.hasOwnProperty(W)&&(a[W]=ha[W]);ha=null;a.thisProgram&&(na=a.thisProgram);var da;a.wasmBinary&&(da=a.wasmBinary);"object"!==typeof WebAssembly&&Y("no native wasm support detected");var ia,gb=new WebAssembly.Table({initial:381,maximum:381,element:"anyfunc"}),za=!1,xa="undefined"!==typeof TextDecoder?new TextDecoder("utf8"):void 0;"undefined"!==typeof TextDecoder&&new TextDecoder("utf-16le");var T,ca,P,Ja=a.TOTAL_MEMORY||16777216;if(ia=a.wasmMemory?
a.wasmMemory:new WebAssembly.Memory({initial:Ja/65536}))var ka=ia.buffer;Ja=ka.byteLength;l(ka);P[4604]=5261456;var Fa=[],Ca=[],Da=[],Ea=[],Ba=!1,aa=0,sa=null,ja=null;a.preloadedImages={};a.preloadedAudios={};var U="draco_decoder.wasm";va(U)||(U=v(U));Ca.push({func:function(){hb()}});var Aa={},R={buffers:[null,[],[]],printChar:function(a,c){var b=R.buffers[a];0===c||10===c?((1===a?ya:Y)(h(b,0)),b.length=0):b.push(c)},varargs:0,get:function(a){R.varargs+=4;return P[R.varargs-4>>2]},getStr:function(){return X(R.get())},
get64:function(){var a=R.get();R.get();return a},getZero:function(){R.get()}},Ka={__cxa_allocate_exception:function(a){return ib(a)},__cxa_throw:function(a,c,b){"uncaught_exception"in ta?ta.uncaught_exceptions++:ta.uncaught_exceptions=1;throw a;},abort:function(){z()},emscripten_get_sbrk_ptr:function(){return 18416},emscripten_memcpy_big:function(a,c,b){ca.set(ca.subarray(c,c+b),a)},emscripten_resize_heap:function(a){if(2147418112<a)return!1;for(var c=Math.max(T.length,16777216);c<a;)c=536870912>=
c?e(2*c,65536):Math.min(e((3*c+2147483648)/4,65536),2147418112);a:{try{ia.grow(c-ka.byteLength+65535>>16);l(ia.buffer);var b=1;break a}catch(d){}b=void 0}return b?!0:!1},environ_get:function(a,c){var b=0;ba().forEach(function(d,e){var f=c+b;e=P[a+4*e>>2]=f;for(f=0;f<d.length;++f)T[e++>>0]=d.charCodeAt(f);T[e>>0]=0;b+=d.length+1});return 0},environ_sizes_get:function(a,c){var b=ba();P[a>>2]=b.length;var d=0;b.forEach(function(a){d+=a.length+1});P[c>>2]=d;return 0},fd_close:function(a){return 0},fd_seek:function(a,
c,b,d,e){return 0},fd_write:function(a,c,b,d){try{for(var e=0,f=0;f<b;f++){for(var g=P[c+8*f>>2],k=P[c+(8*f+4)>>2],h=0;h<k;h++)R.printChar(a,ca[g+h]);e+=k}P[d>>2]=e;return 0}catch(ua){return"undefined"!==typeof FS&&ua instanceof FS.ErrnoError||z(ua),ua.errno}},memory:ia,setTempRet0:function(a){},table:gb},La=function(){function e(c,b){a.asm=c.exports;aa--;a.monitorRunDependencies&&a.monitorRunDependencies(aa);0==aa&&(null!==sa&&(clearInterval(sa),sa=null),ja&&(c=ja,ja=null,c()))}function c(a){e(a.instance)}
function b(a){return Ma().then(function(a){return WebAssembly.instantiate(a,d)}).then(a,function(a){Y("failed to asynchronously prepare wasm: "+a);z(a)})}var d={env:Ka,wasi_unstable:Ka};aa++;a.monitorRunDependencies&&a.monitorRunDependencies(aa);if(a.instantiateWasm)try{return a.instantiateWasm(d,e)}catch(Na){return Y("Module.instantiateWasm callback failed with error: "+Na),!1}(function(){if(da||"function"!==typeof WebAssembly.instantiateStreaming||va(U)||"function"!==typeof fetch)return b(c);fetch(U,
{credentials:"same-origin"}).then(function(a){return WebAssembly.instantiateStreaming(a,d).then(c,function(a){Y("wasm streaming compile failed: "+a);Y("falling back to ArrayBuffer instantiation");b(c)})})})();return{}}();a.asm=La;var hb=a.___wasm_call_ctors=function(){return a.asm.__wasm_call_ctors.apply(null,arguments)},jb=a._emscripten_bind_Status_code_0=function(){return a.asm.emscripten_bind_Status_code_0.apply(null,arguments)},kb=a._emscripten_bind_Status_ok_0=function(){return a.asm.emscripten_bind_Status_ok_0.apply(null,
arguments)},lb=a._emscripten_bind_Status_error_msg_0=function(){return a.asm.emscripten_bind_Status_error_msg_0.apply(null,arguments)},mb=a._emscripten_bind_Status___destroy___0=function(){return a.asm.emscripten_bind_Status___destroy___0.apply(null,arguments)},Oa=a._emscripten_bind_DracoUInt16Array_DracoUInt16Array_0=function(){return a.asm.emscripten_bind_DracoUInt16Array_DracoUInt16Array_0.apply(null,arguments)},nb=a._emscripten_bind_DracoUInt16Array_GetValue_1=function(){return a.asm.emscripten_bind_DracoUInt16Array_GetValue_1.apply(null,
arguments)},ob=a._emscripten_bind_DracoUInt16Array_size_0=function(){return a.asm.emscripten_bind_DracoUInt16Array_size_0.apply(null,arguments)},pb=a._emscripten_bind_DracoUInt16Array___destroy___0=function(){return a.asm.emscripten_bind_DracoUInt16Array___destroy___0.apply(null,arguments)},Pa=a._emscripten_bind_PointCloud_PointCloud_0=function(){return a.asm.emscripten_bind_PointCloud_PointCloud_0.apply(null,arguments)},qb=a._emscripten_bind_PointCloud_num_attributes_0=function(){return a.asm.emscripten_bind_PointCloud_num_attributes_0.apply(null,
arguments)},rb=a._emscripten_bind_PointCloud_num_points_0=function(){return a.asm.emscripten_bind_PointCloud_num_points_0.apply(null,arguments)},sb=a._emscripten_bind_PointCloud___destroy___0=function(){return a.asm.emscripten_bind_PointCloud___destroy___0.apply(null,arguments)},Qa=a._emscripten_bind_DracoUInt8Array_DracoUInt8Array_0=function(){return a.asm.emscripten_bind_DracoUInt8Array_DracoUInt8Array_0.apply(null,arguments)},tb=a._emscripten_bind_DracoUInt8Array_GetValue_1=function(){return a.asm.emscripten_bind_DracoUInt8Array_GetValue_1.apply(null,
arguments)},ub=a._emscripten_bind_DracoUInt8Array_size_0=function(){return a.asm.emscripten_bind_DracoUInt8Array_size_0.apply(null,arguments)},vb=a._emscripten_bind_DracoUInt8Array___destroy___0=function(){return a.asm.emscripten_bind_DracoUInt8Array___destroy___0.apply(null,arguments)},Ra=a._emscripten_bind_DracoUInt32Array_DracoUInt32Array_0=function(){return a.asm.emscripten_bind_DracoUInt32Array_DracoUInt32Array_0.apply(null,arguments)},wb=a._emscripten_bind_DracoUInt32Array_GetValue_1=function(){return a.asm.emscripten_bind_DracoUInt32Array_GetValue_1.apply(null,
arguments)},xb=a._emscripten_bind_DracoUInt32Array_size_0=function(){return a.asm.emscripten_bind_DracoUInt32Array_size_0.apply(null,arguments)},yb=a._emscripten_bind_DracoUInt32Array___destroy___0=function(){return a.asm.emscripten_bind_DracoUInt32Array___destroy___0.apply(null,arguments)},Sa=a._emscripten_bind_AttributeOctahedronTransform_AttributeOctahedronTransform_0=function(){return a.asm.emscripten_bind_AttributeOctahedronTransform_AttributeOctahedronTransform_0.apply(null,arguments)},zb=a._emscripten_bind_AttributeOctahedronTransform_InitFromAttribute_1=
function(){return a.asm.emscripten_bind_AttributeOctahedronTransform_InitFromAttribute_1.apply(null,arguments)},Ab=a._emscripten_bind_AttributeOctahedronTransform_quantization_bits_0=function(){return a.asm.emscripten_bind_AttributeOctahedronTransform_quantization_bits_0.apply(null,arguments)},Bb=a._emscripten_bind_AttributeOctahedronTransform___destroy___0=function(){return a.asm.emscripten_bind_AttributeOctahedronTransform___destroy___0.apply(null,arguments)},Ta=a._emscripten_bind_PointAttribute_PointAttribute_0=
function(){return a.asm.emscripten_bind_PointAttribute_PointAttribute_0.apply(null,arguments)},Cb=a._emscripten_bind_PointAttribute_size_0=function(){return a.asm.emscripten_bind_PointAttribute_size_0.apply(null,arguments)},Db=a._emscripten_bind_PointAttribute_GetAttributeTransformData_0=function(){return a.asm.emscripten_bind_PointAttribute_GetAttributeTransformData_0.apply(null,arguments)},Eb=a._emscripten_bind_PointAttribute_attribute_type_0=function(){return a.asm.emscripten_bind_PointAttribute_attribute_type_0.apply(null,
arguments)},Fb=a._emscripten_bind_PointAttribute_data_type_0=function(){return a.asm.emscripten_bind_PointAttribute_data_type_0.apply(null,arguments)},Gb=a._emscripten_bind_PointAttribute_num_components_0=function(){return a.asm.emscripten_bind_PointAttribute_num_components_0.apply(null,arguments)},Hb=a._emscripten_bind_PointAttribute_normalized_0=function(){return a.asm.emscripten_bind_PointAttribute_normalized_0.apply(null,arguments)},Ib=a._emscripten_bind_PointAttribute_byte_stride_0=function(){return a.asm.emscripten_bind_PointAttribute_byte_stride_0.apply(null,
arguments)},Jb=a._emscripten_bind_PointAttribute_byte_offset_0=function(){return a.asm.emscripten_bind_PointAttribute_byte_offset_0.apply(null,arguments)},Kb=a._emscripten_bind_PointAttribute_unique_id_0=function(){return a.asm.emscripten_bind_PointAttribute_unique_id_0.apply(null,arguments)},Lb=a._emscripten_bind_PointAttribute___destroy___0=function(){return a.asm.emscripten_bind_PointAttribute___destroy___0.apply(null,arguments)},Ua=a._emscripten_bind_AttributeTransformData_AttributeTransformData_0=
function(){return a.asm.emscripten_bind_AttributeTransformData_AttributeTransformData_0.apply(null,arguments)},Mb=a._emscripten_bind_AttributeTransformData_transform_type_0=function(){return a.asm.emscripten_bind_AttributeTransformData_transform_type_0.apply(null,arguments)},Nb=a._emscripten_bind_AttributeTransformData___destroy___0=function(){return a.asm.emscripten_bind_AttributeTransformData___destroy___0.apply(null,arguments)},Va=a._emscripten_bind_AttributeQuantizationTransform_AttributeQuantizationTransform_0=
function(){return a.asm.emscripten_bind_AttributeQuantizationTransform_AttributeQuantizationTransform_0.apply(null,arguments)},Ob=a._emscripten_bind_AttributeQuantizationTransform_InitFromAttribute_1=function(){return a.asm.emscripten_bind_AttributeQuantizationTransform_InitFromAttribute_1.apply(null,arguments)},Pb=a._emscripten_bind_AttributeQuantizationTransform_quantization_bits_0=function(){return a.asm.emscripten_bind_AttributeQuantizationTransform_quantization_bits_0.apply(null,arguments)},
Qb=a._emscripten_bind_AttributeQuantizationTransform_min_value_1=function(){return a.asm.emscripten_bind_AttributeQuantizationTransform_min_value_1.apply(null,arguments)},Rb=a._emscripten_bind_AttributeQuantizationTransform_range_0=function(){return a.asm.emscripten_bind_AttributeQuantizationTransform_range_0.apply(null,arguments)},Sb=a._emscripten_bind_AttributeQuantizationTransform___destroy___0=function(){return a.asm.emscripten_bind_AttributeQuantizationTransform___destroy___0.apply(null,arguments)},
Wa=a._emscripten_bind_DracoInt8Array_DracoInt8Array_0=function(){return a.asm.emscripten_bind_DracoInt8Array_DracoInt8Array_0.apply(null,arguments)},Tb=a._emscripten_bind_DracoInt8Array_GetValue_1=function(){return a.asm.emscripten_bind_DracoInt8Array_GetValue_1.apply(null,arguments)},Ub=a._emscripten_bind_DracoInt8Array_size_0=function(){return a.asm.emscripten_bind_DracoInt8Array_size_0.apply(null,arguments)},Vb=a._emscripten_bind_DracoInt8Array___destroy___0=function(){return a.asm.emscripten_bind_DracoInt8Array___destroy___0.apply(null,
arguments)},Xa=a._emscripten_bind_MetadataQuerier_MetadataQuerier_0=function(){return a.asm.emscripten_bind_MetadataQuerier_MetadataQuerier_0.apply(null,arguments)},Wb=a._emscripten_bind_MetadataQuerier_HasEntry_2=function(){return a.asm.emscripten_bind_MetadataQuerier_HasEntry_2.apply(null,arguments)},Xb=a._emscripten_bind_MetadataQuerier_GetIntEntry_2=function(){return a.asm.emscripten_bind_MetadataQuerier_GetIntEntry_2.apply(null,arguments)},Yb=a._emscripten_bind_MetadataQuerier_GetIntEntryArray_3=
function(){return a.asm.emscripten_bind_MetadataQuerier_GetIntEntryArray_3.apply(null,arguments)},Zb=a._emscripten_bind_MetadataQuerier_GetDoubleEntry_2=function(){return a.asm.emscripten_bind_MetadataQuerier_GetDoubleEntry_2.apply(null,arguments)},$b=a._emscripten_bind_MetadataQuerier_GetStringEntry_2=function(){return a.asm.emscripten_bind_MetadataQuerier_GetStringEntry_2.apply(null,arguments)},ac=a._emscripten_bind_MetadataQuerier_NumEntries_1=function(){return a.asm.emscripten_bind_MetadataQuerier_NumEntries_1.apply(null,
arguments)},bc=a._emscripten_bind_MetadataQuerier_GetEntryName_2=function(){return a.asm.emscripten_bind_MetadataQuerier_GetEntryName_2.apply(null,arguments)},cc=a._emscripten_bind_MetadataQuerier___destroy___0=function(){return a.asm.emscripten_bind_MetadataQuerier___destroy___0.apply(null,arguments)},Ya=a._emscripten_bind_DracoInt16Array_DracoInt16Array_0=function(){return a.asm.emscripten_bind_DracoInt16Array_DracoInt16Array_0.apply(null,arguments)},dc=a._emscripten_bind_DracoInt16Array_GetValue_1=
function(){return a.asm.emscripten_bind_DracoInt16Array_GetValue_1.apply(null,arguments)},ec=a._emscripten_bind_DracoInt16Array_size_0=function(){return a.asm.emscripten_bind_DracoInt16Array_size_0.apply(null,arguments)},fc=a._emscripten_bind_DracoInt16Array___destroy___0=function(){return a.asm.emscripten_bind_DracoInt16Array___destroy___0.apply(null,arguments)},Za=a._emscripten_bind_DracoFloat32Array_DracoFloat32Array_0=function(){return a.asm.emscripten_bind_DracoFloat32Array_DracoFloat32Array_0.apply(null,
arguments)},gc=a._emscripten_bind_DracoFloat32Array_GetValue_1=function(){return a.asm.emscripten_bind_DracoFloat32Array_GetValue_1.apply(null,arguments)},hc=a._emscripten_bind_DracoFloat32Array_size_0=function(){return a.asm.emscripten_bind_DracoFloat32Array_size_0.apply(null,arguments)},ic=a._emscripten_bind_DracoFloat32Array___destroy___0=function(){return a.asm.emscripten_bind_DracoFloat32Array___destroy___0.apply(null,arguments)},$a=a._emscripten_bind_GeometryAttribute_GeometryAttribute_0=function(){return a.asm.emscripten_bind_GeometryAttribute_GeometryAttribute_0.apply(null,
arguments)},jc=a._emscripten_bind_GeometryAttribute___destroy___0=function(){return a.asm.emscripten_bind_GeometryAttribute___destroy___0.apply(null,arguments)},ab=a._emscripten_bind_DecoderBuffer_DecoderBuffer_0=function(){return a.asm.emscripten_bind_DecoderBuffer_DecoderBuffer_0.apply(null,arguments)},kc=a._emscripten_bind_DecoderBuffer_Init_2=function(){return a.asm.emscripten_bind_DecoderBuffer_Init_2.apply(null,arguments)},lc=a._emscripten_bind_DecoderBuffer___destroy___0=function(){return a.asm.emscripten_bind_DecoderBuffer___destroy___0.apply(null,
arguments)},bb=a._emscripten_bind_Decoder_Decoder_0=function(){return a.asm.emscripten_bind_Decoder_Decoder_0.apply(null,arguments)},mc=a._emscripten_bind_Decoder_GetEncodedGeometryType_1=function(){return a.asm.emscripten_bind_Decoder_GetEncodedGeometryType_1.apply(null,arguments)},nc=a._emscripten_bind_Decoder_DecodeBufferToPointCloud_2=function(){return a.asm.emscripten_bind_Decoder_DecodeBufferToPointCloud_2.apply(null,arguments)},oc=a._emscripten_bind_Decoder_DecodeBufferToMesh_2=function(){return a.asm.emscripten_bind_Decoder_DecodeBufferToMesh_2.apply(null,
arguments)},pc=a._emscripten_bind_Decoder_GetAttributeId_2=function(){return a.asm.emscripten_bind_Decoder_GetAttributeId_2.apply(null,arguments)},qc=a._emscripten_bind_Decoder_GetAttributeIdByName_2=function(){return a.asm.emscripten_bind_Decoder_GetAttributeIdByName_2.apply(null,arguments)},rc=a._emscripten_bind_Decoder_GetAttributeIdByMetadataEntry_3=function(){return a.asm.emscripten_bind_Decoder_GetAttributeIdByMetadataEntry_3.apply(null,arguments)},sc=a._emscripten_bind_Decoder_GetAttribute_2=
function(){return a.asm.emscripten_bind_Decoder_GetAttribute_2.apply(null,arguments)},tc=a._emscripten_bind_Decoder_GetAttributeByUniqueId_2=function(){return a.asm.emscripten_bind_Decoder_GetAttributeByUniqueId_2.apply(null,arguments)},uc=a._emscripten_bind_Decoder_GetMetadata_1=function(){return a.asm.emscripten_bind_Decoder_GetMetadata_1.apply(null,arguments)},vc=a._emscripten_bind_Decoder_GetAttributeMetadata_2=function(){return a.asm.emscripten_bind_Decoder_GetAttributeMetadata_2.apply(null,
arguments)},wc=a._emscripten_bind_Decoder_GetFaceFromMesh_3=function(){return a.asm.emscripten_bind_Decoder_GetFaceFromMesh_3.apply(null,arguments)},xc=a._emscripten_bind_Decoder_GetTriangleStripsFromMesh_2=function(){return a.asm.emscripten_bind_Decoder_GetTriangleStripsFromMesh_2.apply(null,arguments)},yc=a._emscripten_bind_Decoder_GetTrianglesUInt16Array_3=function(){return a.asm.emscripten_bind_Decoder_GetTrianglesUInt16Array_3.apply(null,arguments)},zc=a._emscripten_bind_Decoder_GetTrianglesUInt32Array_3=
function(){return a.asm.emscripten_bind_Decoder_GetTrianglesUInt32Array_3.apply(null,arguments)},Ac=a._emscripten_bind_Decoder_GetAttributeFloat_3=function(){return a.asm.emscripten_bind_Decoder_GetAttributeFloat_3.apply(null,arguments)},Bc=a._emscripten_bind_Decoder_GetAttributeFloatForAllPoints_3=function(){return a.asm.emscripten_bind_Decoder_GetAttributeFloatForAllPoints_3.apply(null,arguments)},Cc=a._emscripten_bind_Decoder_GetAttributeIntForAllPoints_3=function(){return a.asm.emscripten_bind_Decoder_GetAttributeIntForAllPoints_3.apply(null,
arguments)},Dc=a._emscripten_bind_Decoder_GetAttributeInt8ForAllPoints_3=function(){return a.asm.emscripten_bind_Decoder_GetAttributeInt8ForAllPoints_3.apply(null,arguments)},Ec=a._emscripten_bind_Decoder_GetAttributeUInt8ForAllPoints_3=function(){return a.asm.emscripten_bind_Decoder_GetAttributeUInt8ForAllPoints_3.apply(null,arguments)},Fc=a._emscripten_bind_Decoder_GetAttributeInt16ForAllPoints_3=function(){return a.asm.emscripten_bind_Decoder_GetAttributeInt16ForAllPoints_3.apply(null,arguments)},
Gc=a._emscripten_bind_Decoder_GetAttributeUInt16ForAllPoints_3=function(){return a.asm.emscripten_bind_Decoder_GetAttributeUInt16ForAllPoints_3.apply(null,arguments)},Hc=a._emscripten_bind_Decoder_GetAttributeInt32ForAllPoints_3=function(){return a.asm.emscripten_bind_Decoder_GetAttributeInt32ForAllPoints_3.apply(null,arguments)},Ic=a._emscripten_bind_Decoder_GetAttributeUInt32ForAllPoints_3=function(){return a.asm.emscripten_bind_Decoder_GetAttributeUInt32ForAllPoints_3.apply(null,arguments)},Jc=
a._emscripten_bind_Decoder_GetAttributeDataArrayForAllPoints_5=function(){return a.asm.emscripten_bind_Decoder_GetAttributeDataArrayForAllPoints_5.apply(null,arguments)},Kc=a._emscripten_bind_Decoder_SkipAttributeTransform_1=function(){return a.asm.emscripten_bind_Decoder_SkipAttributeTransform_1.apply(null,arguments)},Lc=a._emscripten_bind_Decoder___destroy___0=function(){return a.asm.emscripten_bind_Decoder___destroy___0.apply(null,arguments)},cb=a._emscripten_bind_Mesh_Mesh_0=function(){return a.asm.emscripten_bind_Mesh_Mesh_0.apply(null,
arguments)},Mc=a._emscripten_bind_Mesh_num_faces_0=function(){return a.asm.emscripten_bind_Mesh_num_faces_0.apply(null,arguments)},Nc=a._emscripten_bind_Mesh_num_attributes_0=function(){return a.asm.emscripten_bind_Mesh_num_attributes_0.apply(null,arguments)},Oc=a._emscripten_bind_Mesh_num_points_0=function(){return a.asm.emscripten_bind_Mesh_num_points_0.apply(null,arguments)},Pc=a._emscripten_bind_Mesh___destroy___0=function(){return a.asm.emscripten_bind_Mesh___destroy___0.apply(null,arguments)},
Qc=a._emscripten_bind_VoidPtr___destroy___0=function(){return a.asm.emscripten_bind_VoidPtr___destroy___0.apply(null,arguments)},db=a._emscripten_bind_DracoInt32Array_DracoInt32Array_0=function(){return a.asm.emscripten_bind_DracoInt32Array_DracoInt32Array_0.apply(null,arguments)},Rc=a._emscripten_bind_DracoInt32Array_GetValue_1=function(){return a.asm.emscripten_bind_DracoInt32Array_GetValue_1.apply(null,arguments)},Sc=a._emscripten_bind_DracoInt32Array_size_0=function(){return a.asm.emscripten_bind_DracoInt32Array_size_0.apply(null,
arguments)},Tc=a._emscripten_bind_DracoInt32Array___destroy___0=function(){return a.asm.emscripten_bind_DracoInt32Array___destroy___0.apply(null,arguments)},eb=a._emscripten_bind_Metadata_Metadata_0=function(){return a.asm.emscripten_bind_Metadata_Metadata_0.apply(null,arguments)},Uc=a._emscripten_bind_Metadata___destroy___0=function(){return a.asm.emscripten_bind_Metadata___destroy___0.apply(null,arguments)},Vc=a._emscripten_enum_draco_StatusCode_OK=function(){return a.asm.emscripten_enum_draco_StatusCode_OK.apply(null,
arguments)},Wc=a._emscripten_enum_draco_StatusCode_DRACO_ERROR=function(){return a.asm.emscripten_enum_draco_StatusCode_DRACO_ERROR.apply(null,arguments)},Xc=a._emscripten_enum_draco_StatusCode_IO_ERROR=function(){return a.asm.emscripten_enum_draco_StatusCode_IO_ERROR.apply(null,arguments)},Yc=a._emscripten_enum_draco_StatusCode_INVALID_PARAMETER=function(){return a.asm.emscripten_enum_draco_StatusCode_INVALID_PARAMETER.apply(null,arguments)},Zc=a._emscripten_enum_draco_StatusCode_UNSUPPORTED_VERSION=
function(){return a.asm.emscripten_enum_draco_StatusCode_UNSUPPORTED_VERSION.apply(null,arguments)},$c=a._emscripten_enum_draco_StatusCode_UNKNOWN_VERSION=function(){return a.asm.emscripten_enum_draco_StatusCode_UNKNOWN_VERSION.apply(null,arguments)},ad=a._emscripten_enum_draco_DataType_DT_INVALID=function(){return a.asm.emscripten_enum_draco_DataType_DT_INVALID.apply(null,arguments)},bd=a._emscripten_enum_draco_DataType_DT_INT8=function(){return a.asm.emscripten_enum_draco_DataType_DT_INT8.apply(null,
arguments)},cd=a._emscripten_enum_draco_DataType_DT_UINT8=function(){return a.asm.emscripten_enum_draco_DataType_DT_UINT8.apply(null,arguments)},dd=a._emscripten_enum_draco_DataType_DT_INT16=function(){return a.asm.emscripten_enum_draco_DataType_DT_INT16.apply(null,arguments)},ed=a._emscripten_enum_draco_DataType_DT_UINT16=function(){return a.asm.emscripten_enum_draco_DataType_DT_UINT16.apply(null,arguments)},fd=a._emscripten_enum_draco_DataType_DT_INT32=function(){return a.asm.emscripten_enum_draco_DataType_DT_INT32.apply(null,
arguments)},gd=a._emscripten_enum_draco_DataType_DT_UINT32=function(){return a.asm.emscripten_enum_draco_DataType_DT_UINT32.apply(null,arguments)},hd=a._emscripten_enum_draco_DataType_DT_INT64=function(){return a.asm.emscripten_enum_draco_DataType_DT_INT64.apply(null,arguments)},id=a._emscripten_enum_draco_DataType_DT_UINT64=function(){return a.asm.emscripten_enum_draco_DataType_DT_UINT64.apply(null,arguments)},jd=a._emscripten_enum_draco_DataType_DT_FLOAT32=function(){return a.asm.emscripten_enum_draco_DataType_DT_FLOAT32.apply(null,
arguments)},kd=a._emscripten_enum_draco_DataType_DT_FLOAT64=function(){return a.asm.emscripten_enum_draco_DataType_DT_FLOAT64.apply(null,arguments)},ld=a._emscripten_enum_draco_DataType_DT_BOOL=function(){return a.asm.emscripten_enum_draco_DataType_DT_BOOL.apply(null,arguments)},md=a._emscripten_enum_draco_DataType_DT_TYPES_COUNT=function(){return a.asm.emscripten_enum_draco_DataType_DT_TYPES_COUNT.apply(null,arguments)},nd=a._emscripten_enum_draco_EncodedGeometryType_INVALID_GEOMETRY_TYPE=function(){return a.asm.emscripten_enum_draco_EncodedGeometryType_INVALID_GEOMETRY_TYPE.apply(null,
arguments)},od=a._emscripten_enum_draco_EncodedGeometryType_POINT_CLOUD=function(){return a.asm.emscripten_enum_draco_EncodedGeometryType_POINT_CLOUD.apply(null,arguments)},pd=a._emscripten_enum_draco_EncodedGeometryType_TRIANGULAR_MESH=function(){return a.asm.emscripten_enum_draco_EncodedGeometryType_TRIANGULAR_MESH.apply(null,arguments)},qd=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_INVALID_TRANSFORM=function(){return a.asm.emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_INVALID_TRANSFORM.apply(null,
arguments)},rd=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_NO_TRANSFORM=function(){return a.asm.emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_NO_TRANSFORM.apply(null,arguments)},sd=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_QUANTIZATION_TRANSFORM=function(){return a.asm.emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_QUANTIZATION_TRANSFORM.apply(null,arguments)},td=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_OCTAHEDRON_TRANSFORM=function(){return a.asm.emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_OCTAHEDRON_TRANSFORM.apply(null,
arguments)},ud=a._emscripten_enum_draco_GeometryAttribute_Type_INVALID=function(){return a.asm.emscripten_enum_draco_GeometryAttribute_Type_INVALID.apply(null,arguments)},vd=a._emscripten_enum_draco_GeometryAttribute_Type_POSITION=function(){return a.asm.emscripten_enum_draco_GeometryAttribute_Type_POSITION.apply(null,arguments)},wd=a._emscripten_enum_draco_GeometryAttribute_Type_NORMAL=function(){return a.asm.emscripten_enum_draco_GeometryAttribute_Type_NORMAL.apply(null,arguments)},xd=a._emscripten_enum_draco_GeometryAttribute_Type_COLOR=
function(){return a.asm.emscripten_enum_draco_GeometryAttribute_Type_COLOR.apply(null,arguments)},yd=a._emscripten_enum_draco_GeometryAttribute_Type_TEX_COORD=function(){return a.asm.emscripten_enum_draco_GeometryAttribute_Type_TEX_COORD.apply(null,arguments)},zd=a._emscripten_enum_draco_GeometryAttribute_Type_GENERIC=function(){return a.asm.emscripten_enum_draco_GeometryAttribute_Type_GENERIC.apply(null,arguments)};a._setThrew=function(){return a.asm.setThrew.apply(null,arguments)};var ta=a.__ZSt18uncaught_exceptionv=
function(){return a.asm._ZSt18uncaught_exceptionv.apply(null,arguments)};a._free=function(){return a.asm.free.apply(null,arguments)};var ib=a._malloc=function(){return a.asm.malloc.apply(null,arguments)};a.stackSave=function(){return a.asm.stackSave.apply(null,arguments)};a.stackAlloc=function(){return a.asm.stackAlloc.apply(null,arguments)};a.stackRestore=function(){return a.asm.stackRestore.apply(null,arguments)};a.__growWasmMemory=function(){return a.asm.__growWasmMemory.apply(null,arguments)};
a.dynCall_ii=function(){return a.asm.dynCall_ii.apply(null,arguments)};a.dynCall_vi=function(){return a.asm.dynCall_vi.apply(null,arguments)};a.dynCall_iii=function(){return a.asm.dynCall_iii.apply(null,arguments)};a.dynCall_vii=function(){return a.asm.dynCall_vii.apply(null,arguments)};a.dynCall_iiii=function(){return a.asm.dynCall_iiii.apply(null,arguments)};a.dynCall_v=function(){return a.asm.dynCall_v.apply(null,arguments)};a.dynCall_viii=function(){return a.asm.dynCall_viii.apply(null,arguments)};
a.dynCall_viiii=function(){return a.asm.dynCall_viiii.apply(null,arguments)};a.dynCall_iiiiiii=function(){return a.asm.dynCall_iiiiiii.apply(null,arguments)};a.dynCall_iidiiii=function(){return a.asm.dynCall_iidiiii.apply(null,arguments)};a.dynCall_jiji=function(){return a.asm.dynCall_jiji.apply(null,arguments)};a.dynCall_viiiiii=function(){return a.asm.dynCall_viiiiii.apply(null,arguments)};a.dynCall_viiiii=function(){return a.asm.dynCall_viiiii.apply(null,arguments)};a.asm=La;var fa;a.then=function(e){if(fa)e(a);
else{var c=a.onRuntimeInitialized;a.onRuntimeInitialized=function(){c&&c();e(a)}}return a};ja=function c(){fa||ma();fa||(ja=c)};a.run=ma;if(a.preInit)for("function"==typeof a.preInit&&(a.preInit=[a.preInit]);0<a.preInit.length;)a.preInit.pop()();ma();p.prototype=Object.create(p.prototype);p.prototype.constructor=p;p.prototype.__class__=p;p.__cache__={};a.WrapperObject=p;a.getCache=u;a.wrapPointer=N;a.castObject=function(a,b){return N(a.ptr,b)};a.NULL=N(0);a.destroy=function(a){if(!a.__destroy__)throw"Error: Cannot destroy object. (Did you create it yourself?)";
a.__destroy__();delete u(a.__class__)[a.ptr]};a.compare=function(a,b){return a.ptr===b.ptr};a.getPointer=function(a){return a.ptr};a.getClass=function(a){return a.__class__};var n={buffer:0,size:0,pos:0,temps:[],needed:0,prepare:function(){if(n.needed){for(var c=0;c<n.temps.length;c++)a._free(n.temps[c]);n.temps.length=0;a._free(n.buffer);n.buffer=0;n.size+=n.needed;n.needed=0}n.buffer||(n.size+=128,n.buffer=a._malloc(n.size),t(n.buffer));n.pos=0},alloc:function(c,b){t(n.buffer);c=c.length*b.BYTES_PER_ELEMENT;
c=c+7&-8;n.pos+c>=n.size?(t(0<c),n.needed+=c,b=a._malloc(c),n.temps.push(b)):(b=n.buffer+n.pos,n.pos+=c);return b},copy:function(a,b,d){switch(b.BYTES_PER_ELEMENT){case 2:d>>=1;break;case 4:d>>=2;break;case 8:d>>=3}for(var c=0;c<a.length;c++)b[d+c]=a[c]}};x.prototype=Object.create(p.prototype);x.prototype.constructor=x;x.prototype.__class__=x;x.__cache__={};a.Status=x;x.prototype.code=x.prototype.code=function(){return jb(this.ptr)};x.prototype.ok=x.prototype.ok=function(){return!!kb(this.ptr)};x.prototype.error_msg=
x.prototype.error_msg=function(){return X(lb(this.ptr))};x.prototype.__destroy__=x.prototype.__destroy__=function(){mb(this.ptr)};A.prototype=Object.create(p.prototype);A.prototype.constructor=A;A.prototype.__class__=A;A.__cache__={};a.DracoUInt16Array=A;A.prototype.GetValue=A.prototype.GetValue=function(a){var c=this.ptr;a&&"object"===typeof a&&(a=a.ptr);return nb(c,a)};A.prototype.size=A.prototype.size=function(){return ob(this.ptr)};A.prototype.__destroy__=A.prototype.__destroy__=function(){pb(this.ptr)};
B.prototype=Object.create(p.prototype);B.prototype.constructor=B;B.prototype.__class__=B;B.__cache__={};a.PointCloud=B;B.prototype.num_attributes=B.prototype.num_attributes=function(){return qb(this.ptr)};B.prototype.num_points=B.prototype.num_points=function(){return rb(this.ptr)};B.prototype.__destroy__=B.prototype.__destroy__=function(){sb(this.ptr)};C.prototype=Object.create(p.prototype);C.prototype.constructor=C;C.prototype.__class__=C;C.__cache__={};a.DracoUInt8Array=C;C.prototype.GetValue=
C.prototype.GetValue=function(a){var c=this.ptr;a&&"object"===typeof a&&(a=a.ptr);return tb(c,a)};C.prototype.size=C.prototype.size=function(){return ub(this.ptr)};C.prototype.__destroy__=C.prototype.__destroy__=function(){vb(this.ptr)};D.prototype=Object.create(p.prototype);D.prototype.constructor=D;D.prototype.__class__=D;D.__cache__={};a.DracoUInt32Array=D;D.prototype.GetValue=D.prototype.GetValue=function(a){var c=this.ptr;a&&"object"===typeof a&&(a=a.ptr);return wb(c,a)};D.prototype.size=D.prototype.size=
function(){return xb(this.ptr)};D.prototype.__destroy__=D.prototype.__destroy__=function(){yb(this.ptr)};E.prototype=Object.create(p.prototype);E.prototype.constructor=E;E.prototype.__class__=E;E.__cache__={};a.AttributeOctahedronTransform=E;E.prototype.InitFromAttribute=E.prototype.InitFromAttribute=function(a){var c=this.ptr;a&&"object"===typeof a&&(a=a.ptr);return!!zb(c,a)};E.prototype.quantization_bits=E.prototype.quantization_bits=function(){return Ab(this.ptr)};E.prototype.__destroy__=E.prototype.__destroy__=
function(){Bb(this.ptr)};q.prototype=Object.create(p.prototype);q.prototype.constructor=q;q.prototype.__class__=q;q.__cache__={};a.PointAttribute=q;q.prototype.size=q.prototype.size=function(){return Cb(this.ptr)};q.prototype.GetAttributeTransformData=q.prototype.GetAttributeTransformData=function(){return N(Db(this.ptr),J)};q.prototype.attribute_type=q.prototype.attribute_type=function(){return Eb(this.ptr)};q.prototype.data_type=q.prototype.data_type=function(){return Fb(this.ptr)};q.prototype.num_components=
q.prototype.num_components=function(){return Gb(this.ptr)};q.prototype.normalized=q.prototype.normalized=function(){return!!Hb(this.ptr)};q.prototype.byte_stride=q.prototype.byte_stride=function(){return Ib(this.ptr)};q.prototype.byte_offset=q.prototype.byte_offset=function(){return Jb(this.ptr)};q.prototype.unique_id=q.prototype.unique_id=function(){return Kb(this.ptr)};q.prototype.__destroy__=q.prototype.__destroy__=function(){Lb(this.ptr)};J.prototype=Object.create(p.prototype);J.prototype.constructor=
J;J.prototype.__class__=J;J.__cache__={};a.AttributeTransformData=J;J.prototype.transform_type=J.prototype.transform_type=function(){return Mb(this.ptr)};J.prototype.__destroy__=J.prototype.__destroy__=function(){Nb(this.ptr)};w.prototype=Object.create(p.prototype);w.prototype.constructor=w;w.prototype.__class__=w;w.__cache__={};a.AttributeQuantizationTransform=w;w.prototype.InitFromAttribute=w.prototype.InitFromAttribute=function(a){var c=this.ptr;a&&"object"===typeof a&&(a=a.ptr);return!!Ob(c,a)};
w.prototype.quantization_bits=w.prototype.quantization_bits=function(){return Pb(this.ptr)};w.prototype.min_value=w.prototype.min_value=function(a){var c=this.ptr;a&&"object"===typeof a&&(a=a.ptr);return Qb(c,a)};w.prototype.range=w.prototype.range=function(){return Rb(this.ptr)};w.prototype.__destroy__=w.prototype.__destroy__=function(){Sb(this.ptr)};F.prototype=Object.create(p.prototype);F.prototype.constructor=F;F.prototype.__class__=F;F.__cache__={};a.DracoInt8Array=F;F.prototype.GetValue=F.prototype.GetValue=
function(a){var c=this.ptr;a&&"object"===typeof a&&(a=a.ptr);return Tb(c,a)};F.prototype.size=F.prototype.size=function(){return Ub(this.ptr)};F.prototype.__destroy__=F.prototype.__destroy__=function(){Vb(this.ptr)};r.prototype=Object.create(p.prototype);r.prototype.constructor=r;r.prototype.__class__=r;r.__cache__={};a.MetadataQuerier=r;r.prototype.HasEntry=r.prototype.HasEntry=function(a,b){var c=this.ptr;n.prepare();a&&"object"===typeof a&&(a=a.ptr);b=b&&"object"===typeof b?b.ptr:V(b);return!!Wb(c,
a,b)};r.prototype.GetIntEntry=r.prototype.GetIntEntry=function(a,b){var c=this.ptr;n.prepare();a&&"object"===typeof a&&(a=a.ptr);b=b&&"object"===typeof b?b.ptr:V(b);return Xb(c,a,b)};r.prototype.GetIntEntryArray=r.prototype.GetIntEntryArray=function(a,b,d){var c=this.ptr;n.prepare();a&&"object"===typeof a&&(a=a.ptr);b=b&&"object"===typeof b?b.ptr:V(b);d&&"object"===typeof d&&(d=d.ptr);Yb(c,a,b,d)};r.prototype.GetDoubleEntry=r.prototype.GetDoubleEntry=function(a,b){var c=this.ptr;n.prepare();a&&"object"===
typeof a&&(a=a.ptr);b=b&&"object"===typeof b?b.ptr:V(b);return Zb(c,a,b)};r.prototype.GetStringEntry=r.prototype.GetStringEntry=function(a,b){var c=this.ptr;n.prepare();a&&"object"===typeof a&&(a=a.ptr);b=b&&"object"===typeof b?b.ptr:V(b);return X($b(c,a,b))};r.prototype.NumEntries=r.prototype.NumEntries=function(a){var c=this.ptr;a&&"object"===typeof a&&(a=a.ptr);return ac(c,a)};r.prototype.GetEntryName=r.prototype.GetEntryName=function(a,b){var c=this.ptr;a&&"object"===typeof a&&(a=a.ptr);b&&"object"===
typeof b&&(b=b.ptr);return X(bc(c,a,b))};r.prototype.__destroy__=r.prototype.__destroy__=function(){cc(this.ptr)};G.prototype=Object.create(p.prototype);G.prototype.constructor=G;G.prototype.__class__=G;G.__cache__={};a.DracoInt16Array=G;G.prototype.GetValue=G.prototype.GetValue=function(a){var c=this.ptr;a&&"object"===typeof a&&(a=a.ptr);return dc(c,a)};G.prototype.size=G.prototype.size=function(){return ec(this.ptr)};G.prototype.__destroy__=G.prototype.__destroy__=function(){fc(this.ptr)};H.prototype=
Object.create(p.prototype);H.prototype.constructor=H;H.prototype.__class__=H;H.__cache__={};a.DracoFloat32Array=H;H.prototype.GetValue=H.prototype.GetValue=function(a){var c=this.ptr;a&&"object"===typeof a&&(a=a.ptr);return gc(c,a)};H.prototype.size=H.prototype.size=function(){return hc(this.ptr)};H.prototype.__destroy__=H.prototype.__destroy__=function(){ic(this.ptr)};O.prototype=Object.create(p.prototype);O.prototype.constructor=O;O.prototype.__class__=O;O.__cache__={};a.GeometryAttribute=O;O.prototype.__destroy__=
O.prototype.__destroy__=function(){jc(this.ptr)};K.prototype=Object.create(p.prototype);K.prototype.constructor=K;K.prototype.__class__=K;K.__cache__={};a.DecoderBuffer=K;K.prototype.Init=K.prototype.Init=function(a,b){var c=this.ptr;n.prepare();if("object"==typeof a&&"object"===typeof a){var e=n.alloc(a,T);n.copy(a,T,e);a=e}b&&"object"===typeof b&&(b=b.ptr);kc(c,a,b)};K.prototype.__destroy__=K.prototype.__destroy__=function(){lc(this.ptr)};g.prototype=Object.create(p.prototype);g.prototype.constructor=
g;g.prototype.__class__=g;g.__cache__={};a.Decoder=g;g.prototype.GetEncodedGeometryType=g.prototype.GetEncodedGeometryType=function(a){var c=this.ptr;a&&"object"===typeof a&&(a=a.ptr);return mc(c,a)};g.prototype.DecodeBufferToPointCloud=g.prototype.DecodeBufferToPointCloud=function(a,b){var c=this.ptr;a&&"object"===typeof a&&(a=a.ptr);b&&"object"===typeof b&&(b=b.ptr);return N(nc(c,a,b),x)};g.prototype.DecodeBufferToMesh=g.prototype.DecodeBufferToMesh=function(a,b){var c=this.ptr;a&&"object"===typeof a&&
(a=a.ptr);b&&"object"===typeof b&&(b=b.ptr);return N(oc(c,a,b),x)};g.prototype.GetAttributeId=g.prototype.GetAttributeId=function(a,b){var c=this.ptr;a&&"object"===typeof a&&(a=a.ptr);b&&"object"===typeof b&&(b=b.ptr);return pc(c,a,b)};g.prototype.GetAttributeIdByName=g.prototype.GetAttributeIdByName=function(a,b){var c=this.ptr;n.prepare();a&&"object"===typeof a&&(a=a.ptr);b=b&&"object"===typeof b?b.ptr:V(b);return qc(c,a,b)};g.prototype.GetAttributeIdByMetadataEntry=g.prototype.GetAttributeIdByMetadataEntry=
function(a,b,d){var c=this.ptr;n.prepare();a&&"object"===typeof a&&(a=a.ptr);b=b&&"object"===typeof b?b.ptr:V(b);d=d&&"object"===typeof d?d.ptr:V(d);return rc(c,a,b,d)};g.prototype.GetAttribute=g.prototype.GetAttribute=function(a,b){var c=this.ptr;a&&"object"===typeof a&&(a=a.ptr);b&&"object"===typeof b&&(b=b.ptr);return N(sc(c,a,b),q)};g.prototype.GetAttributeByUniqueId=g.prototype.GetAttributeByUniqueId=function(a,b){var c=this.ptr;a&&"object"===typeof a&&(a=a.ptr);b&&"object"===typeof b&&(b=b.ptr);
return N(tc(c,a,b),q)};g.prototype.GetMetadata=g.prototype.GetMetadata=function(a){var c=this.ptr;a&&"object"===typeof a&&(a=a.ptr);return N(uc(c,a),L)};g.prototype.GetAttributeMetadata=g.prototype.GetAttributeMetadata=function(a,b){var c=this.ptr;a&&"object"===typeof a&&(a=a.ptr);b&&"object"===typeof b&&(b=b.ptr);return N(vc(c,a,b),L)};g.prototype.GetFaceFromMesh=g.prototype.GetFaceFromMesh=function(a,b,d){var c=this.ptr;a&&"object"===typeof a&&(a=a.ptr);b&&"object"===typeof b&&(b=b.ptr);d&&"object"===
typeof d&&(d=d.ptr);return!!wc(c,a,b,d)};g.prototype.GetTriangleStripsFromMesh=g.prototype.GetTriangleStripsFromMesh=function(a,b){var c=this.ptr;a&&"object"===typeof a&&(a=a.ptr);b&&"object"===typeof b&&(b=b.ptr);return xc(c,a,b)};g.prototype.GetTrianglesUInt16Array=g.prototype.GetTrianglesUInt16Array=function(a,b,d){var c=this.ptr;a&&"object"===typeof a&&(a=a.ptr);b&&"object"===typeof b&&(b=b.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!yc(c,a,b,d)};g.prototype.GetTrianglesUInt32Array=g.prototype.GetTrianglesUInt32Array=
function(a,b,d){var c=this.ptr;a&&"object"===typeof a&&(a=a.ptr);b&&"object"===typeof b&&(b=b.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!zc(c,a,b,d)};g.prototype.GetAttributeFloat=g.prototype.GetAttributeFloat=function(a,b,d){var c=this.ptr;a&&"object"===typeof a&&(a=a.ptr);b&&"object"===typeof b&&(b=b.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!Ac(c,a,b,d)};g.prototype.GetAttributeFloatForAllPoints=g.prototype.GetAttributeFloatForAllPoints=function(a,b,d){var c=this.ptr;a&&"object"===typeof a&&
(a=a.ptr);b&&"object"===typeof b&&(b=b.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!Bc(c,a,b,d)};g.prototype.GetAttributeIntForAllPoints=g.prototype.GetAttributeIntForAllPoints=function(a,b,d){var c=this.ptr;a&&"object"===typeof a&&(a=a.ptr);b&&"object"===typeof b&&(b=b.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!Cc(c,a,b,d)};g.prototype.GetAttributeInt8ForAllPoints=g.prototype.GetAttributeInt8ForAllPoints=function(a,b,d){var c=this.ptr;a&&"object"===typeof a&&(a=a.ptr);b&&"object"===typeof b&&
(b=b.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!Dc(c,a,b,d)};g.prototype.GetAttributeUInt8ForAllPoints=g.prototype.GetAttributeUInt8ForAllPoints=function(a,b,d){var c=this.ptr;a&&"object"===typeof a&&(a=a.ptr);b&&"object"===typeof b&&(b=b.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!Ec(c,a,b,d)};g.prototype.GetAttributeInt16ForAllPoints=g.prototype.GetAttributeInt16ForAllPoints=function(a,b,d){var c=this.ptr;a&&"object"===typeof a&&(a=a.ptr);b&&"object"===typeof b&&(b=b.ptr);d&&"object"===typeof d&&
(d=d.ptr);return!!Fc(c,a,b,d)};g.prototype.GetAttributeUInt16ForAllPoints=g.prototype.GetAttributeUInt16ForAllPoints=function(a,b,d){var c=this.ptr;a&&"object"===typeof a&&(a=a.ptr);b&&"object"===typeof b&&(b=b.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!Gc(c,a,b,d)};g.prototype.GetAttributeInt32ForAllPoints=g.prototype.GetAttributeInt32ForAllPoints=function(a,b,d){var c=this.ptr;a&&"object"===typeof a&&(a=a.ptr);b&&"object"===typeof b&&(b=b.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!Hc(c,
a,b,d)};g.prototype.GetAttributeUInt32ForAllPoints=g.prototype.GetAttributeUInt32ForAllPoints=function(a,b,d){var c=this.ptr;a&&"object"===typeof a&&(a=a.ptr);b&&"object"===typeof b&&(b=b.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!Ic(c,a,b,d)};g.prototype.GetAttributeDataArrayForAllPoints=g.prototype.GetAttributeDataArrayForAllPoints=function(a,b,d,e,f){var c=this.ptr;a&&"object"===typeof a&&(a=a.ptr);b&&"object"===typeof b&&(b=b.ptr);d&&"object"===typeof d&&(d=d.ptr);e&&"object"===typeof e&&
(e=e.ptr);f&&"object"===typeof f&&(f=f.ptr);return!!Jc(c,a,b,d,e,f)};g.prototype.SkipAttributeTransform=g.prototype.SkipAttributeTransform=function(a){var b=this.ptr;a&&"object"===typeof a&&(a=a.ptr);Kc(b,a)};g.prototype.__destroy__=g.prototype.__destroy__=function(){Lc(this.ptr)};y.prototype=Object.create(p.prototype);y.prototype.constructor=y;y.prototype.__class__=y;y.__cache__={};a.Mesh=y;y.prototype.num_faces=y.prototype.num_faces=function(){return Mc(this.ptr)};y.prototype.num_attributes=y.prototype.num_attributes=
function(){return Nc(this.ptr)};y.prototype.num_points=y.prototype.num_points=function(){return Oc(this.ptr)};y.prototype.__destroy__=y.prototype.__destroy__=function(){Pc(this.ptr)};Q.prototype=Object.create(p.prototype);Q.prototype.constructor=Q;Q.prototype.__class__=Q;Q.__cache__={};a.VoidPtr=Q;Q.prototype.__destroy__=Q.prototype.__destroy__=function(){Qc(this.ptr)};I.prototype=Object.create(p.prototype);I.prototype.constructor=I;I.prototype.__class__=I;I.__cache__={};a.DracoInt32Array=I;I.prototype.GetValue=
I.prototype.GetValue=function(a){var b=this.ptr;a&&"object"===typeof a&&(a=a.ptr);return Rc(b,a)};I.prototype.size=I.prototype.size=function(){return Sc(this.ptr)};I.prototype.__destroy__=I.prototype.__destroy__=function(){Tc(this.ptr)};L.prototype=Object.create(p.prototype);L.prototype.constructor=L;L.prototype.__class__=L;L.__cache__={};a.Metadata=L;L.prototype.__destroy__=L.prototype.__destroy__=function(){Uc(this.ptr)};(function(){function c(){a.OK=Vc();a.DRACO_ERROR=Wc();a.IO_ERROR=Xc();a.INVALID_PARAMETER=
Yc();a.UNSUPPORTED_VERSION=Zc();a.UNKNOWN_VERSION=$c();a.DT_INVALID=ad();a.DT_INT8=bd();a.DT_UINT8=cd();a.DT_INT16=dd();a.DT_UINT16=ed();a.DT_INT32=fd();a.DT_UINT32=gd();a.DT_INT64=hd();a.DT_UINT64=id();a.DT_FLOAT32=jd();a.DT_FLOAT64=kd();a.DT_BOOL=ld();a.DT_TYPES_COUNT=md();a.INVALID_GEOMETRY_TYPE=nd();a.POINT_CLOUD=od();a.TRIANGULAR_MESH=pd();a.ATTRIBUTE_INVALID_TRANSFORM=qd();a.ATTRIBUTE_NO_TRANSFORM=rd();a.ATTRIBUTE_QUANTIZATION_TRANSFORM=sd();a.ATTRIBUTE_OCTAHEDRON_TRANSFORM=td();a.INVALID=ud();
a.POSITION=vd();a.NORMAL=wd();a.COLOR=xd();a.TEX_COORD=yd();a.GENERIC=zd()}Ba?c():Da.unshift(c)})();if("function"===typeof a.onModuleParsed)a.onModuleParsed();return m}}();"object"===typeof exports&&"object"===typeof module?module.exports=DracoDecoderModule:"function"===typeof define&&define.amd?define([],function(){return DracoDecoderModule}):"object"===typeof exports&&(exports.DracoDecoderModule=DracoDecoderModule);

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

@@ -5,8 +5,9 @@ export default defineConfig({
root: '.', root: '.',
server: { server: {
port: 8080, port: 8080,
host: '0.0.0.0',
open: true, open: true,
// 允许访问父目录的文件(用于加载 SDK allowedHosts: true,
fs: { fs: {
allow: ['..'] allow: ['..']
} }

View File

@@ -11,7 +11,7 @@ import { SectionPlaneDialogManager } from './managers/section-plane-dialog-manag
import { SectionAxisDialogManager } from './managers/section-axis-dialog-manager'; import { SectionAxisDialogManager } from './managers/section-axis-dialog-manager';
import { SectionBoxDialogManager } from './managers/section-box-dialog-manager'; import { SectionBoxDialogManager } from './managers/section-box-dialog-manager';
import { WalkControlManager } from './managers/walk-control-manager'; import { WalkControlManager } from './managers/walk-control-manager';
import { MapDialogManager } from './managers/map-dialog-manager'; import { EngineInfoDialogManager } from './managers/engine-info-dialog-manager';
import { ComponentDetailManager } from './managers/component-detail-manager'; import { ComponentDetailManager } from './managers/component-detail-manager';
import { AiChatManager } from './managers/ai-chat-manager'; import { AiChatManager } from './managers/ai-chat-manager';
import type { EngineOptions, ModelLoadOptions } from './components/engine'; import type { EngineOptions, ModelLoadOptions } from './components/engine';
@@ -41,7 +41,7 @@ export class BimEngine {
public sectionAxis: SectionAxisDialogManager | null = null; public sectionAxis: SectionAxisDialogManager | null = null;
public sectionBox: SectionBoxDialogManager | null = null; public sectionBox: SectionBoxDialogManager | null = null;
public walkControl: WalkControlManager | null = null; public walkControl: WalkControlManager | null = null;
public map: MapDialogManager | null = null; public engineInfo: EngineInfoDialogManager | null = null;
public componentDetail: ComponentDetailManager | null = null; public componentDetail: ComponentDetailManager | null = null;
public aiChat: AiChatManager | null = null; public aiChat: AiChatManager | null = null;
@@ -116,8 +116,8 @@ export class BimEngine {
this.sectionBox = new SectionBoxDialogManager(); this.sectionBox = new SectionBoxDialogManager();
this.walkControl = new WalkControlManager(); this.walkControl = new WalkControlManager();
this.walkControl.init(); this.walkControl.init();
this.map = new MapDialogManager(); this.engineInfo = new EngineInfoDialogManager();
this.map.init(); this.engineInfo.init();
this.registry.engine3d = this.engine; this.registry.engine3d = this.engine;
this.registry.dialog = this.dialog; this.registry.dialog = this.dialog;
@@ -131,7 +131,7 @@ export class BimEngine {
this.registry.sectionAxis = this.sectionAxis; this.registry.sectionAxis = this.sectionAxis;
this.registry.sectionBox = this.sectionBox; this.registry.sectionBox = this.sectionBox;
this.registry.walkControl = this.walkControl; this.registry.walkControl = this.walkControl;
this.registry.map = this.map; this.registry.engineInfo = this.engineInfo;
this.componentDetail = new ComponentDetailManager(); this.componentDetail = new ComponentDetailManager();
this.registry.componentDetail = this.componentDetail; this.registry.componentDetail = this.componentDetail;

View File

@@ -3,6 +3,8 @@ import { getIcon } from '../../../../../utils/icon-manager';
import { ManagerRegistry } from '../../../../../core/manager-registry'; import { ManagerRegistry } from '../../../../../core/manager-registry';
export const createInfoButton = (): ButtonConfig => { export const createInfoButton = (): ButtonConfig => {
const registry = ManagerRegistry.getInstance();
return { return {
id: 'info', id: 'info',
groupId: 'group-2', groupId: 'group-2',
@@ -11,8 +13,7 @@ export const createInfoButton = (): ButtonConfig => {
icon: getIcon('信息'), icon: getIcon('信息'),
keepActive: false, keepActive: false,
onClick: () => { onClick: () => {
const registry = ManagerRegistry.getInstance(); registry.engineInfo?.show();
registry.emit('ui:open-dialog', { id: 'info' });
} }
}; };
}; };

View File

@@ -5,14 +5,6 @@ import { ManagerRegistry } from '../../../../../core/manager-registry';
export const createMapButton = (): ButtonConfig => { export const createMapButton = (): ButtonConfig => {
const registry = ManagerRegistry.getInstance(); const registry = ManagerRegistry.getInstance();
registry.on('map:opened', () => {
registry.toolbar?.setBtnActive('map', true);
});
registry.on('map:closed', () => {
registry.toolbar?.setBtnActive('map', false);
});
return { return {
id: 'map', id: 'map',
groupId: 'group-1', groupId: 'group-1',
@@ -22,11 +14,7 @@ export const createMapButton = (): ButtonConfig => {
keepActive: true, keepActive: true,
icon: getIcon('地图'), icon: getIcon('地图'),
onClick: () => { onClick: () => {
if (registry.map?.isOpen()) { registry.engine3d?.toggleMiniMap();
registry.map?.hide();
} else {
registry.map?.show();
}
} }
}; };
}; };

View File

@@ -1,30 +0,0 @@
.bim-info-dialog-content {
padding: 16px;
font-family: sans-serif;
color: #333;
}
.bim-info-dialog-content h3 {
margin-top: 0;
margin-bottom: 12px;
border-bottom: 1px solid #eee;
padding-bottom: 8px;
color: #0078d4;
}
.bim-info-dialog-content ul {
list-style: none;
padding: 0;
margin: 0;
}
.bim-info-dialog-content li {
margin-bottom: 8px;
font-size: 14px;
display: flex;
}
.bim-info-dialog-content li strong {
width: 80px;
color: #555;
}

View File

@@ -1,65 +0,0 @@
import './index.css';
import { BimDialog } from '../index';
/**
* BimInfoDialog (继承版)
* 这是一个展示项目信息的业务弹窗组件,直接继承自 BimDialog。
*/
export class BimInfoDialog extends BimDialog {
/**
* 构造函数
* @param container 父容器
*/
constructor(container: HTMLElement) {
// 1. 准备内容 DOM
const contentEl = document.createElement('div');
contentEl.className = 'bim-info-dialog-content';
const infoTitle = document.createElement('h3');
infoTitle.textContent = 'Model Information';
const infoList = document.createElement('ul');
infoList.innerHTML = `
<li><strong>Name:</strong> Sample Project</li>
<li><strong>Version:</strong> 1.0.0</li>
<li><strong>Date:</strong> ${new Date().toLocaleDateString()}</li>
<li><strong>Status:</strong> <span style="color: green;">Active</span></li>
`;
const actionBtn = document.createElement('button');
actionBtn.textContent = 'Update Status';
actionBtn.style.marginTop = '10px';
actionBtn.onclick = () => {
alert('Status updated!');
};
contentEl.appendChild(infoTitle);
contentEl.appendChild(infoList);
contentEl.appendChild(actionBtn);
// 2. 调用父类构造函数,传入特定的配置
super({
container: container,
title: 'dialog.testTitle',
content: contentEl,
width: 320,
height: 'auto',
position: 'center',
resizable: true,
draggable: true,
// 可以在这里添加特定的 onClose 逻辑
onClose: () => {
console.log('Info dialog closed');
},
onOpen: () => {
console.log('Info dialog opened');
}
});
// 3. 如果有特定于子类的初始化逻辑,可以在 super() 之后执行
// 例如this.element.classList.add('my-special-class');
}
// 不需要再手动实现 setTheme, destroy, close, init
// 它们都已从 BimDialog 继承
}

View File

@@ -513,6 +513,32 @@ export class Engine implements IBimComponent {
} }
} }
/**
* 剖切盒适应(缩放到场景整体包围盒)
* @remarks
* - 对接底层 `engine.clipping.scaleBox()`
* - 会确保当前处于剖切盒模式
*/
public scaleSectionBox(): void {
if (!this._isInitialized || !this.engine?.clipping) {
console.error('[Engine] Cannot scale section box: engine not initialized.');
return;
}
this.engine.clipping.scaleBox();
}
/**
* 反向剖切
* @remarks 对接底层 `engine.clipping.reverse()`
*/
public reverseSection(): void {
if (!this._isInitialized || !this.engine?.clipping) {
console.error('[Engine] Cannot reverse section: engine not initialized.');
return;
}
this.engine.clipping.reverse();
}
// ==================== 结束:剖切功能 ==================== // ==================== 结束:剖切功能 ====================
// ==================== 漫游功能 ==================== // ==================== 漫游功能 ====================
@@ -845,17 +871,60 @@ export class Engine implements IBimComponent {
} }
/** /**
* 隐藏指定模型 * 高亮指定模型构件
* @param models 要隐藏的模型对象 *
* 用于在 3D 场景中高亮显示指定的构件,常用于:
* - 点击构件树节点时高亮对应模型
* - 搜索结果定位
* - 批量选中构件
*
* @param models - 要高亮的模型数组,每个元素包含:
* - url: 模型资源 URL
* - ids: 构件 ID 数组
*
* @example
* engine.highlightModel([
* { url: 'https://xxx/models/xxx/', ids: [350518, 350520] }
* ]);
*/ */
public hideModels(models: any): void { public highlightModel(models: { url: string; ids: number[] }[]): void {
if (!this._isInitialized || !this.engine?.modelToolModule) {
console.warn('[Engine] Cannot highlight model: engine not initialized.');
return;
}
this.engine.modelToolModule.highlightModel(models);
}
public unhighlightAllModels(): void {
if (!this._isInitialized || !this.engine?.modelToolModule) {
console.warn('[Engine] Cannot unhighlight models: engine not initialized.');
return;
}
this.engine.modelToolModule.unhighlightAllModels();
}
public viewScaleToModel(models: { url: string; ids: number[] }[]): void {
if (!this._isInitialized || !this.engine?.modelToolModule) {
console.warn('[Engine] Cannot view scale to model: engine not initialized.');
return;
}
this.engine.modelToolModule.viewScaleToModel(models);
}
public hideModels(models: { url: string; ids: number[] }[]): void {
if (!this._isInitialized || !this.engine?.modelToolModule) { if (!this._isInitialized || !this.engine?.modelToolModule) {
console.warn('[Engine] Cannot hide models: engine not initialized.'); console.warn('[Engine] Cannot hide models: engine not initialized.');
return; return;
} }
if (models) { this.engine.modelToolModule.hideModel(models);
this.engine.modelToolModule.hideModel(models); }
public showModel(models: { url: string; ids: number[] }[]): void {
if (!this._isInitialized || !this.engine?.modelToolModule) {
console.warn('[Engine] Cannot show model: engine not initialized.');
return;
} }
this.engine.modelToolModule.showModel(models);
} }
/** /**
@@ -1004,4 +1073,3 @@ export class Engine implements IBimComponent {
} }
} }

View File

@@ -15,10 +15,12 @@ export interface EngineOptions {
showViewCube?: boolean; showViewCube?: boolean;
} }
/** export interface EngineInfo {
* 模型加载选项 totalVertices: number;
* 用于配置模型的位置、旋转和缩放 totalTriangles: number;
*/ meshCount: number;
}
export interface ModelLoadOptions { export interface ModelLoadOptions {
/** 模型初始位置 [x, y, z] */ /** 模型初始位置 [x, y, z] */
position?: [number, number, number]; position?: [number, number, number];

View File

@@ -1,49 +0,0 @@
import type { ThemeConfig } from '../../themes/types';
import { IBimComponent } from '../../types/component';
import { localeManager } from '../../services/locale';
import { themeManager } from '../../services/theme';
/**
* 地图面板组件
*/
export class MapPanel implements IBimComponent {
public element!: HTMLElement;
private unsubscribeLocale: (() => void) | null = null;
private unsubscribeTheme: (() => void) | null = null;
constructor() {}
public init(): void {
this.element = this.createPanel();
this.unsubscribeLocale = localeManager.subscribe(() => this.setLocales());
this.unsubscribeTheme = themeManager.subscribe((theme) => this.setTheme(theme));
this.setLocales();
this.setTheme(themeManager.getTheme());
}
private createPanel(): HTMLElement {
const panel = document.createElement('div');
panel.className = 'map-panel';
panel.style.padding = '20px';
panel.style.color = '#fff';
panel.textContent = '地图内容待实现';
return panel;
}
public setLocales(): void {
// 更新文本
}
public setTheme(_theme: ThemeConfig): void {
// 应用主题
}
public destroy(): void {
this.unsubscribeLocale?.();
this.unsubscribeTheme?.();
if (this.element && this.element.parentElement) {
this.element.parentElement.removeChild(this.element);
}
}
}

View File

@@ -4,7 +4,7 @@ import { IBimComponent } from '../../types/component';
import { localeManager, t } from '../../services/locale'; import { localeManager, t } from '../../services/locale';
import { themeManager } from '../../services/theme'; import { themeManager } from '../../services/theme';
import type { MeasureConfig, MeasurePanelOptions, MeasurePrecision, MeasureResult, MeasureUnit } from './types'; import type { MeasureConfig, MeasurePanelOptions, MeasurePrecision, MeasureResult, MeasureUnit } from './types';
import { MEASURE_TYPES, MEASURE_MODES_ORDERED, type MeasureMode } from '../../types/measure'; import { MEASURE_TYPES, MEASURE_MODES_ORDERED, getValueType, type MeasureMode, type MeasureValueType } from '../../types/measure';
/** /**
* 测量面板组件(只做 UI不实现真实测量 * 测量面板组件(只做 UI不实现真实测量
@@ -55,6 +55,7 @@ export class MeasurePanel implements IBimComponent {
private toolButtons: Map<MeasureMode, HTMLButtonElement> = new Map(); private toolButtons: Map<MeasureMode, HTMLButtonElement> = new Map();
private toggleBtn!: HTMLButtonElement; private toggleBtn!: HTMLButtonElement;
private toggleTextEl!: HTMLElement; private toggleTextEl!: HTMLElement;
private mainValueRowEl!: HTMLElement;
private mainValueValueEl!: HTMLElement; private mainValueValueEl!: HTMLElement;
private mainValueLabelEl!: HTMLElement; private mainValueLabelEl!: HTMLElement;
private mainNumberEl!: HTMLElement; private mainNumberEl!: HTMLElement;
@@ -428,6 +429,7 @@ export class MeasurePanel implements IBimComponent {
// 主结果值(随模式变化) // 主结果值(随模式变化)
const mainValueRow = document.createElement('div'); const mainValueRow = document.createElement('div');
mainValueRow.className = 'bim-measure-row'; mainValueRow.className = 'bim-measure-row';
this.mainValueRowEl = mainValueRow;
const mainValueLabel = document.createElement('span'); const mainValueLabel = document.createElement('span');
mainValueLabel.className = 'label'; mainValueLabel.className = 'label';
this.mainValueLabelEl = mainValueLabel; this.mainValueLabelEl = mainValueLabel;
@@ -764,7 +766,7 @@ export class MeasurePanel implements IBimComponent {
private renderResult(): void { private renderResult(): void {
// 1) 根据模式决定结果区显示规则 // 1) 根据模式决定结果区显示规则
// 你给的规则: // 你给的规则:
// - 距离:显示数值 + xyz // - 距离:显示数值
// - 最小距离:只显示数值 // - 最小距离:只显示数值
// - 角度:--° // - 角度:--°
// - 标高:--m固定 m // - 标高:--m固定 m
@@ -773,14 +775,10 @@ export class MeasurePanel implements IBimComponent {
// - 坡度:--% // - 坡度:--%
// - 空间体积:--mm³单位随设置变动即 unit³ // - 空间体积:--mm³单位随设置变动即 unit³
this.mainValueLabelEl.style.display = ''; const isPointMode = this.activeMode === 'point';
this.mainValueLabelEl.textContent = t(this.getModeValueLabelI18nKey(this.activeMode));
const parts = this.formatMainValueParts(this.activeMode, this.result);
this.mainNumberEl.textContent = parts.numberText;
this.mainUnitEl.textContent = parts.unitText;
const showXyz = this.activeMode === 'distance' || this.activeMode === 'point'; if (isPointMode) {
if (showXyz) { this.mainValueRowEl.style.display = 'none';
this.xyzBoxEl.style.display = ''; this.xyzBoxEl.style.display = '';
const xyz = this.result?.xyz; const xyz = this.result?.xyz;
if (!xyz) { if (!xyz) {
@@ -789,12 +787,24 @@ export class MeasurePanel implements IBimComponent {
this.xyzZEl.textContent = '--'; this.xyzZEl.textContent = '--';
return; return;
} }
this.xyzXEl.textContent = this.formatNumberWithPrecision(xyz.x, this.config.precision); this.xyzXEl.textContent = this.convertValue('length', xyz.x) + ' ' + this.getUnitText('length');
this.xyzYEl.textContent = this.formatNumberWithPrecision(xyz.y, this.config.precision); this.xyzYEl.textContent = this.convertValue('length', xyz.y) + ' ' + this.getUnitText('length');
this.xyzZEl.textContent = this.formatNumberWithPrecision(xyz.z, this.config.precision); this.xyzZEl.textContent = this.convertValue('length', xyz.z) + ' ' + this.getUnitText('length');
return; return;
} }
this.mainValueRowEl.style.display = '';
this.mainValueLabelEl.textContent = t(this.getModeValueLabelI18nKey(this.activeMode));
const value = this.result ? (this.result as any)[MEASURE_TYPES[this.activeMode].callBackType] : undefined;
if (this.activeMode === 'slope'||this.activeMode === 'angle') {
this.mainNumberEl.textContent = value ?? '--';
this.mainUnitEl.textContent = '';
} else {
const valueType = getValueType(this.activeMode);
this.mainNumberEl.textContent = this.convertValue(valueType, value);
this.mainUnitEl.textContent = this.getUnitText(valueType);
}
this.xyzBoxEl.style.display = 'none'; this.xyzBoxEl.style.display = 'none';
} }
@@ -809,99 +819,69 @@ export class MeasurePanel implements IBimComponent {
return `measure.labels.value.${mode}`; return `measure.labels.value.${mode}`;
} }
// 注意:旧的 formatMainValue/formatWithFixedUnit 已被 formatMainValueParts 替代, /**
// 以支持“数值与单位分色显示”和“无数据时仍展示单位”。 * 统一的数值转换方法
* @param type 测量值类型length(长度)、area(面积)、angle(角度)、percent(百分比)、point(坐标)
* @param value 原始数值(单位:长度为米,面积为平方米)
* @returns 转换后的格式化字符串,无效值返回 '--'
*/
private convertValue(type: MeasureValueType, value: number | undefined | null): string {
if (value === null || value === undefined || Number.isNaN(value)) {
return '--';
}
const unit = this.config.unit;
const precision = this.config.precision;
let converted: number;
switch (type) {
case 'length':
switch (unit) {
case 'mm': converted = value * 1000; break;
case 'cm': converted = value * 100; break;
case 'km': converted = value / 1000; break;
default: converted = value;
}
break;
case 'area':
switch (unit) {
case 'mm': converted = value * 1000 * 1000; break;
case 'cm': converted = value * 100 * 100; break;
case 'km': converted = value / 1000 / 1000; break;
default: converted = value;
}
break;
case 'angle':
case 'percent':
case 'point':
default:
converted = value;
}
return converted.toFixed(precision);
}
/** /**
* 基础数字格式化(按精度显示) * 获取单位文本
* @param type 测量值类型
* @returns 对应的单位文本(如 mm、m²、°
*/ */
private formatNumberWithPrecision(value: number, precision: MeasurePrecision): string { private getUnitText(type: MeasureValueType): string {
// 你要求精度可选0 / 0.0 / 0.00 / 0.000,因此这里不做 trim严格按 toFixed 输出
return value.toFixed(precision);
}
// 注意:旧的 formatLengthWithConfig 已被 formatLengthParts 替代。
private getUnitI18nKey(unit: MeasureUnit): string {
return `measure.units.${unit}`;
}
private formatMainValueParts(mode: MeasureMode, result: MeasureResult | null): { numberText: string; unitText: string } {
if (!result) {
return this.getEmptyValuePartsByMode(mode);
}
const config = MEASURE_TYPES[mode];
const value = (result as any)[config.resultField];
switch (config.valueType) {
case 'length':
case 'area':
return this.formatMeasureValue(value, config.valueType);
case 'angle':
return this.formatFixedUnitParts(value, t('measure.units.deg'));
case 'percent':
return this.formatFixedUnitParts(value, t('measure.units.percent'));
case 'point':
return { numberText: '--', unitText: '' };
default:
return { numberText: '--', unitText: '' };
}
}
private getEmptyValuePartsByMode(mode: MeasureMode): { numberText: string; unitText: string } {
const config = MEASURE_TYPES[mode];
switch (config.valueType) {
case 'length':
return { numberText: '--', unitText: t(this.getUnitI18nKey(this.config.unit)) };
case 'area':
return { numberText: '--', unitText: `${this.config.unit}²` };
case 'angle':
return { numberText: '--', unitText: t('measure.units.deg') };
case 'percent':
return { numberText: '--', unitText: t('measure.units.percent') };
case 'point':
return { numberText: '--', unitText: '' };
default:
return { numberText: '--', unitText: '' };
}
}
private formatFixedUnitParts(value: number | undefined, unitText: string): { numberText: string; unitText: string } {
if (value === null || value === undefined || Number.isNaN(value)) {
return { numberText: '--', unitText };
}
return { numberText: this.formatNumberWithPrecision(value, this.config.precision), unitText };
}
private formatMeasureValue(value: number | undefined, type: 'length' | 'area'): { numberText: string; unitText: string } {
const unit = this.config.unit; const unit = this.config.unit;
const unitText = type === 'area' ? `${unit}²` : t(this.getUnitI18nKey(unit)); switch (type) {
case 'length':
if (value === null || value === undefined || Number.isNaN(value)) { case 'point':
return { numberText: '--', unitText }; return unit;
case 'area':
return `${unit}²`;
case 'angle':
case 'percent':
return '°';
default:
return '';
} }
let converted: number;
if (type === 'length') {
switch (unit) {
case 'mm': converted = value * 1000; break;
case 'cm': converted = value * 100; break;
case 'km': converted = value / 1000; break;
default: converted = value;
}
} else {
switch (unit) {
case 'mm': converted = value * 1000 * 1000; break;
case 'cm': converted = value * 100 * 100; break;
case 'km': converted = value / 1000 / 1000; break;
default: converted = value;
}
}
return { numberText: converted.toFixed(this.config.precision), unitText };
} }
} }

View File

@@ -7,9 +7,8 @@ export const fourMenuButton = (): MenuItemConfig => {
label: 'menu.info', label: 'menu.info',
icon: '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M12 7q.425 0 .713-.288T13 6t-.288-.712T12 5t-.712.288T11 6t.288.713T12 7m0 8q.425 0 .713-.288T13 14v-4q0-.425-.288-.712T12 9t-.712.288T11 10v4q0 .425.288.713T12 15m-6 3l-2.3 2.3q-.475.475-1.088.213T2 19.575V4q0-.825.588-1.412T4 2h16q.825 0 1.413.588T22 4v12q0 .825-.587 1.413T20 18z"/></svg>', icon: '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M12 7q.425 0 .713-.288T13 6t-.288-.712T12 5t-.712.288T11 6t.288.713T12 7m0 8q.425 0 .713-.288T13 14v-4q0-.425-.288-.712T12 9t-.712.288T11 10v4q0 .425.288.713T12 15m-6 3l-2.3 2.3q-.475.475-1.088.213T2 19.575V4q0-.825.588-1.412T4 2h16q.825 0 1.413.588T22 4v12q0 .825-.587 1.413T20 18z"/></svg>',
onClick: () => { onClick: () => {
console.log('dianjile');
const registry = ManagerRegistry.getInstance(); const registry = ManagerRegistry.getInstance();
registry.dialog?.showInfoDialog(); registry.engineInfo?.show();
registry.engine3d?.rightKey?.hide(); registry.engine3d?.rightKey?.hide();
} }
}; };

View File

@@ -12,7 +12,7 @@ export const homeMenuButton = (): MenuItemConfig => {
icon: '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M12 7q.425 0 .713-.288T13 6t-.288-.712T12 5t-.712.288T11 6t.288.713T12 7m0 8q.425 0 .713-.288T13 14v-4q0-.425-.288-.712T12 9t-.712.288T11 10v4q0 .425.288.713T12 15m-6 3l-2.3 2.3q-.475.475-1.088.213T2 19.575V4q0-.825.588-1.412T4 2h16q.825 0 1.413.588T22 4v12q0 .825-.587 1.413T20 18z"/></svg>', icon: '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M12 7q.425 0 .713-.288T13 6t-.288-.712T12 5t-.712.288T11 6t.288.713T12 7m0 8q.425 0 .713-.288T13 14v-4q0-.425-.288-.712T12 9t-.712.288T11 10v4q0 .425.288.713T12 15m-6 3l-2.3 2.3q-.475.475-1.088.213T2 19.575V4q0-.825.588-1.412T4 2h16q.825 0 1.413.588T22 4v12q0 .825-.587 1.413T20 18z"/></svg>',
onClick: () => { onClick: () => {
const registry = ManagerRegistry.getInstance(); const registry = ManagerRegistry.getInstance();
registry.dialog?.showInfoDialog(); registry.engineInfo?.show();
registry.engine3d?.rightKey?.hide(); registry.engine3d?.rightKey?.hide();
} }
}; };

View File

@@ -8,9 +8,8 @@ export const infoMenuButton = (): MenuItemConfig => {
group: 'info', group: 'info',
icon: '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M12 7q.425 0 .713-.288T13 6t-.288-.712T12 5t-.712.288T11 6t.288.713T12 7m0 8q.425 0 .713-.288T13 14v-4q0-.425-.288-.712T12 9t-.712.288T11 10v4q0 .425.288.713T12 15m-6 3l-2.3 2.3q-.475.475-1.088.213T2 19.575V4q0-.825.588-1.412T4 2h16q.825 0 1.413.588T22 4v12q0 .825-.587 1.413T20 18z"/></svg>', icon: '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M12 7q.425 0 .713-.288T13 6t-.288-.712T12 5t-.712.288T11 6t.288.713T12 7m0 8q.425 0 .713-.288T13 14v-4q0-.425-.288-.712T12 9t-.712.288T11 10v4q0 .425.288.713T12 15m-6 3l-2.3 2.3q-.475.475-1.088.213T2 19.575V4q0-.825.588-1.412T4 2h16q.825 0 1.413.588T22 4v12q0 .825-.587 1.413T20 18z"/></svg>',
onClick: () => { onClick: () => {
console.log('dianjile');
const registry = ManagerRegistry.getInstance(); const registry = ManagerRegistry.getInstance();
registry.dialog?.showInfoDialog(); registry.engineInfo?.show();
registry.engine3d?.rightKey?.hide(); registry.engine3d?.rightKey?.hide();
} }
}; };

View File

@@ -7,9 +7,8 @@ export const secondMenuButton = (): MenuItemConfig => {
label: 'menu.info', label: 'menu.info',
icon: '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M12 7q.425 0 .713-.288T13 6t-.288-.712T12 5t-.712.288T11 6t.288.713T12 7m0 8q.425 0 .713-.288T13 14v-4q0-.425-.288-.712T12 9t-.712.288T11 10v4q0 .425.288.713T12 15m-6 3l-2.3 2.3q-.475.475-1.088.213T2 19.575V4q0-.825.588-1.412T4 2h16q.825 0 1.413.588T22 4v12q0 .825-.587 1.413T20 18z"/></svg>', icon: '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M12 7q.425 0 .713-.288T13 6t-.288-.712T12 5t-.712.288T11 6t.288.713T12 7m0 8q.425 0 .713-.288T13 14v-4q0-.425-.288-.712T12 9t-.712.288T11 10v4q0 .425.288.713T12 15m-6 3l-2.3 2.3q-.475.475-1.088.213T2 19.575V4q0-.825.588-1.412T4 2h16q.825 0 1.413.588T22 4v12q0 .825-.587 1.413T20 18z"/></svg>',
onClick: () => { onClick: () => {
console.log('dianjile');
const registry = ManagerRegistry.getInstance(); const registry = ManagerRegistry.getInstance();
registry.dialog?.showInfoDialog(); registry.engineInfo?.show();
registry.engine3d?.rightKey?.hide(); registry.engine3d?.rightKey?.hide();
} }
}; };

View File

@@ -31,6 +31,7 @@ export class BimTree implements IBimComponent {
// 事件回调 (由 Manager 注入) // 事件回调 (由 Manager 注入)
public onNodeCheck?: (node: BimTreeNode) => void; public onNodeCheck?: (node: BimTreeNode) => void;
public onNodeSelect?: (node: BimTreeNode) => void; public onNodeSelect?: (node: BimTreeNode) => void;
public onNodeDeselect?: (node: BimTreeNode) => void;
public onNodeExpand?: (node: BimTreeNode) => void; public onNodeExpand?: (node: BimTreeNode) => void;
constructor(options: TreeOptions) { constructor(options: TreeOptions) {
@@ -61,6 +62,7 @@ export class BimTree implements IBimComponent {
// 初始化回调 // 初始化回调
if (options.onNodeCheck) this.onNodeCheck = options.onNodeCheck; if (options.onNodeCheck) this.onNodeCheck = options.onNodeCheck;
if (options.onNodeSelect) this.onNodeSelect = options.onNodeSelect; if (options.onNodeSelect) this.onNodeSelect = options.onNodeSelect;
if (options.onNodeDeselect) this.onNodeDeselect = options.onNodeDeselect;
if (options.onNodeExpand) this.onNodeExpand = options.onNodeExpand; if (options.onNodeExpand) this.onNodeExpand = options.onNodeExpand;
} }
@@ -333,18 +335,26 @@ export class BimTree implements IBimComponent {
/** /**
* 处理节点选择 (高亮) * 处理节点选择 (高亮)
* 点击已选中节点时切换为取消选中
*/ */
private handleNodeSelect(node: BimTreeNode) { private handleNodeSelect(node: BimTreeNode) {
// 如果之前有选中的,先取消选中 // 再次点击已选中的节点 → 取消选中
if (this.selectedNode && this.selectedNode !== node) { if (this.selectedNode === node) {
node.setSelected(false);
this.selectedNode = null;
if (this.onNodeDeselect) this.onNodeDeselect(node);
return;
}
// 取消之前选中的节点
if (this.selectedNode) {
this.selectedNode.setSelected(false); this.selectedNode.setSelected(false);
} }
// 设置当前为选中 // 选中当前节点
node.setSelected(true); node.setSelected(true);
this.selectedNode = node; this.selectedNode = node;
// 触发外部回调
if (this.onNodeSelect) this.onNodeSelect(node); if (this.onNodeSelect) this.onNodeSelect(node);
} }
@@ -421,6 +431,11 @@ export class BimTree implements IBimComponent {
this.nodeMap.forEach(node => node.toggleExpand(expanded)); this.nodeMap.forEach(node => node.toggleExpand(expanded));
} }
public checkAllNodes(checked: boolean): void {
const state = checked ? TreeNodeCheckState.Checked : TreeNodeCheckState.Unchecked;
this.nodeMap.forEach(node => node.setChecked(state, false, true));
}
public getCheckedNodes(includeHalfChecked: boolean = false): TreeNodeConfig[] { public getCheckedNodes(includeHalfChecked: boolean = false): TreeNodeConfig[] {
const result: TreeNodeConfig[] = []; const result: TreeNodeConfig[] = [];
this.nodeMap.forEach(node => { this.nodeMap.forEach(node => {

View File

@@ -208,13 +208,8 @@ export class BimTreeNode {
this.setChecked(newChecked ? TreeNodeCheckState.Checked : TreeNodeCheckState.Unchecked, true); this.setChecked(newChecked ? TreeNodeCheckState.Checked : TreeNodeCheckState.Unchecked, true);
} }
/** public setChecked(state: TreeNodeCheckState, fireEvent: boolean = false, force: boolean = false) {
* 设置选中状态 (API调用或联动) if (!force && this.checkState === state) return;
* @param state 新状态
* @param fireEvent 是否触发事件
*/
public setChecked(state: TreeNodeCheckState, fireEvent: boolean = false) {
if (this.checkState === state) return;
this.checkState = state; this.checkState = state;
this.config.checked = (state === TreeNodeCheckState.Checked); this.config.checked = (state === TreeNodeCheckState.Checked);

View File

@@ -84,6 +84,9 @@ export interface TreeOptions {
/** 节点选择回调 */ /** 节点选择回调 */
onNodeSelect?: (node: BimTreeNode) => void; onNodeSelect?: (node: BimTreeNode) => void;
/** 节点取消选择回调(再次点击已选中节点时触发) */
onNodeDeselect?: (node: BimTreeNode) => void;
/** 节点展开/折叠回调 */ /** 节点展开/折叠回调 */
onNodeExpand?: (node: BimTreeNode) => void; onNodeExpand?: (node: BimTreeNode) => void;

View File

@@ -24,7 +24,7 @@ export class WalkControlPanel implements IBimComponent {
// DOM 引用 - 左侧按钮 // DOM 引用 - 左侧按钮
private planViewBtn!: HTMLButtonElement; private planViewBtn!: HTMLButtonElement;
private pathModeBtn!: HTMLButtonElement; private pathModeBtn!: HTMLButtonElement;
private walkModeBtn!: HTMLButtonElement; // private walkModeBtn!: HTMLButtonElement;
// DOM 引用 - 中间设置区 // DOM 引用 - 中间设置区
private settingsContainer!: HTMLElement; private settingsContainer!: HTMLElement;
@@ -138,15 +138,15 @@ export class WalkControlPanel implements IBimComponent {
this.options.onPathModeToggle?.(newMode === 'path'); this.options.onPathModeToggle?.(newMode === 'path');
}); });
this.walkModeBtn = this.createIconButton('walk', () => { // this.walkModeBtn = this.createIconButton('walk', () => {
const newMode: WalkControlMode = this.state.mode === 'walk' ? 'none' : 'walk'; // const newMode: WalkControlMode = this.state.mode === 'walk' ? 'none' : 'walk';
this.setMode(newMode); // this.setMode(newMode);
this.options.onWalkModeToggle?.(newMode === 'walk'); // this.options.onWalkModeToggle?.(newMode === 'walk');
}); // });
container.appendChild(this.planViewBtn); container.appendChild(this.planViewBtn);
container.appendChild(this.pathModeBtn); container.appendChild(this.pathModeBtn);
container.appendChild(this.walkModeBtn); // container.appendChild(this.walkModeBtn);
return container; return container;
} }
@@ -339,7 +339,7 @@ export class WalkControlPanel implements IBimComponent {
this.pathModeBtn.classList.toggle('active', this.state.mode === 'path'); this.pathModeBtn.classList.toggle('active', this.state.mode === 'path');
// 漫游按钮 // 漫游按钮
this.walkModeBtn.classList.toggle('active', this.state.mode === 'walk'); // this.walkModeBtn.classList.toggle('active', this.state.mode === 'walk');
} }
private updateSettingsView(): void { private updateSettingsView(): void {

View File

@@ -0,0 +1,253 @@
/* 路径漫游面板根容器 */
.walk-path-panel {
padding: 16px;
box-sizing: border-box;
}
/* ==================== 按钮样式 ==================== */
/* 按钮通用样式 */
.walk-path-btn {
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
box-sizing: border-box;
}
/* 按钮组 */
.walk-path-btn-group {
display: flex;
gap: 8px;
margin-top: 16px;
}
/* 播放按钮 */
.walk-path-btn-play {
flex: 1;
height: 32px;
padding: 0 16px;
background: var(--bim-primary, #3b82f6);
color: #fff;
}
.walk-path-btn-play:hover:not(:disabled) {
opacity: 0.9;
}
.walk-path-btn-play:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* 停止按钮 */
.walk-path-btn-stop {
flex: 1;
height: 32px;
padding: 0 16px;
background: #ef4444;
color: #fff;
}
.walk-path-btn-stop:hover:not(:disabled) {
opacity: 0.9;
}
.walk-path-btn-stop:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* 小按钮 */
.walk-path-btn-small {
height: 28px;
padding: 0 12px;
font-size: 12px;
background: var(--bim-bg-elevated, #1f2d3e);
color: var(--bim-text-primary, #fff);
border: 1px solid var(--bim-border-default, #334155);
}
.walk-path-btn-small:hover {
background: var(--bim-border-default, #334155);
}
/* 危险按钮(删除) */
.walk-path-btn-danger {
color: #ef4444;
}
/* ==================== 表单样式 ==================== */
/* 设置区域 */
.walk-path-settings {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 16px;
}
/* 表单组 */
.walk-path-form-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.walk-path-form-group label {
font-size: 12px;
color: var(--bim-text-secondary, #94a3b8);
}
/* 行内表单组(复选框) */
.walk-path-form-group-inline {
flex-direction: row;
align-items: center;
}
/* 输入框 */
.walk-path-input {
padding: 8px 12px;
border: 1px solid var(--bim-border-default, #334155);
border-radius: 4px;
background: var(--bim-bg-elevated, #1f2d3e);
color: var(--bim-text-primary, #fff);
font-size: 14px;
width: 100%;
box-sizing: border-box;
}
.walk-path-input:focus {
outline: none;
border-color: var(--bim-primary, #3b82f6);
}
/* 输入框包装器(带单位) */
.walk-path-input-wrapper {
display: flex;
align-items: center;
gap: 8px;
}
.walk-path-input-wrapper .walk-path-input {
flex: 1;
}
/* 单位文本 */
.walk-path-unit {
color: var(--bim-text-secondary, #94a3b8);
font-size: 14px;
}
/* 复选框 */
.walk-path-checkbox {
width: 16px;
height: 16px;
margin-right: 8px;
}
/* ==================== 漫游点区域 ==================== */
/* 漫游点区域容器 */
.walk-path-points-section {
margin-bottom: 16px;
}
/* 操作栏 */
.walk-path-points-toolbar {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
/* 漫游点列表 */
.walk-path-points-list {
height: 200px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 8px;
}
/* 空状态提示 */
.walk-path-empty {
text-align: center;
padding: 32px 16px;
color: var(--bim-text-secondary, #94a3b8);
font-size: 14px;
}
/* ==================== 漫游点项 ==================== */
/* 漫游点项容器 */
.walk-path-point-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 12px;
background: var(--bim-bg-elevated, #1f2d3e);
border-radius: 4px;
transition: all 0.2s;
}
.walk-path-point-item:hover {
background: var(--bim-border-default, #334155);
}
/* 悬浮时显示操作按钮 */
.walk-path-point-item:hover .walk-path-point-actions {
opacity: 1;
}
/* 播放中的点高亮样式 */
.walk-path-point-item-active {
background: var(--bim-primary, #3b82f6);
}
.walk-path-point-item-active .walk-path-point-name {
color: #fff;
}
.walk-path-point-item-active .walk-path-point-actions {
opacity: 1;
}
/* 漫游点名称 */
.walk-path-point-name {
font-size: 14px;
color: var(--bim-text-primary, #fff);
}
/* 操作按钮容器 */
.walk-path-point-actions {
display: flex;
gap: 8px;
opacity: 0;
transition: opacity 0.2s;
}
/* 图标按钮 */
.walk-path-btn-icon {
width: 28px;
height: 28px;
border: none;
border-radius: 4px;
background: transparent;
color: var(--bim-text-primary, #fff);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
}
.walk-path-btn-icon:hover {
background: rgba(255, 255, 255, 0.1);
}
/* 危险图标按钮 */
.walk-path-btn-icon-danger:hover {
background: rgba(239, 68, 68, 0.2);
color: #ef4444;
}

View File

@@ -1,53 +1,408 @@
import './index.css';
import type { ThemeConfig } from '../../themes/types'; import type { ThemeConfig } from '../../themes/types';
import { IBimComponent } from '../../types/component'; import { IBimComponent } from '../../types/component';
import { localeManager } from '../../services/locale'; import { localeManager, t } from '../../services/locale';
import { themeManager } from '../../services/theme'; import { themeManager } from '../../services/theme';
import { ManagerRegistry } from '../../core/manager-registry';
import type { RoamingPoint } from './types';
/** /**
* 路径漫游面板组件(暂时空内容) * 路径漫游面板组件
* 提供漫游点的添加、删除、跳转和播放功能
*/ */
export class WalkPathPanel implements IBimComponent { export class WalkPathPanel implements IBimComponent {
public element!: HTMLElement; public element!: HTMLElement;
/** 管理器注册表实例 */
private registry = ManagerRegistry.getInstance();
/** 国际化订阅取消函数 */
private unsubscribeLocale: (() => void) | null = null; private unsubscribeLocale: (() => void) | null = null;
/** 主题订阅取消函数 */
private unsubscribeTheme: (() => void) | null = null; private unsubscribeTheme: (() => void) | null = null;
constructor() { /** 漫游点列表 */
// 暂时无配置 private points: RoamingPoint[] = [];
} /** 漫游时间(毫秒) */
private duration: number = 10000;
/** 是否循环播放 */
private loop: boolean = false;
/** 当前播放中的点索引,-1 表示未播放 */
private playingPointIndex: number = -1;
/** 是否正在播放 */
private isPlaying: boolean = false;
/**
* 初始化组件
* 创建 DOM 元素,订阅国际化和主题变化,加载已有漫游点
*/
public init(): void { public init(): void {
this.element = this.createPanel(); // 创建根元素
this.element = document.createElement('div');
this.element.className = 'walk-path-panel';
// 订阅 // 订阅国际化变化
this.unsubscribeLocale = localeManager.subscribe(() => this.setLocales()); this.unsubscribeLocale = localeManager.subscribe(() => this.render());
// 订阅主题变化
this.unsubscribeTheme = themeManager.subscribe((theme) => this.setTheme(theme)); this.unsubscribeTheme = themeManager.subscribe((theme) => this.setTheme(theme));
this.setLocales(); // 从引擎加载已有的漫游点
this.loadPointsFromEngine();
// 渲染界面
this.render();
// 应用当前主题
this.setTheme(themeManager.getTheme()); this.setTheme(themeManager.getTheme());
} }
private createPanel(): HTMLElement { /**
const panel = document.createElement('div'); * 从引擎加载已有的漫游点
panel.className = 'walk-path-panel'; * 在面板打开时调用,获取底层引擎中已存在的漫游点
panel.style.padding = '20px'; */
panel.style.color = 'var(--bim-text-color, #fff)'; private loadPointsFromEngine(): void {
panel.textContent = '路径漫游内容待实现'; const enginePoints = this.registry.engine3d?.pathRoamingGetPoints() ?? [];
return panel; // 将引擎返回的点转换为本地格式
this.points = enginePoints.map((_: any, index: number) => ({
index: index
}));
} }
/**
* 渲染整个面板
* 清空现有内容并重新构建界面
*/
private render(): void {
this.element.innerHTML = '';
// 渲染路径设置区域
const settings = this.createSettingsSection();
this.element.appendChild(settings);
// 渲染漫游点区域
const pointsSection = this.createPointsSection();
this.element.appendChild(pointsSection);
// 渲染播放按钮
const playBtn = this.createPlayButton();
this.element.appendChild(playBtn);
}
/**
* 创建路径设置区域
* 包含漫游时间和循环播放设置
*/
private createSettingsSection(): HTMLElement {
const section = document.createElement('div');
section.className = 'walk-path-settings';
// ===== 漫游时间 =====
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';
durationInput.value = String(this.duration / 1000);
durationInput.min = '1';
durationInput.oninput = (e) => {
// 更新漫游时间,转换为毫秒
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);
durationGroup.appendChild(durationWrapper);
// ===== 循环播放 =====
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';
loopCheckbox.className = 'walk-path-checkbox';
loopCheckbox.checked = this.loop;
loopCheckbox.onchange = (e) => {
// 更新循环播放状态
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);
section.appendChild(durationGroup);
section.appendChild(loopGroup);
return section;
}
/**
* 创建漫游点区域
* 包含操作按钮和漫游点列表
*/
private createPointsSection(): HTMLElement {
const section = document.createElement('div');
section.className = 'walk-path-points-section';
// ===== 操作栏 =====
const toolbar = document.createElement('div');
toolbar.className = 'walk-path-points-toolbar';
// 添加漫游点按钮
const addBtn = document.createElement('button');
addBtn.className = 'walk-path-btn walk-path-btn-small';
addBtn.textContent = `+ ${t('walkControl.path.addPoint')}`;
addBtn.onclick = () => this.addPoint();
// 删除全部按钮
const deleteAllBtn = document.createElement('button');
deleteAllBtn.className = 'walk-path-btn walk-path-btn-small walk-path-btn-danger';
deleteAllBtn.textContent = t('walkControl.path.deleteAll');
deleteAllBtn.onclick = () => this.deleteAllPoints();
toolbar.appendChild(addBtn);
toolbar.appendChild(deleteAllBtn);
section.appendChild(toolbar);
// ===== 漫游点列表 =====
const list = document.createElement('div');
list.className = 'walk-path-points-list';
if (this.points.length === 0) {
// 空状态提示
const empty = document.createElement('div');
empty.className = 'walk-path-empty';
empty.textContent = t('walkControl.path.noPoints');
list.appendChild(empty);
} else {
// 渲染每个漫游点
this.points.forEach((_, index) => {
const item = this.createPointItem(index);
list.appendChild(item);
});
}
section.appendChild(list);
return section;
}
/**
* 创建单个漫游点项
* @param index 漫游点索引
*/
private createPointItem(index: number): HTMLElement {
const item = document.createElement('div');
item.className = 'walk-path-point-item';
// 如果是当前播放的点,添加高亮样式
if (this.playingPointIndex === index) {
item.classList.add('walk-path-point-item-active');
}
// 漫游点名称
const name = document.createElement('span');
name.className = 'walk-path-point-name';
name.textContent = `${t('walkControl.path.point')}${index}`;
// 操作按钮容器
const actions = document.createElement('div');
actions.className = 'walk-path-point-actions';
// 跳转按钮
const jumpBtn = document.createElement('button');
jumpBtn.className = 'walk-path-btn-icon';
jumpBtn.innerHTML = '▶';
jumpBtn.title = t('walkControl.path.play');
jumpBtn.onclick = (e) => {
e.stopPropagation();
this.jumpToPoint(index);
};
// 删除按钮
const deleteBtn = document.createElement('button');
deleteBtn.className = 'walk-path-btn-icon walk-path-btn-icon-danger';
deleteBtn.innerHTML = '×';
deleteBtn.onclick = (e) => {
e.stopPropagation();
this.deletePoint(index);
};
actions.appendChild(jumpBtn);
actions.appendChild(deleteBtn);
item.appendChild(name);
item.appendChild(actions);
return item;
}
/**
* 创建播放/停止按钮组
*/
private createPlayButton(): HTMLElement {
const wrapper = document.createElement('div');
wrapper.className = 'walk-path-btn-group';
const playBtn = document.createElement('button');
playBtn.className = 'walk-path-btn walk-path-btn-play';
playBtn.textContent = `${t('walkControl.path.play')}`;
playBtn.disabled = this.points.length === 0 || this.isPlaying;
playBtn.onclick = () => this.playPath();
const stopBtn = document.createElement('button');
stopBtn.className = 'walk-path-btn walk-path-btn-stop';
stopBtn.textContent = `${t('walkControl.path.stop')}`;
stopBtn.disabled = !this.isPlaying;
stopBtn.onclick = () => this.stopPath();
wrapper.appendChild(playBtn);
wrapper.appendChild(stopBtn);
return wrapper;
}
// ==================== 操作方法 ====================
/**
* 添加漫游点
* 将当前相机位置添加为新的漫游点
*/
private addPoint(): void {
// 调用引擎添加漫游点
this.registry.engine3d?.pathRoamingAddPoint();
// 更新本地状态
const newIndex = this.points.length;
this.points.push({ index: newIndex });
// 重新渲染
this.render();
}
/**
* 删除指定漫游点
* @param index 要删除的漫游点索引
*/
private deletePoint(index: number): void {
// 调用引擎删除漫游点
this.registry.engine3d?.pathRoamingRemovePoint(index);
// 从本地列表中移除
this.points.splice(index, 1);
// 重新索引
this.points.forEach((p, i) => p.index = i);
// 重新渲染
this.render();
}
/**
* 删除所有漫游点
*/
private deleteAllPoints(): void {
// 调用引擎清除所有漫游点
this.registry.engine3d?.pathRoamingClearPoints();
// 清空本地列表
this.points = [];
// 重新渲染
this.render();
}
/**
* 跳转到指定漫游点
* @param index 目标漫游点索引
*/
private jumpToPoint(index: number): void {
this.registry.engine3d?.pathRoamingJumpToPoint(index);
}
/**
* 播放漫游
* 按顺序播放所有漫游点
*/
private playPath(): void {
if (this.points.length === 0) return;
this.isPlaying = true;
this.render();
console.log('[WalkPathPanel] 开始播放漫游', { duration: this.duration, loop: this.loop, pointsCount: this.points.length });
this.registry.engine3d?.pathRoamingPlay({
duration: this.duration,
loop: this.loop,
onPointComplete: (pointIndex: number) => {
console.log('[WalkPathPanel] onPointComplete', { pointIndex });
this.playingPointIndex = pointIndex;
this.render();
},
onComplete: () => {
console.log('[WalkPathPanel] onComplete 播放完成');
this.isPlaying = false;
this.playingPointIndex = -1;
this.render();
}
});
}
/**
* 停止漫游
*/
private stopPath(): void {
console.log('[WalkPathPanel] 停止漫游');
this.registry.engine3d?.pathRoamingStop();
this.isPlaying = false;
this.playingPointIndex = -1;
this.render();
}
// ==================== 生命周期 ====================
/**
* 更新国际化文本
* 当语言切换时重新渲染整个面板
*/
public setLocales(): void { public setLocales(): void {
// 更新文本 // 重新渲染以更新所有文本
this.render();
} }
public setTheme(_theme: ThemeConfig): void { /**
// 应用主题 * 应用主题
* @param theme 主题配置
*/
public setTheme(theme: ThemeConfig): void {
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-bg-elevated', theme.bgElevated ?? '#1f2d3e');
this.element.style.setProperty('--bim-border-default', theme.borderDefault ?? '#334155');
this.element.style.setProperty('--bim-primary', theme.primary ?? '#3b82f6');
} }
/**
* 销毁组件
* 清理订阅和 DOM 元素
*/
public destroy(): void { public destroy(): void {
// 如果正在播放,先停止
if (this.isPlaying) {
this.stopPath();
}
// 取消订阅
this.unsubscribeLocale?.(); this.unsubscribeLocale?.();
this.unsubscribeTheme?.(); this.unsubscribeTheme?.();
if (this.element && this.element.parentElement) { // 移除 DOM 元素
if (this.element?.parentElement) {
this.element.parentElement.removeChild(this.element); this.element.parentElement.removeChild(this.element);
} }
} }

View File

@@ -0,0 +1,23 @@
/**
* 漫游点接口
* 表示路径中的一个漫游点
*/
export interface RoamingPoint {
/** 漫游点索引 */
index: number;
}
/**
* 播放选项接口
* 配置漫游播放的参数
*/
export interface PlayOptions {
/** 总播放时长(毫秒),不包括停留时间 */
duration?: number;
/** 是否循环播放 */
loop?: boolean;
/** 播放完成的回调 */
onComplete?: () => void;
/** 每个点播放完成的回调,用于高亮当前点 */
onPointComplete?: (pointIndex: number) => void;
}

View File

@@ -14,12 +14,12 @@ import type { ConstructTreeManagerBtn } from '../managers/construct-tree-manager
import type { MeasureDialogManager } from '../managers/measure-dialog-manager'; import type { MeasureDialogManager } from '../managers/measure-dialog-manager';
import type { WalkControlManager } from '../managers/walk-control-manager'; import type { WalkControlManager } from '../managers/walk-control-manager';
import type { MapDialogManager } from '../managers/map-dialog-manager';
import type { SectionPlaneDialogManager } from '../managers/section-plane-dialog-manager'; import type { SectionPlaneDialogManager } from '../managers/section-plane-dialog-manager';
import type { SectionAxisDialogManager } from '../managers/section-axis-dialog-manager'; import type { SectionAxisDialogManager } from '../managers/section-axis-dialog-manager';
import type { SectionBoxDialogManager } from '../managers/section-box-dialog-manager'; import type { SectionBoxDialogManager } from '../managers/section-box-dialog-manager';
import type { WalkPathDialogManager } from '../managers/walk-path-dialog-manager'; import type { WalkPathDialogManager } from '../managers/walk-path-dialog-manager';
import type { WalkPlanViewDialogManager } from '../managers/walk-plan-view-dialog-manager'; import type { WalkPlanViewDialogManager } from '../managers/walk-plan-view-dialog-manager';
import type { EngineInfoDialogManager } from '../managers/engine-info-dialog-manager';
import type { ComponentDetailManager } from '../managers/component-detail-manager'; import type { ComponentDetailManager } from '../managers/component-detail-manager';
import type { AiChatManager } from '../managers/ai-chat-manager'; import type { AiChatManager } from '../managers/ai-chat-manager';
@@ -55,8 +55,6 @@ export class ManagerRegistry {
public measure: MeasureDialogManager | null = null; public measure: MeasureDialogManager | null = null;
/** 漫游控制管理器 */ /** 漫游控制管理器 */
public walkControl: WalkControlManager | null = null; public walkControl: WalkControlManager | null = null;
/** 地图对话框管理器 */
public map: MapDialogManager | null = null;
/** 拾取面剖切对话框管理器 */ /** 拾取面剖切对话框管理器 */
public sectionPlane: SectionPlaneDialogManager | null = null; public sectionPlane: SectionPlaneDialogManager | null = null;
/** 轴向剖切对话框管理器 */ /** 轴向剖切对话框管理器 */
@@ -67,6 +65,8 @@ export class ManagerRegistry {
public walkPath: WalkPathDialogManager | null = null; public walkPath: WalkPathDialogManager | null = null;
/** 漫游平面图对话框管理器 */ /** 漫游平面图对话框管理器 */
public walkPlanView: WalkPlanViewDialogManager | null = null; public walkPlanView: WalkPlanViewDialogManager | null = null;
/** 引擎信息对话框管理器 */
public engineInfo: EngineInfoDialogManager | null = null;
/** 构件详情管理器 */ /** 构件详情管理器 */
public componentDetail: ComponentDetailManager | null = null; public componentDetail: ComponentDetailManager | null = null;
/** AI 聊天管理器 */ /** AI 聊天管理器 */

View File

@@ -1,64 +1,257 @@
/**
* @file construct-tree-manager-btn.ts
* @description 构件树管理器 - 负责管理构件树按钮和对话框
*
* 功能概述:
* 1. 在界面左上角显示构件树按钮
* 2. 点击按钮打开构件树对话框,包含三个选项卡:楼层树、类型树、专业树
* 3. 点击树节点时,如果节点有 ids则高亮对应的 3D 模型构件
*
* 数据流:
* 1. 从 3D 引擎获取原始树数据 (getLevelTreeData/getTypeTreeData/getMajorTreeData)
* 2. 通过 transformTreeData 转换为 SDK 标准的 TreeNodeConfig 格式
* 3. 创建 BimTree 组件渲染树结构
* 4. 用户点击节点时,通过 onNodeSelect 回调触发模型高亮
*/
import type { ButtonGroupColors, ButtonConfig } from '../components/button-group/index.type'; import type { ButtonGroupColors, ButtonConfig } from '../components/button-group/index.type';
import { BaseManager } from '../core/base-manager'; import { BaseManager } from '../core/base-manager';
import { BimButtonGroup } from '../components/button-group'; import { BimButtonGroup } from '../components/button-group';
import { BimTree } from '../components/tree'; import { BimTree } from '../components/tree';
import { TreeNodeConfig } from '../components/tree/types'; import { TreeNodeConfig, TreeNodeCheckState } from '../components/tree/types';
import type { BimTreeNode } from '../components/tree/tree-node';
import { BimDialog } from '../components/dialog'; import { BimDialog } from '../components/dialog';
import { BimTab } from '../components/tab'; import { BimTab } from '../components/tab';
import { getIcon } from '../utils/icon-manager'; import { getIcon } from '../utils/icon-manager';
function transformTreeData(apiData: any[]): TreeNodeConfig[] { // ============================================================================
if (!apiData || apiData.length === 0) return []; // 类型定义
// ============================================================================
return apiData.map((model, modelIndex) => { /**
const transformNode = (node: any, index: number): TreeNodeConfig => { * 3D 引擎返回的原始树节点数据结构
const hasChildren = node.children && node.children.length > 0; *
return { * @example
id: node.id || `node-${modelIndex}-${index}`, * {
label: node.name || node.label || '未命名', * name: "标高 1",
expanded: false, * id: null,
clickAction: hasChildren ? 'expand' : 'select', * ids: ["350518", "350520", ...], // 构件 ID 数组,用于高亮模型
children: hasChildren * children: [...],
? node.children.map((child: any, childIndex: number) => transformNode(child, childIndex)) * isLeaf: false
: undefined, * }
data: node */
}; interface EngineTreeNode {
}; /** 节点显示名称 */
name?: string;
const hasChildren = model.children && model.children.length > 0; /** 节点 ID可能为 null */
return { id?: string | null;
id: `model-${modelIndex}`, /** 构件 ID 数组 - 用于调用 highlightModel 高亮模型 */
label: model.name || '模型', ids?: string[] | null;
expanded: true, /** 子节点列表 */
clickAction: 'expand', children?: EngineTreeNode[] | null;
children: hasChildren /** 是否为叶子节点 */
? model.children.map((child: any, childIndex: number) => transformNode(child, childIndex)) isLeaf?: boolean;
: undefined
};
});
} }
/**
* 3D 引擎返回的原始模型数据结构(最外层)
*
* @example
* {
* name: "栈桥模型",
* url: "https://xxx.com/models/xxx/", // 模型 URL用于 highlightModel
* children: [...]
* }
*/
interface EngineModelData {
/** 模型显示名称 */
name?: string;
/** 模型资源 URL - 调用 highlightModel 时需要此参数 */
url: string;
/** 子节点列表 */
children?: EngineTreeNode[] | null;
}
/**
* 扩展的节点数据,包含模型 URL
* 转换后存储在 TreeNodeConfig.data 中
*/
interface TransformedNodeData extends EngineTreeNode {
/** 模型 URL - 从最外层 model.url 传递下来 */
_modelUrl: string;
}
// ============================================================================
// 工具函数
// ============================================================================
/**
* 使用 SHA-256 算法对 ids 数组生成唯一哈希值
*
* 为什么需要 hash
* - 原始数据中 node.id 可能为 null
* - ids 数组可能很长(几百个元素),不适合直接作为 ID
* - 需要一个稳定且唯一的标识符用于树组件的 nodeMap
*
* @param ids - 构件 ID 数组
* @returns 64 位十六进制哈希字符串
*
* @example
* hashIds(["350518", "350520"]) // => "a1b2c3d4e5f6..."
*/
async function hashIds(ids: string[]): Promise<string> {
const str = JSON.stringify(ids);
const data = new TextEncoder().encode(str);
const buf = await crypto.subtle.digest('SHA-256', data);
return Array.from(new Uint8Array(buf))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
}
/**
* 递归收集节点及其所有子孙节点的 ids
* 用于父级节点操作时,获取其下所有叶子节点对应的构件 ids
*/
function collectAllIds(node: BimTreeNode): string[] {
const result: string[] = [];
const data = node.config.data as TransformedNodeData | undefined;
if (data?.ids?.length) {
result.push(...data.ids);
}
for (const child of node.children || []) {
result.push(...collectAllIds(child));
}
return result;
}
/**
* 将 3D 引擎返回的原始树数据转换为 SDK 标准的 TreeNodeConfig 格式
*
* 转换过程:
* 1. 遍历最外层的模型数组,提取每个模型的 url
* 2. 递归转换每个节点,将 url 注入到所有子节点的 data 中
* 3. 如果节点有 ids 数组,使用 SHA-256 生成唯一 ID
*
* 点击行为:
* - 点击节点内容:触发 onNodeSelect高亮+跳转)
* - 点击箭头:展开/折叠子节点
*
* 数据结构转换示意:
*
* 输入:[{ name, url, children: [{ name, ids, children }] }]
* 输出:[{ id, label, data: { ids, _modelUrl }, children }]
*
* @param apiData - 3D 引擎返回的原始树数据
* @returns 转换后的 TreeNodeConfig 数组
*/
let nodeIdCounter = 0;
async function transformTreeData(apiData: EngineModelData[]): Promise<TreeNodeConfig[]> {
if (!apiData || apiData.length === 0) return [];
/**
* 递归转换单个节点
* @param node - 原始节点数据
* @param modelUrl - 从最外层传递的模型 URL
*/
const transformNode = async (node: EngineTreeNode, modelUrl: string): Promise<TreeNodeConfig> => {
const hasChildren = node.children && node.children.length > 0;
// 生成节点 ID优先使用 ids 哈希,其次使用原始 id最后生成唯一 ID
let id: string;
if (node.ids?.length) {
id = await hashIds(node.ids);
} else if (node.id) {
id = node.id;
} else {
id = `node_${++nodeIdCounter}`;
}
return {
id,
label: node.name || '未命名',
expanded: false,
checked: true,
children: hasChildren
? await Promise.all(node.children!.map(child => transformNode(child, modelUrl)))
: undefined,
data: {
...node,
_modelUrl: modelUrl
} as TransformedNodeData
};
};
// 遍历最外层模型数组
return Promise.all(apiData.map(async (model) => {
const hasChildren = model.children && model.children.length > 0;
// ⭐ 提取模型 URL将传递给所有子节点
const modelUrl = model.url;
return {
id: modelUrl,
label: model.name || '模型',
expanded: true,
checked: true,
children: hasChildren
? await Promise.all(model.children!.map(child => transformNode(child, modelUrl)))
: undefined,
data: {
_modelUrl: modelUrl
} as TransformedNodeData
};
}));
}
// ============================================================================
// 管理器类
// ============================================================================
/**
* 构件树管理器
*
* 职责:
* 1. 管理构件树按钮的生命周期(创建、显示/隐藏、销毁)
* 2. 管理构件树对话框(打开、关闭、内容渲染)
* 3. 处理树节点点击事件,调用 3D 引擎高亮模型
*
* 使用示例:
* ```typescript
* const manager = new ConstructTreeManagerBtn(container);
* manager.openConstructTreeDialog(); // 打开构件树对话框
* ```
*/
export class ConstructTreeManagerBtn extends BaseManager { export class ConstructTreeManagerBtn extends BaseManager {
/** 按钮组实例 */ /** 按钮组实例 - 用于渲染构件树按钮 */
private toolbar: BimButtonGroup | null = null; private toolbar: BimButtonGroup | null = null;
/** 按钮容器元素 */ /** 按钮容器元素 */
private toolbarContainer: HTMLElement | null = null; private toolbarContainer: HTMLElement | null = null;
/** 主容器元素 */ /** 主容器元素 - 按钮挂载的父容器 */
private container: HTMLElement; private container: HTMLElement;
/** 构件树对话框实例 */ /** 构件树对话框实例 */
private dialog: BimDialog | null = null; private dialog: BimDialog | null = null;
/**
* 创建构件树管理器
* @param container - 按钮挂载的容器元素
*/
constructor(container: HTMLElement) { constructor(container: HTMLElement) {
super(); super();
this.container = container; this.container = container;
this.init(); this.init();
} }
/** 初始化按钮 */ /**
* 初始化按钮
* 创建按钮容器和按钮组,添加构件树按钮
*/
private init() { private init() {
// 创建按钮容器
this.toolbarContainer = document.createElement('div'); this.toolbarContainer = document.createElement('div');
this.toolbarContainer.id = 'bim-construct-tree'; this.toolbarContainer.id = 'bim-construct-tree';
this.container.appendChild(this.toolbarContainer); this.container.appendChild(this.toolbarContainer);
// 创建按钮组
this.toolbar = new BimButtonGroup({ this.toolbar = new BimButtonGroup({
container: this.toolbarContainer, container: this.toolbarContainer,
showLabel: false, showLabel: false,
@@ -69,6 +262,8 @@ export class ConstructTreeManagerBtn extends BaseManager {
}); });
this.toolbar.init(); this.toolbar.init();
this.toolbar.addGroup('construct-tree'); this.toolbar.addGroup('construct-tree');
// 添加构件树按钮
this.toolbar.addButton({ this.toolbar.addButton({
id: 'construct-tree-btn', id: 'construct-tree-btn',
groupId: 'construct-tree', groupId: 'construct-tree',
@@ -82,31 +277,103 @@ export class ConstructTreeManagerBtn extends BaseManager {
this.toolbar.render(); this.toolbar.render();
} }
public openConstructTreeDialog() { /**
* 打开构件树对话框
*
* 流程:
* 1. 隐藏按钮组
* 2. 从 3D 引擎获取三种树数据(楼层/类型/专业)
* 3. 转换数据格式
* 4. 创建三个 BimTree 实例
* 5. 创建选项卡组件
* 6. 创建对话框并显示
*/
public async openConstructTreeDialog() {
// 隐藏按钮组,避免遮挡对话框
this.setVisible(false); this.setVisible(false);
// 从 3D 引擎获取原始树数据
const levelTreeData = this.registry.engine3d?.getLevelTreeData() ?? []; const levelTreeData = this.registry.engine3d?.getLevelTreeData() ?? [];
const typeTreeData = this.registry.engine3d?.getTypeTreeData() ?? []; const typeTreeData = this.registry.engine3d?.getTypeTreeData() ?? [];
const majorTreeData = this.registry.engine3d?.getMajorTreeData() ?? []; const majorTreeData = this.registry.engine3d?.getMajorTreeData() ?? [];
console.log('[ConstructTree] 构件树数据 (Level):', levelTreeData); // 调试日志:输出原始数据
console.log('[ConstructTree] 类型树数据 (Type):', typeTreeData); console.log('[ConstructTree] 原始数据 (Level):', levelTreeData);
console.log('[ConstructTree] 专业树数据 (Major):', majorTreeData); console.log('[ConstructTree] 原始数据 (Type):', typeTreeData);
console.log('[ConstructTree] 原始数据 (Major):', majorTreeData);
/**
* 创建树组件
* @param data - 原始树数据
* @param label - 调试用标签
*/
const createTree = async (data: any[], label: string) => {
// 转换数据格式
const transformedData = await transformTreeData(data);
console.log(`[ConstructTree] 转换后数据 (${label}):`, transformedData);
const createTree = (data: any[]) => {
const tree = new BimTree({ const tree = new BimTree({
data: transformTreeData(data), data: transformedData,
checkable: true, checkable: true,
indent: 0, indent: 0,
enableSearch: true, enableSearch: true,
checkStrictly: true, checkStrictly: true,
defaultExpandAll: true, defaultExpandAll: true,
/**
* 节点勾选回调 - 显示/隐藏模型构件
* 勾选 → showModel取消勾选 → hideModels
*/
onNodeCheck: (node) => { onNodeCheck: (node) => {
console.log('onNodeCheck', node); const nodeData = node.config.data as TransformedNodeData | undefined;
if (!nodeData?._modelUrl) return;
const ids = nodeData.ids?.length
? nodeData.ids
: collectAllIds(node);
if (!ids.length) return;
const modelParam = [{
url: nodeData._modelUrl,
ids: ids.map(Number)
}];
if (node.checkState === TreeNodeCheckState.Checked) {
this.registry.engine3d?.showModel(modelParam);
} else {
this.registry.engine3d?.hideModels(modelParam);
}
}, },
/**
* 节点选中回调 - 高亮并跳转到模型构件
*/
onNodeSelect: (node) => { onNodeSelect: (node) => {
console.log('onNodeSelect', node); const nodeData = node.config.data as TransformedNodeData | undefined;
if (!nodeData?._modelUrl) return;
const ids = nodeData.ids?.length
? nodeData.ids
: collectAllIds(node);
if (!ids.length) return;
const modelParam = [{
url: nodeData._modelUrl,
ids: ids.map(Number)
}];
this.registry.engine3d?.unhighlightAllModels();
this.registry.engine3d?.highlightModel(modelParam);
this.registry.engine3d?.viewScaleToModel(modelParam);
}, },
// 再次点击已选中节点时取消高亮
onNodeDeselect: () => {
this.registry.engine3d?.unhighlightAllModels();
},
onNodeExpand: () => { onNodeExpand: () => {
this.dialog?.fitWidth(); this.dialog?.fitWidth();
}, },
@@ -115,10 +382,12 @@ export class ConstructTreeManagerBtn extends BaseManager {
return tree; return tree;
}; };
const componentTree = createTree(levelTreeData); // 创建三个树实例
const typeTree = createTree(typeTreeData); const componentTree = await createTree(levelTreeData, 'Level');
const majorTree = createTree(majorTreeData); const typeTree = await createTree(typeTreeData, 'Type');
const majorTree = await createTree(majorTreeData, 'Major');
// 创建选项卡面板容器
const componentPanel = document.createElement('div'); const componentPanel = document.createElement('div');
componentPanel.className = 'construct-tab__panel-content'; componentPanel.className = 'construct-tab__panel-content';
componentPanel.appendChild(componentTree.element); componentPanel.appendChild(componentTree.element);
@@ -131,10 +400,23 @@ export class ConstructTreeManagerBtn extends BaseManager {
majorPanel.className = 'construct-tab__panel-content'; majorPanel.className = 'construct-tab__panel-content';
majorPanel.appendChild(majorTree.element); majorPanel.appendChild(majorTree.element);
// 创建选项卡组件
const tabMount = document.createElement('div'); const tabMount = document.createElement('div');
tabMount.className = 'construct-tab__container'; tabMount.className = 'construct-tab__container';
tabMount.style.height = '100%'; tabMount.style.height = '100%';
tabMount.style.overflow = 'hidden'; tabMount.style.overflow = 'hidden';
/**
* 重置所有树的勾选状态并显示所有模型
* 在 Tab 切换和对话框初始化时调用
*/
const resetAllTrees = () => {
this.registry.engine3d?.showAllModels();
componentTree.checkAllNodes(true);
typeTree.checkAllNodes(true);
majorTree.checkAllNodes(true);
};
const tab = new BimTab({ const tab = new BimTab({
container: tabMount, container: tabMount,
tabs: [ tabs: [
@@ -144,11 +426,21 @@ export class ConstructTreeManagerBtn extends BaseManager {
], ],
activeId: 'component', activeId: 'component',
onChange: () => { onChange: () => {
resetAllTrees();
this.dialog?.fitWidth(); this.dialog?.fitWidth();
} }
}); });
tab.init(); tab.init();
// BimTab 初始化时不触发 onChange需要手动调用
resetAllTrees();
// 监听右键菜单"显示全部"事件
const unsubscribeShowAll = this.registry.on('menu:show-all', () => {
resetAllTrees();
});
// 创建对话框
this.dialog = this.registry.dialog!.create({ this.dialog = this.registry.dialog!.create({
title: 'constructTree.title', title: 'constructTree.title',
minWidth: 320, minWidth: 320,
@@ -157,6 +449,7 @@ export class ConstructTreeManagerBtn extends BaseManager {
position: { x: 20, y: 20 }, position: { x: 20, y: 20 },
resizable: false, resizable: false,
onClose: () => { onClose: () => {
unsubscribeShowAll();
tab.destroy(); tab.destroy();
componentTree.destroy(); componentTree.destroy();
typeTree.destroy(); typeTree.destroy();
@@ -181,8 +474,8 @@ export class ConstructTreeManagerBtn extends BaseManager {
/** /**
* 添加按钮组 * 添加按钮组
* @param groupId 组 ID * @param groupId - 组 ID
* @param beforeGroupId 插入位置 * @param beforeGroupId - 插入位置
*/ */
public addGroup(groupId: string, beforeGroupId?: string) { public addGroup(groupId: string, beforeGroupId?: string) {
this.toolbar?.addGroup(groupId, beforeGroupId); this.toolbar?.addGroup(groupId, beforeGroupId);
@@ -191,7 +484,7 @@ export class ConstructTreeManagerBtn extends BaseManager {
/** /**
* 添加按钮 * 添加按钮
* @param config 按钮配置 * @param config - 按钮配置
*/ */
public addButton(config: ButtonConfig) { public addButton(config: ButtonConfig) {
this.toolbar?.addButton(config); this.toolbar?.addButton(config);
@@ -200,8 +493,8 @@ export class ConstructTreeManagerBtn extends BaseManager {
/** /**
* 设置按钮可见性 * 设置按钮可见性
* @param id 按钮 ID * @param id - 按钮 ID
* @param v 是否可见 * @param v - 是否可见
*/ */
public setButtonVisibility(id: string, v: boolean) { public setButtonVisibility(id: string, v: boolean) {
this.toolbar?.updateButtonVisibility(id, v); this.toolbar?.updateButtonVisibility(id, v);
@@ -209,7 +502,7 @@ export class ConstructTreeManagerBtn extends BaseManager {
/** /**
* 设置是否显示标签 * 设置是否显示标签
* @param show 是否显示 * @param show - 是否显示
*/ */
public setShowLabel(show: boolean) { public setShowLabel(show: boolean) {
this.toolbar?.setShowLabel(show); this.toolbar?.setShowLabel(show);
@@ -217,7 +510,7 @@ export class ConstructTreeManagerBtn extends BaseManager {
/** /**
* 设置按钮组可见性 * 设置按钮组可见性
* @param visible 是否可见 * @param visible - 是否可见
*/ */
public setVisible(visible: boolean) { public setVisible(visible: boolean) {
if (this.toolbarContainer) { if (this.toolbarContainer) {
@@ -227,7 +520,7 @@ export class ConstructTreeManagerBtn extends BaseManager {
/** /**
* 设置背景颜色 * 设置背景颜色
* @param color 颜色值 * @param color - 颜色值
*/ */
public setBackgroundColor(color: string) { public setBackgroundColor(color: string) {
this.toolbar?.setBackgroundColor(color); this.toolbar?.setBackgroundColor(color);
@@ -235,7 +528,7 @@ export class ConstructTreeManagerBtn extends BaseManager {
/** /**
* 设置按钮组颜色 * 设置按钮组颜色
* @param colors 颜色配置 * @param colors - 颜色配置
*/ */
public setColors(colors: ButtonGroupColors) { public setColors(colors: ButtonGroupColors) {
this.toolbar?.setColors(colors); this.toolbar?.setColors(colors);

View File

@@ -1,41 +1,18 @@
/**
* 对话框管理器
* 负责创建和管理所有对话框实例
*/
import { BimDialog } from '../components/dialog'; import { BimDialog } from '../components/dialog';
import { BimInfoDialog } from '../components/dialog/bimInfoDialog';
import type { DialogOptions } from '../components/dialog/index.type'; import type { DialogOptions } from '../components/dialog/index.type';
import type { ThemeConfig } from '../themes/types'; import type { ThemeConfig } from '../themes/types';
import { themeManager } from '../services/theme'; import { themeManager } from '../services/theme';
import { BaseManager } from '../core/base-manager'; import { BaseManager } from '../core/base-manager';
/**
* 对话框管理器
* 统一管理对话框的创建、主题更新和销毁
*/
export class DialogManager extends BaseManager { export class DialogManager extends BaseManager {
/** 容器元素 */
private container: HTMLElement; private container: HTMLElement;
/** 活跃的对话框列表 */
private activeDialogs: BimDialog[] = []; private activeDialogs: BimDialog[] = [];
constructor(container: HTMLElement) { constructor(container: HTMLElement) {
super(); super();
this.container = container; this.container = container;
this.subscribe('ui:open-dialog', (payload) => {
console.log('[DialogManager] Received open-dialog event:', payload);
if (payload.id === 'info') {
this.showInfoDialog();
}
});
} }
/**
* 创建对话框
* @param options 对话框配置选项
* @returns 对话框实例
*/
public create(options: Omit<DialogOptions, 'container'>): BimDialog { public create(options: Omit<DialogOptions, 'container'>): BimDialog {
const dialog = new BimDialog({ const dialog = new BimDialog({
container: this.container, container: this.container,
@@ -51,15 +28,6 @@ export class DialogManager extends BaseManager {
return dialog; return dialog;
} }
/** 显示信息对话框 */
public showInfoDialog() {
new BimInfoDialog(this.container);
}
/**
* 更新所有对话框的主题
* @param theme 主题配置
*/
public updateTheme(theme: ThemeConfig) { public updateTheme(theme: ThemeConfig) {
this.activeDialogs.forEach(dialog => { this.activeDialogs.forEach(dialog => {
if (dialog.setTheme) { if (dialog.setTheme) {
@@ -68,7 +36,6 @@ export class DialogManager extends BaseManager {
}); });
} }
/** 销毁管理器和所有对话框 */
public destroy() { public destroy() {
this.activeDialogs.forEach(d => d.destroy()); this.activeDialogs.forEach(d => d.destroy());
this.activeDialogs = []; this.activeDialogs = [];

View File

@@ -0,0 +1,55 @@
import { BaseDialogManager } from '../core/base-dialog-manager';
import { t } from '../services/locale';
import type { EngineInfo } from '../components/engine';
export class EngineInfoDialogManager extends BaseDialogManager {
protected get dialogId() { return 'engine-info-dialog'; }
protected get dialogTitle() { return 'info.dialogTitle'; }
protected get dialogWidth() { return 280; }
public init(): void {}
protected getDialogPosition() {
const container = this.registry.container;
if (!container) return { x: 100, y: 100 };
const containerWidth = container.clientWidth;
const containerHeight = container.clientHeight;
return {
x: (containerWidth - this.dialogWidth) / 2,
y: (containerHeight - 150) / 2
};
}
protected createContent(): HTMLElement {
const info: EngineInfo | null = this.registry.engine3d?.getEngineInfo() ?? null;
const content = document.createElement('div');
content.className = 'engine-info-content';
content.style.cssText = 'padding: 16px;';
const createRow = (label: string, value: number | string) => {
const row = document.createElement('div');
row.style.cssText = 'display: flex; justify-content: space-between; margin-bottom: 12px; font-size: 14px;';
const labelEl = document.createElement('span');
labelEl.style.cssText = 'color: var(--bim-text-secondary, #94a3b8);';
labelEl.textContent = label;
const valueEl = document.createElement('span');
valueEl.style.cssText = 'color: var(--bim-text-primary, #fff); font-weight: 500;';
valueEl.textContent = String(value);
row.appendChild(labelEl);
row.appendChild(valueEl);
return row;
};
content.appendChild(createRow(t('info.meshCount'), info?.meshCount ?? '-'));
content.appendChild(createRow(t('info.totalTriangles'), info?.totalTriangles ?? '-'));
content.appendChild(createRow(t('info.totalVertices'), info?.totalVertices ?? '-'));
return content;
}
}

View File

@@ -208,6 +208,7 @@ export class EngineManager extends BaseManager {
order: 8, order: 8,
onClick: () => { onClick: () => {
this.showAllModels(); this.showAllModels();
this.emit('menu:show-all', {});
this.rightKey?.hide(); this.rightKey?.hide();
} }
}); });
@@ -400,6 +401,28 @@ export class EngineManager extends BaseManager {
this.engineInstance.fitSectionBoxToModel(); this.engineInstance.fitSectionBoxToModel();
} }
/**
* 剖切盒适应(缩放到场景整体包围盒)
* @remarks 对接底层 clipping.scaleBox()
*/
public scaleSectionBox(): void {
if (!this.engineInstance) {
return;
}
this.engineInstance.scaleSectionBox();
}
/**
* 反向剖切
* @remarks 对接底层 clipping.reverse()
*/
public reverseSection(): void {
if (!this.engineInstance) {
return;
}
this.engineInstance.reverseSection();
}
/** 激活框选放大功能 */ /** 激活框选放大功能 */
public activateZoomBox(): void { public activateZoomBox(): void {
if (!this.engineInstance) { if (!this.engineInstance) {
@@ -569,13 +592,35 @@ export class EngineManager extends BaseManager {
} }
/** /**
* 隐藏指定模型 * 高亮指定模型构件
* @param models 要隐藏的模型对象 *
* @param models - 要高亮的模型数组,格式: [{ url: string, ids: string[] }]
*
* @example
* manager.highlightModel([
* { url: 'https://xxx/models/xxx/', ids: [350518, 350520] }
* ]);
*/ */
public hideModels(models: any): void { public highlightModel(models: { url: string; ids: number[] }[]): void {
this.engineInstance?.highlightModel(models);
}
public unhighlightAllModels(): void {
this.engineInstance?.unhighlightAllModels();
}
public viewScaleToModel(models: { url: string; ids: number[] }[]): void {
this.engineInstance?.viewScaleToModel(models);
}
public hideModels(models: { url: string; ids: number[] }[]): void {
this.engineInstance?.hideModels(models); this.engineInstance?.hideModels(models);
} }
public showModel(models: { url: string; ids: number[] }[]): void {
this.engineInstance?.showModel(models);
}
/** /**
* 半透明指定模型 * 半透明指定模型
* @param models 要半透明的模型对象 * @param models 要半透明的模型对象

View File

@@ -1,76 +0,0 @@
/**
* 地图对话框管理器
* 负责管理地图/平面图对话框的显示和交互
*/
import { BaseDialogManager } from '../core/base-dialog-manager';
import { MapPanel } from '../components/map-panel';
/**
* 地图对话框管理器
* 继承自 BaseDialogManager提供地图面板的对话框管理功能
*/
export class MapDialogManager extends BaseDialogManager {
/** 地图面板实例 */
private panel: MapPanel | null = null;
/** 对话框唯一标识 */
protected get dialogId() { return 'map-dialog'; }
/** 对话框标题(国际化 key */
protected get dialogTitle() { return 'map.dialogTitle'; }
/** 对话框宽度 */
protected get dialogWidth() { return 300; }
/** 对话框高度 */
protected get dialogHeight(): number { return 400; }
/** 初始化 */
public init(): void {}
/**
* 获取对话框位置
* 定位在容器左下角
*/
protected getDialogPosition() {
const container = this.registry.container;
if (!container) return { x: 20, y: 100 };
const paddingLeft = 20;
const paddingBottom = 20;
const containerHeight = container.clientHeight;
return {
x: paddingLeft,
y: containerHeight - this.dialogHeight - paddingBottom
};
}
/** 创建对话框内容 */
protected createContent(): HTMLElement {
this.panel = new MapPanel();
this.panel.init();
return this.panel.element;
}
/** 对话框创建后的回调 */
protected onDialogCreated(): void {
this.emit('map:opened', {});
}
/** 对话框关闭时的回调 */
protected onDialogClose(): void {
this.emit('map:closed', {});
}
/** 销毁前的清理 */
protected onBeforeDestroy(): void {
if (this.panel) {
this.panel.destroy();
this.panel = null;
}
}
/** 隐藏对话框 */
public hide(): void {
super.hide();
this.emit('map:closed', {});
}
}

View File

@@ -1,14 +1,15 @@
import { BaseDialogManager } from '../core/base-dialog-manager'; import { BaseDialogManager } from '../core/base-dialog-manager';
import { MeasurePanel } from '../components/measure-panel'; import { MeasurePanel } from '../components/measure-panel';
import type { MeasureConfig, MeasureResult } from '../components/measure-panel/types'; import type { MeasureConfig, MeasureResult } from '../components/measure-panel/types';
import { ENGINE_TYPE_TO_MODE, MEASURE_TYPES, type MeasureMode } from '../types/measure'; import { MEASURE_TYPES, getModeBycallBackType, getValueType, type MeasureMode, type CallBackType } from '../types/measure';
interface EngineMeasureData { interface EngineMeasureData {
id: string; id: string;
point1?: { x: number; y: number; z: number };
point2?: { x: number; y: number; z: number };
text: number; text: number;
type: string; textX?: number;
textY?: number;
textZ?: number;
type: CallBackType;
isSelect: boolean; isSelect: boolean;
container: any; container: any;
} }
@@ -80,20 +81,24 @@ export class MeasureDialogManager extends BaseDialogManager {
const engine = this.registry.engine3d?.getEngine(); const engine = this.registry.engine3d?.getEngine();
if (engine?.events) { if (engine?.events) {
const handler = (data: EngineMeasureData) => { const handler = (data: EngineMeasureData) => {
console.log('[MeasureDialogManager] 测量值变化:', data); console.log('[MeasureDialogManager] 测量值回调:', data);
if (data && this.panel) { if (data && this.panel) {
this.handleMeasureChanged(data); this.handleMeasureChanged(data);
} }
}; };
engine.events.on('measure-changed', handler); engine.events.on('measure-changed', handler);
this.unsubscribeMeasureChanged = () => engine.events.off('measure-changed', handler); engine.events.on('measure-click', handler);
this.unsubscribeMeasureChanged = () => {
engine.events.off('measure-changed', handler);
engine.events.on('measure-click', handler);
}
} }
} }
private handleMeasureChanged(data: EngineMeasureData): void { private handleMeasureChanged(data: EngineMeasureData): void {
if (!this.panel) return; if (!this.panel) return;
const targetMode = ENGINE_TYPE_TO_MODE[data.type]; const targetMode = getModeBycallBackType(data.type);
if (!targetMode) { if (!targetMode) {
console.warn('[MeasureDialogManager] 未知测量类型:', data.type); console.warn('[MeasureDialogManager] 未知测量类型:', data.type);
return; return;
@@ -112,10 +117,12 @@ export class MeasureDialogManager extends BaseDialogManager {
const config = MEASURE_TYPES[mode]; const config = MEASURE_TYPES[mode];
const result: MeasureResult = {}; const result: MeasureResult = {};
if (config.valueType === 'point' && data.point1) { if (getValueType(mode) === 'point') {
result.xyz = { x: data.point1.x, y: data.point1.y, z: data.point1.z }; if (data.textX !== undefined && data.textY !== undefined && data.textZ !== undefined) {
result.xyz = { x: data.textX, y: data.textY, z: data.textZ };
}
} else { } else {
(result as any)[config.resultField] = data.text; (result as any)[config.callBackType] = data.text;
} }
return result; return result;

View File

@@ -57,14 +57,21 @@ export class SectionBoxDialogManager extends BaseDialogManager {
} }
}, },
onReverseToggle: (isReversed) => { onReverseToggle: (isReversed) => {
// 底层暂不支持反向功能 console.log('[SectionBoxDialogManager] 反向切换:', isReversed);
console.log('[SectionBoxDialogManager] 反向切换(底层暂不支持):', isReversed); // 底层 reverse() 为“切换一次”,这里不使用 isReversed 作为入参,只要用户点击就触发。
this.registry.engine3d?.reverseSection();
}, },
onFitToModel: () => { onFitToModel: () => {
console.log('[SectionBoxDialogManager] Fit to model not supported in new API'); // 对接底层 scaleBox():缩放剖切盒到场景整体包围盒
this.registry.engine3d?.scaleSectionBox();
}, },
onReset: () => { onReset: () => {
console.log('[SectionBoxDialogManager] Reset not supported in new API'); // 重置定义:关闭剖切再打开剖切盒。
// UI 侧会自行将滑块强制恢复到 0-100并将隐藏/反向按钮恢复为关闭状态。
this.registry.engine3d?.deactivateSection();
this.registry.engine3d?.activeSection('box');
// 确保剖切可见(避免上一次处于隐藏状态导致“看起来没重置”)
this.registry.engine3d?.recoverSection();
}, },
onRangeChange: (range) => { onRangeChange: (range) => {
this.registry.engine3d?.setSectionBoxRange(range); this.registry.engine3d?.setSectionBoxRange(range);

View File

@@ -41,12 +41,8 @@ export class WalkControlManager extends BaseManager {
this.panel = new WalkControlPanel({ this.panel = new WalkControlPanel({
onPlanViewToggle: (isActive) => { onPlanViewToggle: (isActive) => {
console.log('[WalkControl] 地图:', isActive); console.log('[WalkControl] 地图:', isActive);
if (isActive) { this.registry.engine3d?.toggleMiniMap();
this.registry.map?.show();
} else {
this.registry.map?.hide();
}
this.emit('walk:plan-view-toggle', { isActive }); this.emit('walk:plan-view-toggle', { isActive });
}, },
onPathModeToggle: (isActive) => { onPathModeToggle: (isActive) => {
@@ -94,17 +90,7 @@ export class WalkControlManager extends BaseManager {
}); });
this.panel.init(); this.panel.init();
if (this.registry.map?.isOpen()) {
this.panel.setPlanViewActive(true);
}
this.subscribe('map:opened', () => {
this.panel?.setPlanViewActive(true);
});
this.subscribe('map:closed', () => {
this.panel?.setPlanViewActive(false);
});
if (this.registry.container) { if (this.registry.container) {
this.panel.element.style.position = 'absolute'; this.panel.element.style.position = 'absolute';

View File

@@ -20,26 +20,26 @@ export class WalkPathDialogManager extends BaseDialogManager {
/** 对话框宽度 */ /** 对话框宽度 */
protected get dialogWidth() { return 300; } protected get dialogWidth() { return 300; }
/** 对话框高度 */ /** 对话框高度 */
protected get dialogHeight(): number { return 400; } protected get dialogHeight(): number { return 450; }
/** 初始化 */ /** 初始化 */
public init(): void {} public init(): void {}
/** /**
* 获取对话框位置 * 获取对话框位置
* 定位在容器右侧居中 * 定位在容器右上角
*/ */
protected getDialogPosition() { protected getDialogPosition() {
const container = this.registry.container; const container = this.registry.container;
if (!container) return { x: 100, y: 100 }; if (!container) return { x: 100, y: 100 };
const paddingRight = 20; const paddingRight = 20;
const paddingTop = 20;
const containerWidth = container.clientWidth; const containerWidth = container.clientWidth;
const containerHeight = container.clientHeight;
return { return {
x: containerWidth - this.dialogWidth - paddingRight, x: containerWidth - this.dialogWidth - paddingRight,
y: (containerHeight - this.dialogHeight) / 2 y: paddingTop
}; };
} }

View File

@@ -27,14 +27,13 @@ export interface EngineEvents {
'walk:gravity-toggle': { enabled: boolean }; 'walk:gravity-toggle': { enabled: boolean };
'walk:collision-toggle': { enabled: boolean }; 'walk:collision-toggle': { enabled: boolean };
// 地图事件
'map:opened': {};
'map:closed': {};
// 构件选中事件 // 构件选中事件
'component:selected': { url: string; id: string }; 'component:selected': { url: string; id: string };
'component:deselected': {}; 'component:deselected': {};
// 右键菜单事件
'menu:show-all': {};
// 剖切事件 // 剖切事件
'section:move': { x?: { min: number; max: number }; y?: { min: number; max: number }; z?: { min: number; max: number } }; 'section:move': { x?: { min: number; max: number }; y?: { min: number; max: number }; z?: { min: number; max: number } };

View File

@@ -1,24 +1,3 @@
export type MeasureMode =
| 'clearHeight'
| 'clearDistance'
| 'distance'
| 'elevation'
| 'point'
| 'angle'
| 'area'
| 'slope';
export type MeasureValueType = 'length' | 'area' | 'angle' | 'percent' | 'point';
export interface MeasureTypeConfig {
key: MeasureMode;
engineKey: string;
valueType: MeasureValueType;
icon: string;
order: number;
resultField: string;
}
const ICONS = { const ICONS = {
clearHeight: `<svg viewBox="0 0 32 32" aria-hidden="true"><text x="16" y="20" text-anchor="middle" fill="currentColor" font-size="12" font-family="system-ui, sans-serif">净高</text></svg>`, clearHeight: `<svg viewBox="0 0 32 32" aria-hidden="true"><text x="16" y="20" text-anchor="middle" fill="currentColor" font-size="12" font-family="system-ui, sans-serif">净高</text></svg>`,
clearDistance: `<svg viewBox="0 0 32 32" aria-hidden="true"><text x="16" y="20" text-anchor="middle" fill="currentColor" font-size="12" font-family="system-ui, sans-serif">净距</text></svg>`, clearDistance: `<svg viewBox="0 0 32 32" aria-hidden="true"><text x="16" y="20" text-anchor="middle" fill="currentColor" font-size="12" font-family="system-ui, sans-serif">净距</text></svg>`,
@@ -28,83 +7,88 @@ const ICONS = {
angle: `<svg viewBox="0 0 32 32" aria-hidden="true"><path fill="currentColor" d="M39.587,50.766h13.7a1,1,0,0,1,0,2H23.171a1,1,0,0,1,0-2h1.418l6.582-7.006v-.006a.517.517,0,0,1,.14-.357.456.456,0,0,1,.337-.144l12.1-12.876a.451.451,0,0,1,.665,0,.524.524,0,0,1,0,.708L32.883,43.355a8.3,8.3,0,0,1,6.7,7.411Zm-.949,0a7.254,7.254,0,0,0-6.611-6.5l-6.108,6.5Z" transform="translate(-22.229 -26.489)"/></svg>`, angle: `<svg viewBox="0 0 32 32" aria-hidden="true"><path fill="currentColor" d="M39.587,50.766h13.7a1,1,0,0,1,0,2H23.171a1,1,0,0,1,0-2h1.418l6.582-7.006v-.006a.517.517,0,0,1,.14-.357.456.456,0,0,1,.337-.144l12.1-12.876a.451.451,0,0,1,.665,0,.524.524,0,0,1,0,.708L32.883,43.355a8.3,8.3,0,0,1,6.7,7.411Zm-.949,0a7.254,7.254,0,0,0-6.611-6.5l-6.108,6.5Z" transform="translate(-22.229 -26.489)"/></svg>`,
area: `<svg viewBox="0 0 32 32" aria-hidden="true"><text x="16" y="20" text-anchor="middle" fill="currentColor" font-size="12" font-family="system-ui, sans-serif">面积</text></svg>`, area: `<svg viewBox="0 0 32 32" aria-hidden="true"><text x="16" y="20" text-anchor="middle" fill="currentColor" font-size="12" font-family="system-ui, sans-serif">面积</text></svg>`,
slope: `<svg viewBox="0 0 32 32" aria-hidden="true"><path fill="currentColor" d="M202.1,188.337l2.629-2.191-8.447-3.106,1.533,8.871,2.629-2.194,9.341,11.209,1.656-1.379Zm-13.726-.435a1.075,1.075,0,0,0-1.07-.341,1.057,1.057,0,0,0-.5.277l-5.11,4.08a1.08,1.08,0,0,0-.406.84l-.007,17.386a1.079,1.079,0,0,0,1.077,1.077L205.7,211.2a1.078,1.078,0,0,0,.822-1.774Zm-4.934,21.164.007-15.788,3.968-3.171,15.974,18.941Z" transform="translate(-180.36 -181.131)"/></svg>`, slope: `<svg viewBox="0 0 32 32" aria-hidden="true"><path fill="currentColor" d="M202.1,188.337l2.629-2.191-8.447-3.106,1.533,8.871,2.629-2.194,9.341,11.209,1.656-1.379Zm-13.726-.435a1.075,1.075,0,0,0-1.07-.341,1.057,1.057,0,0,0-.5.277l-5.11,4.08a1.08,1.08,0,0,0-.406.84l-.007,17.386a1.079,1.079,0,0,0,1.077,1.077L205.7,211.2a1.078,1.078,0,0,0,.822-1.774Zm-4.934,21.164.007-15.788,3.968-3.171,15.974,18.941Z" transform="translate(-180.36 -181.131)"/></svg>`,
}; } as const;
export const MEASURE_TYPES: Record<MeasureMode, MeasureTypeConfig> = { export const MEASURE_TYPES = {
distance: {
key: 'distance',
callBackType: 'distance',
icon: ICONS.distance,
order: 0,
},
clearHeight: { clearHeight: {
key: 'clearHeight', key: 'clearHeight',
engineKey: 'clear-height', callBackType: 'clear-height',
valueType: 'length',
icon: ICONS.clearHeight, icon: ICONS.clearHeight,
order: 0, order: 1,
resultField: 'clearHeightMm',
}, },
clearDistance: { clearDistance: {
key: 'clearDistance', key: 'clearDistance',
engineKey: 'clear-distance', callBackType: 'clear-distance',
valueType: 'length',
icon: ICONS.clearDistance, icon: ICONS.clearDistance,
order: 1,
resultField: 'clearDistanceMm',
},
distance: {
key: 'distance',
engineKey: 'distance',
valueType: 'length',
icon: ICONS.distance,
order: 2, order: 2,
resultField: 'distanceMm',
}, },
elevation: { elevation: {
key: 'elevation', key: 'elevation',
engineKey: 'elevation', callBackType: 'elevation',
valueType: 'length',
icon: ICONS.elevation, icon: ICONS.elevation,
order: 3, order: 3,
resultField: 'elevationMm',
}, },
point: { point: {
key: 'point', key: 'point',
engineKey: 'point', callBackType: 'point',
valueType: 'point',
icon: ICONS.point, icon: ICONS.point,
order: 4, order: 4,
resultField: 'xyz',
}, },
angle: { angle: {
key: 'angle', key: 'angle',
engineKey: 'angle', callBackType: 'angle',
valueType: 'angle',
icon: ICONS.angle, icon: ICONS.angle,
order: 5, order: 5,
resultField: 'angleDeg',
}, },
area: { area: {
key: 'area', key: 'area',
engineKey: 'area', callBackType: 'area',
valueType: 'area',
icon: ICONS.area, icon: ICONS.area,
order: 6, order: 6,
resultField: 'areaM2',
}, },
slope: { slope: {
key: 'slope', key: 'slope',
engineKey: 'slope', callBackType: 'slope',
valueType: 'percent',
icon: ICONS.slope, icon: ICONS.slope,
order: 7, order: 7,
resultField: 'slopePercent',
}, },
}; } as const;
export type MeasureMode = keyof typeof MEASURE_TYPES;
export type CallBackType = typeof MEASURE_TYPES[MeasureMode]['callBackType'];
export type MeasureTypeConfig = typeof MEASURE_TYPES[MeasureMode];
export type MeasureValueType = 'length' | 'area' | 'angle' | 'percent' | 'point';
export const MEASURE_MODES_ORDERED: MeasureMode[] = Object.values(MEASURE_TYPES) export const MEASURE_MODES_ORDERED: MeasureMode[] = Object.values(MEASURE_TYPES)
.sort((a, b) => a.order - b.order) .sort((a, b) => a.order - b.order)
.map((t) => t.key); .map((t) => t.key);
export const ENGINE_TYPE_TO_MODE: Record<string, MeasureMode> = Object.fromEntries( export function getValueType(key: MeasureMode): MeasureValueType {
Object.values(MEASURE_TYPES).map((t) => [t.engineKey, t.key]) switch (key) {
) as Record<string, MeasureMode>; case 'clearHeight':
case 'clearDistance':
case 'distance':
case 'elevation':
return 'length';
case 'area':
return 'area';
case 'angle':
return 'angle';
case 'slope':
return 'percent';
case 'point':
default:
return 'point';
}
}
export const MODE_TO_ENGINE_TYPE: Record<MeasureMode, string> = Object.fromEntries( export function getModeBycallBackType(callBackType: CallBackType): MeasureMode | undefined {
Object.values(MEASURE_TYPES).map((t) => [t.key, t.engineKey]) return (Object.values(MEASURE_TYPES) as MeasureTypeConfig[]).find(t => t.callBackType === callBackType)?.key;
) as Record<MeasureMode, string>; }

View File

@@ -21,7 +21,9 @@ export default defineConfig(() => {
// 开发服务器配置 // 开发服务器配置
server: { server: {
port: 3000, port: 3000,
open: '/demo/index.html', // 自动打开 demo 页面 host: '0.0.0.0',
open: '/demo/index.html',
allowedHosts: true,
}, },
build: { build: {
lib: { lib: {