refactor: sync managers and section box actions
Wire section box scale/reverse/reset to clipping APIs and sync demo artifacts.
@@ -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"
|
||||||
}
|
}
|
||||||
5
.sisyphus/notepads/path-roaming/decisions.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Decisions - Path Roaming Implementation
|
||||||
|
|
||||||
|
This notepad tracks architectural and design decisions made during implementation.
|
||||||
|
|
||||||
|
---
|
||||||
5
.sisyphus/notepads/path-roaming/issues.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Issues - Path Roaming Implementation
|
||||||
|
|
||||||
|
This notepad tracks problems, gotchas, and issues encountered during implementation.
|
||||||
|
|
||||||
|
---
|
||||||
199
.sisyphus/notepads/path-roaming/learnings.md
Normal 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
|
||||||
|
|
||||||
29
.sisyphus/notepads/path-roaming/problems.md
Normal 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
|
||||||
|
|
||||||
109
.sisyphus/notepads/right-click-menu-functions/learnings.md
Normal 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
|
||||||
@@ -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 创建两个不同的 Dialog(ID 不同) |
|
|
||||||
| 无背景色 | `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 个原始问题全部解决
|
|
||||||
1037
.sisyphus/plans/path-roaming.md
Normal file
547
.sisyphus/plans/right-click-menu-functions.md
Normal 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] 构建成功
|
||||||
@@ -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 引擎管理器 API(UI 通过此层调用)
|
||||||
|
|
||||||
|
- 关键交互约定:
|
||||||
|
- “适应”按钮:调用 `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
1
demo/assets/svg/rotate-orbit.svg
Normal 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 |
BIN
demo/assets/viewcube/cn_back.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
demo/assets/viewcube/cn_bottom.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
demo/assets/viewcube/cn_front.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
demo/assets/viewcube/cn_left.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
demo/assets/viewcube/cn_right.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
demo/assets/viewcube/cn_top.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
demo/assets/viewcube/home.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
739
demo/draco/DRACOLoader.js
Normal 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 };
|
||||||
52
demo/draco/draco_decoder.js
Normal file
BIN
demo/draco/draco_decoder.wasm
Normal file
33
demo/draco/draco_encoder.js
Normal file
104
demo/draco/draco_wasm_wrapper.js
Normal 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);
|
||||||
@@ -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: ['..']
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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' });
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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 继承
|
|
||||||
}
|
|
||||||
@@ -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 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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];
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 formatNumberWithPrecision(value: number, precision: MeasurePrecision): string {
|
private convertValue(type: MeasureValueType, value: number | undefined | null): 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)) {
|
if (value === null || value === undefined || Number.isNaN(value)) {
|
||||||
return { numberText: '--', unitText };
|
return '--';
|
||||||
}
|
|
||||||
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));
|
const precision = this.config.precision;
|
||||||
|
|
||||||
if (value === null || value === undefined || Number.isNaN(value)) {
|
|
||||||
return { numberText: '--', unitText };
|
|
||||||
}
|
|
||||||
|
|
||||||
let converted: number;
|
let converted: number;
|
||||||
if (type === 'length') {
|
|
||||||
|
switch (type) {
|
||||||
|
case 'length':
|
||||||
switch (unit) {
|
switch (unit) {
|
||||||
case 'mm': converted = value * 1000; break;
|
case 'mm': converted = value * 1000; break;
|
||||||
case 'cm': converted = value * 100; break;
|
case 'cm': converted = value * 100; break;
|
||||||
case 'km': converted = value / 1000; break;
|
case 'km': converted = value / 1000; break;
|
||||||
default: converted = value;
|
default: converted = value;
|
||||||
}
|
}
|
||||||
} else {
|
break;
|
||||||
|
case 'area':
|
||||||
switch (unit) {
|
switch (unit) {
|
||||||
case 'mm': converted = value * 1000 * 1000; break;
|
case 'mm': converted = value * 1000 * 1000; break;
|
||||||
case 'cm': converted = value * 100 * 100; break;
|
case 'cm': converted = value * 100 * 100; break;
|
||||||
case 'km': converted = value / 1000 / 1000; break;
|
case 'km': converted = value / 1000 / 1000; break;
|
||||||
default: converted = value;
|
default: converted = value;
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
|
case 'angle':
|
||||||
|
case 'percent':
|
||||||
|
case 'point':
|
||||||
|
default:
|
||||||
|
converted = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { numberText: converted.toFixed(this.config.precision), unitText };
|
return converted.toFixed(precision);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取单位文本
|
||||||
|
* @param type 测量值类型
|
||||||
|
* @returns 对应的单位文本(如 mm、m²、°)
|
||||||
|
*/
|
||||||
|
private getUnitText(type: MeasureValueType): string {
|
||||||
|
const unit = this.config.unit;
|
||||||
|
switch (type) {
|
||||||
|
case 'length':
|
||||||
|
case 'point':
|
||||||
|
return unit;
|
||||||
|
case 'area':
|
||||||
|
return `${unit}²`;
|
||||||
|
case 'angle':
|
||||||
|
case 'percent':
|
||||||
|
return '°';
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 => {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
253
src/components/walk-path-panel/index.css
Normal 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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
23
src/components/walk-path-panel/types.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* 漫游点接口
|
||||||
|
* 表示路径中的一个漫游点
|
||||||
|
*/
|
||||||
|
export interface RoamingPoint {
|
||||||
|
/** 漫游点索引 */
|
||||||
|
index: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 播放选项接口
|
||||||
|
* 配置漫游播放的参数
|
||||||
|
*/
|
||||||
|
export interface PlayOptions {
|
||||||
|
/** 总播放时长(毫秒),不包括停留时间 */
|
||||||
|
duration?: number;
|
||||||
|
/** 是否循环播放 */
|
||||||
|
loop?: boolean;
|
||||||
|
/** 播放完成的回调 */
|
||||||
|
onComplete?: () => void;
|
||||||
|
/** 每个点播放完成的回调,用于高亮当前点 */
|
||||||
|
onPointComplete?: (pointIndex: number) => void;
|
||||||
|
}
|
||||||
@@ -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 聊天管理器 */
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 = [];
|
||||||
|
|||||||
55
src/managers/engine-info-dialog-manager.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 要半透明的模型对象
|
||||||
|
|||||||
@@ -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', {});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 } };
|
||||||
|
|
||||||
|
|||||||
@@ -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>;
|
}
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||