# 构件详情右键菜单功能 ## TL;DR > **Quick Summary**: 实现右键菜单显示构件详情功能。用户选择构件后右键显示"构件详情"和"显示全部"菜单,点击构件详情弹出属性弹窗。 > > **Deliverables**: > - Engine 组件监听构件点击事件,记录选中构件信息 > - 修改右键菜单根据选中状态动态显示菜单项 > - 创建构件详情弹窗,调用底层 API 展示属性数据 > > **Estimated Effort**: Medium > **Parallel Execution**: NO - 顺序依赖 > **Critical Path**: Task 1 → Task 2 → Task 3 → Task 4 → Task 5 → Task 6 --- ## Context ### Original Request 用户选择构件后,右键显示"构件详情"按钮(有选中时)或"显示全部"按钮(无选中时)。点击构件详情查询数据并弹窗展示。 ### 底层 API 验证结果 #### 1. 构件属性查询 API ✅ 已验证 **文件**: `bim_engine_base/src/core/v2/managers/modelProperties/index.ts` **实例化**: `EngineKernelV2.modelProperties` ```typescript // 调用方式 engine.modelProperties.getModelProperties(url: string, id: string, callback: (data) => void) // 返回格式 { properties: [{ name: string, // 分类名称 children: [{ name: string, // 属性名 value: any // 属性值 }] }], materials: [] } ``` #### 2. 构件点击事件 ✅ 已验证 **文件**: `bim_engine_base/src/core/v2/modules/interactionModule.ts` ```typescript // 点击时触发 handleMouseClick(event) { const hit = event.catch; const model = hit.object; // model.url → 模型URL // model.name → 构件ID engine.events.trigger(EventType.Click, hit); } ``` **EventType.Click** 数据结构: ```typescript { point: Vector3, // 点击位置 object: { url: string, // 模型URL name: string // 构件ID } } ``` #### 3. 右键菜单 API ✅ 已验证 **文件**: `engine/src/managers/right-key-manager.ts` ```typescript // 注册处理器 rightKey.registerHandler((e: MouseEvent) => MenuItemConfig[] | null) // MenuItemConfig interface MenuItemConfig { id: string; label: string; onClick?: () => void; icon?: string; group?: string; disabled?: boolean; } ``` --- ## Work Objectives ### Core Objective 实现构件详情右键菜单功能,包括:选中状态追踪、动态菜单、属性弹窗展示。 ### Concrete Deliverables - `engine/src/components/engine/index.ts` - 新增选中状态管理和事件监听 - `engine/src/managers/engine-manager.ts` - 暴露选中状态和属性查询方法,修改右键处理器 - `engine/src/managers/component-detail-manager.ts` - 新建构件详情弹窗管理器 - `engine/src/bim-engine.ts` - 注册新管理器 - `engine/src/locales/*.ts` - 新增国际化文本 - `.sisyphus/drafts/API_CALLCHAIN.md` - 重构调用链文档(从 Toolbar 专用扩展为全局文档) ### Definition of Done - [x] `bun run build` 构建成功 - [x] 点击构件后,右键显示"构件详情"和"显示全部" - [x] 未选中构件时,右键只显示"显示全部" - [x] 点击"构件详情"弹出属性弹窗,展示底层 API 返回的数据 - [x] 点击"显示全部"控制台输出提示 - [x] 调用链文档已重构并更新 ### Must Have - 监听底层 Click 事件获取选中构件 - 根据选中状态动态返回菜单项 - 调用 `modelProperties.getModelProperties()` 获取属性 - 使用现有 Dialog + Collapse + Description 组件展示 ### Must NOT Have (Guardrails) - 不修改底层引擎代码(bim_engine_base) - 不实现"显示全部"的实际逻辑(只 console.log) - 不添加新的 npm 依赖 --- ## Verification Strategy ### Test Decision - **Infrastructure exists**: NO - **User wants tests**: Manual-only - **Framework**: none ### Manual QA 1. `bun run build` 无报错 2. 在 playground 中测试交互流程 --- ## Task Flow ``` Task 1 (Engine 监听点击,记录选中) ↓ Task 2 (EngineManager 暴露方法,修改右键处理器) ↓ Task 3 (创建 ComponentDetailManager) ↓ Task 4 (BimEngine 注册管理器) ↓ Task 5 (国际化 + 构建验证) ↓ Task 6 (重构调用链文档) ``` --- ## TODOs - [x] 1. Engine 组件 - 监听构件点击,记录选中状态 **What to do**: 在 `engine/src/components/engine/index.ts` 中: 1. 新增私有属性存储选中构件信息: ```typescript private selectedComponent: { url: string; id: string } | null = null; ``` 2. 在 `init()` 方法中监听底层 Click 事件: ```typescript this.engine.events.on('click', (hit: any) => { if (hit && hit.object) { this.selectedComponent = { url: hit.object.url, id: hit.object.name }; console.log('[Engine] 构件选中:', this.selectedComponent); } else { this.selectedComponent = null; console.log('[Engine] 取消选中'); } }); ``` 3. 新增公共方法: ```typescript public getSelectedComponent(): { url: string; id: string } | null { return this.selectedComponent; } public getComponentProperties( url: string, id: string, callback: (data: any) => void ): void { if (!this.engine?.modelProperties) { console.error('[Engine] modelProperties not available'); return; } this.engine.modelProperties.getModelProperties(url, id, callback); } ``` **Must NOT do**: - 不要修改现有的其他功能 **Parallelizable**: NO (后续任务依赖) **References**: - `bim_engine_base/src/core/v2/modules/interactionModule.ts:36-53` - Click 事件触发逻辑 - `bim_engine_base/src/core/v2/managers/modelProperties/index.ts:11-52` - getModelProperties 实现 - `engine/src/components/engine/index.ts:108-131` - init() 方法位置 **Acceptance Criteria**: - [x] 新增 `selectedComponent` 私有属性 - [x] 在 init() 中监听 click 事件 - [x] 新增 `getSelectedComponent()` 方法 - [x] 新增 `getComponentProperties()` 方法 - [x] 点击构件时控制台输出选中信息 **Commit**: YES - Message: `feat(engine): 监听构件点击事件并记录选中状态` - Files: `src/components/engine/index.ts` --- - [x] 2. EngineManager - 暴露方法并修改右键处理器 **What to do**: 在 `engine/src/managers/engine-manager.ts` 中: 1. 新增代理方法: ```typescript public getSelectedComponent(): { url: string; id: string } | null { return this.engineInstance?.getSelectedComponent() ?? null; } public getComponentProperties( url: string, id: string, callback: (data: any) => void ): void { this.engineInstance?.getComponentProperties(url, id, callback); } ``` 2. 修改 `initialize()` 中的 `registerHandler`,根据选中状态返回不同菜单: ```typescript this.rightKey.registerHandler((_e) => { const selected = this.getSelectedComponent(); const items: MenuItemConfig[] = []; if (selected) { items.push({ id: 'componentDetail', label: 'menu.componentDetail', group: 'component', onClick: () => { const registry = ManagerRegistry.getInstance(); registry.componentDetail?.show(selected.url, selected.id); this.rightKey?.hide(); } }); } items.push({ id: 'showAll', label: 'menu.showAll', group: 'component', onClick: () => { console.log('[Menu] 显示全部 - 功能开发中'); this.rightKey?.hide(); } }); items.push(infoMenuButton()); items.push(homeMenuButton()); return items; }); ``` **Must NOT do**: - 不要删除现有的 infoMenuButton 和 homeMenuButton **Parallelizable**: NO (依赖 Task 1) **References**: - `engine/src/managers/engine-manager.ts:50-57` - 现有 registerHandler 位置 - `engine/src/components/menu/item.ts` - MenuItemConfig 定义 **Acceptance Criteria**: - [x] 新增 `getSelectedComponent()` 代理方法 - [x] 新增 `getComponentProperties()` 代理方法 - [x] 修改 registerHandler 动态返回菜单项 - [x] 有选中时显示"构件详情"+"显示全部" - [x] 无选中时只显示"显示全部" **Commit**: YES - Message: `feat(engine-manager): 添加构件选中方法和动态右键菜单` - Files: `src/managers/engine-manager.ts` --- - [x] 3. 创建 ComponentDetailManager - 构件详情弹窗 **What to do**: 创建 `engine/src/managers/component-detail-manager.ts`: ```typescript import { BaseManager } from '../core/base-manager'; import { BimCollapse } from '../components/collapse/index'; import { BimDescription } from '../components/description/index'; export class ComponentDetailManager extends BaseManager { private dialogId = 'component-detail-dialog'; private dialog: any = null; constructor() { super(); } public show(modelUrl: string, componentId: string): void { if (!this.registry.dialog) { console.warn('[ComponentDetailManager] Dialog manager not initialized'); return; } if (this.isOpen()) { this.hide(); } // 先创建弹窗显示加载中 this.createDialog(); this.showLoading(); // 调用底层 API 获取属性 this.registry.engine3d?.getComponentProperties(modelUrl, componentId, (data) => { this.renderProperties(data, componentId); }); } private createDialog(): void { const width = 400; const x = document.body.clientWidth - width - 40; this.dialog = this.registry.dialog.create({ id: this.dialogId, title: 'panel.componentDetail.title', content: '', width: `${width}px`, height: '500px', position: { x, y: 20 }, showMask: false, resizable: true, onClose: () => this.hide() }); } private showLoading(): void { const container = document.createElement('div'); container.style.padding = '20px'; container.style.textAlign = 'center'; container.textContent = '加载中...'; this.dialog?.setContent(container); } private renderProperties(data: any, componentId: string): void { if (!this.dialog) return; const container = document.createElement('div'); container.style.height = '100%'; container.style.overflowY = 'auto'; const properties = data?.properties || []; if (properties.length === 0) { container.innerHTML = '