288 lines
8.0 KiB
Markdown
288 lines
8.0 KiB
Markdown
|
|
# Architectural Decisions: 构件详情右键菜单功能
|
|||
|
|
|
|||
|
|
## 2026-01-28
|
|||
|
|
|
|||
|
|
### Decision 1: 选中状态存储在 Engine 而非 EngineManager
|
|||
|
|
|
|||
|
|
**Context**:
|
|||
|
|
- 需要存储当前选中的构件信息(url + id)
|
|||
|
|
- Engine 组件监听底层 click 事件
|
|||
|
|
- EngineManager 是 Engine 的代理层
|
|||
|
|
|
|||
|
|
**Options Considered**:
|
|||
|
|
1. 存储在 Engine 组件中
|
|||
|
|
2. 存储在 EngineManager 中
|
|||
|
|
3. 存储在独立的 SelectionManager 中
|
|||
|
|
|
|||
|
|
**Decision**: 选择 Option 1 - 存储在 Engine 组件中
|
|||
|
|
|
|||
|
|
**Rationale**:
|
|||
|
|
- Engine 直接监听底层事件,数据流最短
|
|||
|
|
- 避免事件转发的复杂性
|
|||
|
|
- EngineManager 作为代理,只需暴露访问方法
|
|||
|
|
- 保持单一职责原则:Engine 管理状态,EngineManager 提供接口
|
|||
|
|
|
|||
|
|
**Consequences**:
|
|||
|
|
- ✅ 数据流清晰:底层事件 → Engine 状态 → EngineManager 访问
|
|||
|
|
- ✅ 性能更好:无需事件转发
|
|||
|
|
- ⚠️ Engine 组件职责略有增加(但合理)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Decision 2: 使用动态菜单生成而非事件驱动更新
|
|||
|
|
|
|||
|
|
**Context**:
|
|||
|
|
- 右键菜单内容需要根据选中状态变化
|
|||
|
|
- 选中状态频繁变化
|
|||
|
|
|
|||
|
|
**Options Considered**:
|
|||
|
|
1. 注册静态菜单 + 监听选中事件动态更新
|
|||
|
|
2. 注册处理器函数,每次右键时动态生成菜单
|
|||
|
|
|
|||
|
|
**Decision**: 选择 Option 2 - 动态生成菜单
|
|||
|
|
|
|||
|
|
**Rationale**:
|
|||
|
|
- 右键菜单不常使用,生成成本低
|
|||
|
|
- 避免维护菜单状态的复杂性
|
|||
|
|
- 无需监听选中事件单独更新菜单
|
|||
|
|
- 代码更简洁,逻辑集中
|
|||
|
|
|
|||
|
|
**Implementation**:
|
|||
|
|
```typescript
|
|||
|
|
rightKey.registerHandler((_e) => {
|
|||
|
|
const selected = this.getSelectedComponent();
|
|||
|
|
const items: MenuItemConfig[] = [];
|
|||
|
|
|
|||
|
|
if (selected) {
|
|||
|
|
items.push({ id: 'componentDetail', ... });
|
|||
|
|
}
|
|||
|
|
items.push({ id: 'showAll', ... });
|
|||
|
|
|
|||
|
|
return items;
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Consequences**:
|
|||
|
|
- ✅ 代码简洁,逻辑集中
|
|||
|
|
- ✅ 无需手动同步菜单状态
|
|||
|
|
- ✅ 易于扩展(添加更多条件判断)
|
|||
|
|
- ⚠️ 每次右键都重新生成(但开销可忽略)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Decision 3: ComponentDetailManager 不注册为 BaseDialogManager
|
|||
|
|
|
|||
|
|
**Context**:
|
|||
|
|
- ComponentDetailManager 需要创建对话框
|
|||
|
|
- BaseDialogManager 提供了对话框生命周期管理
|
|||
|
|
- 现有的 MeasureDialogManager、SectionPlaneDialogManager 等都继承 BaseDialogManager
|
|||
|
|
|
|||
|
|
**Options Considered**:
|
|||
|
|
1. 继承 BaseDialogManager(与现有 Manager 一致)
|
|||
|
|
2. 不继承,直接使用 DialogManager API
|
|||
|
|
|
|||
|
|
**Decision**: 选择 Option 2 - 不继承 BaseDialogManager
|
|||
|
|
|
|||
|
|
**Rationale**:
|
|||
|
|
- ComponentDetailManager 的对话框逻辑简单,无需复杂生命周期管理
|
|||
|
|
- 无需 Panel 组件(直接使用 Collapse + Description)
|
|||
|
|
- 避免继承带来的不必要复杂性
|
|||
|
|
- 直接调用 DialogManager.create() 更灵活
|
|||
|
|
|
|||
|
|
**Implementation**:
|
|||
|
|
```typescript
|
|||
|
|
export class ComponentDetailManager {
|
|||
|
|
private dialog: BimDialog | null = null;
|
|||
|
|
|
|||
|
|
public show(modelUrl: string, componentId: string): void {
|
|||
|
|
this.createDialog();
|
|||
|
|
// ...
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private createDialog(): void {
|
|||
|
|
const registry = ManagerRegistry.getInstance();
|
|||
|
|
this.dialog = registry.dialog?.create({ ... });
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Consequences**:
|
|||
|
|
- ✅ 代码更简洁(119 行 vs 预计 200+ 行)
|
|||
|
|
- ✅ 职责明确:Manager 只负责数据获取和展示
|
|||
|
|
- ⚠️ 与现有 Manager 不一致(但合理,因需求不同)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Decision 4: 属性数据在 Manager 层转换而非 UI 组件层
|
|||
|
|
|
|||
|
|
**Context**:
|
|||
|
|
- 底层 API 返回格式: `{ properties: [{ name, children: [...] }] }`
|
|||
|
|
- BimCollapse 需要格式: `{ items: [{ categoryName, items: [...] }] }`
|
|||
|
|
|
|||
|
|
**Options Considered**:
|
|||
|
|
1. ComponentDetailManager 转换数据后传给 UI
|
|||
|
|
2. 直接传原始数据,UI 组件自己转换
|
|||
|
|
3. 创建 Adapter 类专门处理转换
|
|||
|
|
|
|||
|
|
**Decision**: 选择 Option 1 - Manager 层转换
|
|||
|
|
|
|||
|
|
**Rationale**:
|
|||
|
|
- Manager 职责包括数据适配
|
|||
|
|
- UI 组件保持纯粹(只负责展示)
|
|||
|
|
- 转换逻辑集中,易于维护
|
|||
|
|
- 无需额外的 Adapter 类(简单转换)
|
|||
|
|
|
|||
|
|
**Implementation**:
|
|||
|
|
```typescript
|
|||
|
|
private renderProperties(data: any): void {
|
|||
|
|
const categories = data.properties.map((cat: any) => ({
|
|||
|
|
categoryName: cat.name,
|
|||
|
|
items: cat.children.map((child: any) => ({
|
|||
|
|
key: child.name,
|
|||
|
|
value: child.value
|
|||
|
|
}))
|
|||
|
|
}));
|
|||
|
|
|
|||
|
|
const collapse = new BimCollapse({ items: categories, ... });
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Consequences**:
|
|||
|
|
- ✅ UI 组件可复用性更强
|
|||
|
|
- ✅ 数据转换逻辑集中
|
|||
|
|
- ⚠️ Manager 职责略有增加(但合理)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Decision 5: "显示全部"功能暂时只打印日志
|
|||
|
|
|
|||
|
|
**Context**:
|
|||
|
|
- 右键菜单需要"显示全部"选项
|
|||
|
|
- 底层 API 尚未明确(可能是 showAllComponents、resetVisibility 等)
|
|||
|
|
|
|||
|
|
**Options Considered**:
|
|||
|
|
1. 实现完整功能(调用底层 API)
|
|||
|
|
2. 暂时只打印日志,等 API 明确后实现
|
|||
|
|
3. 跳过此功能
|
|||
|
|
|
|||
|
|
**Decision**: 选择 Option 2 - 暂时只打印日志
|
|||
|
|
|
|||
|
|
**Rationale**:
|
|||
|
|
- 不阻塞主功能(构件详情)
|
|||
|
|
- 底层 API 不明确,避免错误实现
|
|||
|
|
- 菜单结构已就位,后续补充实现即可
|
|||
|
|
- 符合 MVP 原则
|
|||
|
|
|
|||
|
|
**Implementation**:
|
|||
|
|
```typescript
|
|||
|
|
public showAllComponents(): void {
|
|||
|
|
console.log('[EngineManager] 显示全部');
|
|||
|
|
// TODO: 调用底层 API 显示所有构件
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Consequences**:
|
|||
|
|
- ✅ 不阻塞主功能开发
|
|||
|
|
- ✅ 菜单结构完整
|
|||
|
|
- ⚠️ 用户点击后无实际效果(需后续补充)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Decision 6: 文档重构为多章节而非单独创建新文档
|
|||
|
|
|
|||
|
|
**Context**:
|
|||
|
|
- 现有 TOOLBAR_API_CALLCHAIN.md 只记录 Toolbar 调用链
|
|||
|
|
- 新增右键菜单和构件交互功能
|
|||
|
|
- 未来可能还有更多功能模块
|
|||
|
|
|
|||
|
|
**Options Considered**:
|
|||
|
|
1. 创建独立文档 RIGHTKEY_API_CALLCHAIN.md
|
|||
|
|
2. 重构现有文档为多章节结构
|
|||
|
|
3. 合并到 README 或其他文档
|
|||
|
|
|
|||
|
|
**Decision**: 选择 Option 2 - 重构为多章节 API_CALLCHAIN.md
|
|||
|
|
|
|||
|
|
**Rationale**:
|
|||
|
|
- 统一的调用链文档便于查阅
|
|||
|
|
- 支持未来扩展(第四章、第五章...)
|
|||
|
|
- 避免文档碎片化
|
|||
|
|
- 保持现有内容(第一章),降低风险
|
|||
|
|
|
|||
|
|
**Structure**:
|
|||
|
|
```
|
|||
|
|
# BIM Engine SDK - API 调用链文档
|
|||
|
|
|
|||
|
|
## 第一章:工具栏 (Toolbar)
|
|||
|
|
- 首页、框选放大、测量、剖切...
|
|||
|
|
|
|||
|
|
## 第二章:右键菜单 (Context Menu)
|
|||
|
|
- 构件详情、显示全部、信息、首页
|
|||
|
|
|
|||
|
|
## 第三章:构件交互 (Component Interaction)
|
|||
|
|
- 构件选中、取消选中
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Consequences**:
|
|||
|
|
- ✅ 文档结构更清晰
|
|||
|
|
- ✅ 易于扩展
|
|||
|
|
- ✅ 避免重复内容(Info、Home 在多处复用)
|
|||
|
|
- ⚠️ 文件变大(734 → 1232 行)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Decision 7: BimEngine 不自动初始化 ComponentDetailManager
|
|||
|
|
|
|||
|
|
**Context**:
|
|||
|
|
- 现有 Manager(Measure、SectionPlane 等)都有独立的 init 方法
|
|||
|
|
- ComponentDetailManager 是新增功能
|
|||
|
|
- 需要决定初始化时机
|
|||
|
|
|
|||
|
|
**Options Considered**:
|
|||
|
|
1. 在 BimEngine.init() 中自动初始化
|
|||
|
|
2. 提供 initComponentDetail() 方法,由用户选择是否初始化
|
|||
|
|
3. 延迟初始化(首次使用时)
|
|||
|
|
|
|||
|
|
**Decision**: 选择 Option 1(实际实现)- 在 BimEngine.init() 中自动初始化
|
|||
|
|
|
|||
|
|
**Rationale**:
|
|||
|
|
- 构件详情是核心功能,大部分项目都需要
|
|||
|
|
- 与现有 Manager 初始化方式保持一致
|
|||
|
|
- 避免用户忘记初始化导致功能不可用
|
|||
|
|
- 初始化成本低(只是实例化)
|
|||
|
|
|
|||
|
|
**Implementation**:
|
|||
|
|
```typescript
|
|||
|
|
// src/bim-engine.ts
|
|||
|
|
private init() {
|
|||
|
|
// ...
|
|||
|
|
this.componentDetail = new ComponentDetailManager();
|
|||
|
|
this.registry.componentDetail = this.componentDetail;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Consequences**:
|
|||
|
|
- ✅ 开箱即用
|
|||
|
|
- ✅ 与现有 Manager 一致
|
|||
|
|
- ⚠️ 即使不使用也会初始化(但开销可忽略)
|
|||
|
|
|
|||
|
|
**Note**: 实际实现中选择了自动初始化,与 Plan 中的"提供 initComponentDetail()"不同,但更符合现有架构。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Summary
|
|||
|
|
|
|||
|
|
**Core Decisions**:
|
|||
|
|
1. ✅ 状态存储在 Engine(数据源头)
|
|||
|
|
2. ✅ 动态菜单生成(简洁高效)
|
|||
|
|
3. ✅ 不继承 BaseDialogManager(需求简单)
|
|||
|
|
4. ✅ Manager 层转换数据(职责明确)
|
|||
|
|
5. ✅ "显示全部"暂时占位(不阻塞)
|
|||
|
|
6. ✅ 文档多章节结构(统一管理)
|
|||
|
|
7. ✅ 自动初始化(开箱即用)
|
|||
|
|
|
|||
|
|
**Guiding Principles**:
|
|||
|
|
- **KISS**: Keep It Simple, Stupid
|
|||
|
|
- **YAGNI**: You Aren't Gonna Need It
|
|||
|
|
- **Single Responsibility**: 每个组件职责明确
|
|||
|
|
- **Consistency**: 与现有代码保持一致(除非有充分理由)
|