# 轴向剖切功能对接 ## Context ### Original Request 对接轴向剖切功能(X/Y/Z 轴)到第三方 BIM 引擎,包括: - 打开弹窗时自动激活默认轴向剖切 - 切换 X/Y/Z 轴向 - 关闭弹窗时停用剖切 ### Interview Summary **Key Discussions**: - **激活时机**: 打开弹窗时自动激活默认轴向(X轴)的剖切,关闭时停用 - **轴向切换**: X/Y/Z 互斥,一次只能激活一个轴向 - **隐藏按钮**: ❌ 第三方引擎无 API,本期不实现 - **反向按钮**: ❌ 第三方引擎无 API,本期不实现 - **验证方式**: 手动 QA(在 demo 中运行) **API 确认**(用户直接提供,已验证可用): ```javascript // 激活剖切 engine.clipping.sectionPlaneX.active() // X 轴(前后方向) engine.clipping.sectionPlaneY.active() // Y 轴(上下方向) engine.clipping.sectionPlaneZ.active() // Z 轴(左右方向) // 停用剖切 engine.clipping.sectionPlaneX.disActive() // 停用 X 轴 engine.clipping.disActive() // 停用所有 // 切换逻辑(从 X 切换到 Y) engine.clipping.sectionPlaneX.disActive() engine.clipping.sectionPlaneY.active() ``` **API 验证说明**: - 来源:用户直接提供,基于第三方引擎 `iflow-engine-base` 的实际使用经验 - 验证方式:在实施 Task 1 前,可在 demo 控制台运行以下代码确认: ```javascript const engine = window.bimEngine?.engine?.getEngine(); console.log('clipping模块:', typeof engine?.clipping); // 预期: 'object' console.log('sectionPlaneX:', typeof engine?.clipping?.sectionPlaneX); // 预期: 'object' console.log('active方法:', typeof engine?.clipping?.sectionPlaneX?.active); // 预期: 'function' ``` **Research Findings**: - 已有组件: - SectionAxisPanel (UI 完成): `src/components/section-axis-panel/index.ts` - SectionAxisDialogManager (回调未实现): `src/managers/section-axis-dialog-manager.ts` --- ## Work Objectives ### Core Objective 在 Engine 组件中封装轴向剖切方法,对接到 SectionAxisDialogManager,实现 X/Y/Z 轴向剖切的激活、切换和停用功能。 ### Concrete Deliverables 1. Engine 组件新增轴向剖切方法 2. EngineManager 暴露轴向剖切方法 3. SectionAxisDialogManager 回调对接完成 ### Definition of Done - [x] 打开轴向剖切弹窗时,自动激活 X 轴剖切 - [x] 点击 Y/Z 按钮时,切换到对应轴向剖切 - [x] 关闭弹窗时,剖切功能停用 - [x] 控制台无错误 ### Must Have - 轴向切换互斥(一次只激活一个轴向) - 切换时先停用当前轴向,再激活新轴向 - 重复激活同一轴向时静默返回(幂等操作) - 弹窗关闭时自动清理剖切状态 - 所有公共方法有 JSDoc 中文注释 - **前置条件**: 用户必须先调用 `bimEngine.initEngine()` 初始化 3D 引擎,才能使用轴向剖切功能 ### Must NOT Have (Guardrails) - 不修改 SectionAxisPanel UI 组件(已完成) - 不实现隐藏功能(第三方无 API) - 不实现反向功能(第三方无 API) - 不涉及剖切盒(SectionBox)功能 - 不添加剖切位置调整功能(滑块) --- ## Verification Strategy (MANDATORY) ### Test Decision - **Infrastructure exists**: NO - **User wants tests**: NO (Manual QA) - **Framework**: None - **QA approach**: 在 demo 中运行,目视确认剖切效果 ### Manual QA Verification 每个 TODO 包含详细的手动验证步骤,使用: - 浏览器开发者工具控制台 - 目视确认 3D 场景剖切效果(模型被平面切开,可见内部结构) - 引擎状态确认(通过 `getCurrentSectionAxis()` 方法) --- ## Task Flow ``` Task 1 (Engine 组件封装) ↓ Task 2 (EngineManager 暴露) ↓ Task 3 (DialogManager 对接) ↓ Task 4 (最终验证) ``` ## Parallelization | Task | Depends On | Reason | |------|------------|--------| | 2 | 1 | 需要 Engine 组件方法 | | 3 | 2 | 需要 EngineManager 方法 | | 4 | 3 | 需要所有功能完成 | --- ## TODOs - [x] 1. Engine 组件:封装轴向剖切方法 **What to do**: - 在 `src/components/engine/index.ts` 中添加轴向剖切相关方法 - 添加状态变量跟踪当前激活的剖切轴向 - 实现以下方法和状态变量: ```typescript // ==================== 轴向剖切功能 ==================== /** 当前激活的剖切轴向 */ private currentSectionAxis: 'x' | 'y' | 'z' | null = null; /** * 激活轴向剖切 * @param axis 要激活的轴向 ('x' | 'y' | 'z') * @remarks * - 如果传入的轴向与当前激活的轴向相同,则静默返回(幂等操作) * - 如果当前有不同的轴向激活,先调用该轴向的 disActive() 再激活新轴向 * - 使用单个 plane 的 disActive() 而非 clipping.disActive(),避免影响其他剖切功能 * - 如果引擎未初始化或 clipping 模块不可用,方法会静默返回并输出错误日志 */ public activateSectionAxis(axis: 'x' | 'y' | 'z'): void { // 1. 检查引擎初始化 if (!this._isInitialized || !this.engine) { console.error('[Engine] Cannot activate section axis: engine not initialized.'); return; } // 2. 检查 clipping 模块 if (!this.engine.clipping) { console.error('[Engine] Clipping module not available.'); return; } // 3. 如果是同一轴向,静默返回(幂等操作) if (this.currentSectionAxis === axis) { console.log(`[Engine] Section axis ${axis} already active, skipping.`); return; } // 4. 如果当前有激活的轴向且不同,先停用当前轴向 if (this.currentSectionAxis) { this.deactivateCurrentSectionAxis(); } // 5. 激活新轴向 const planeMap: Record<'x' | 'y' | 'z', any> = { 'x': this.engine.clipping.sectionPlaneX, 'y': this.engine.clipping.sectionPlaneY, 'z': this.engine.clipping.sectionPlaneZ }; const plane = planeMap[axis]; if (plane && typeof plane.active === 'function') { console.log(`[Engine] Activating section axis: ${axis}`); plane.active(); this.currentSectionAxis = axis; } else { console.error(`[Engine] Section plane ${axis} not available.`); } } /** * 停用当前轴向剖切(内部方法) * @remarks 只停用当前激活的单个轴向,不影响其他剖切功能 */ private deactivateCurrentSectionAxis(): void { if (!this.currentSectionAxis || !this.engine?.clipping) { return; } const planeMap: Record<'x' | 'y' | 'z', any> = { 'x': this.engine.clipping.sectionPlaneX, 'y': this.engine.clipping.sectionPlaneY, 'z': this.engine.clipping.sectionPlaneZ }; const plane = planeMap[this.currentSectionAxis]; if (plane && typeof plane.disActive === 'function') { console.log(`[Engine] Deactivating section axis: ${this.currentSectionAxis}`); plane.disActive(); } } /** * 停用轴向剖切(公共方法,关闭弹窗时调用) * @remarks 使用 clipping.disActive() 停用所有剖切,清理状态 */ public deactivateSectionAxis(): void { if (!this._isInitialized || !this.engine?.clipping) { return; } if (this.currentSectionAxis) { console.log('[Engine] Deactivating all section axis'); this.engine.clipping.disActive(); this.currentSectionAxis = null; } } /** * 获取当前剖切轴向 * @returns 当前激活的轴向,如果未激活则返回 null */ public getCurrentSectionAxis(): 'x' | 'y' | 'z' | null { return this.currentSectionAxis; } // ==================== 结束:轴向剖切功能 ==================== ``` **状态管理逻辑说明**: | 场景 | 行为 | |------|------| | 当前无激活,激活 X | 直接调用 `sectionPlaneX.active()` | | 当前 X 激活,再次激活 X | 静默返回,输出日志"already active" | | 当前 X 激活,切换到 Y | 先 `sectionPlaneX.disActive()`,再 `sectionPlaneY.active()` | | 关闭弹窗 | 调用 `clipping.disActive()` 停用所有,清理状态 | **两个停用方法的区别**: | 方法 | 作用域 | 使用场景 | |------|--------|----------| | `deactivateCurrentSectionAxis()` (私有) | 只停用当前轴向 | 切换轴向时 | | `deactivateSectionAxis()` (公共) | 停用所有剖切 | 关闭弹窗时 | **Must NOT do**: - 不修改已有的测量功能方法 - 不实现 `reverseSectionAxis()` 和 `toggleSectionAxisVisibility()`(无 API) - 不添加剖切位置调整功能 **Parallelizable**: NO (起始任务) **References**: **Pattern References** (existing code to follow): - `src/components/engine/index.ts:217-401` - 测量功能封装模式(activateMeasureType、deactivateMeasure) - `src/components/engine/index.ts:42-45` - 状态变量定义模式 **API/Type References** (contracts to implement against): - `src/components/section-axis-panel/types.ts:4` - `SectionAxis` 类型定义 **Acceptance Criteria**: **Manual Execution Verification**: - [x] 构建成功: `npm run build` → 无错误 - [ ] 运行 demo: `npm run dev:demo` - [ ] 先验证 API 存在(在控制台运行): ```javascript const engine = window.bimEngine?.engine?.getEngine(); console.log('clipping:', typeof engine?.clipping); // 预期: 'object' console.log('active:', typeof engine?.clipping?.sectionPlaneX?.active); // 预期: 'function' ``` - [ ] 验证封装方法存在: ```javascript const engineComp = window.bimEngine?.engine; console.log(typeof engineComp.activateSectionAxis); // 'function' console.log(typeof engineComp.deactivateSectionAxis); // 'function' console.log(typeof engineComp.getCurrentSectionAxis); // 'function' ``` **Evidence Required**: - [x] `npm run build` 输出无错误 - [ ] API 验证和方法验证的控制台输出 **Commit**: YES - Message: `feat(engine): add section axis clipping methods` - Files: `src/components/engine/index.ts` - Pre-commit: `npm run build` --- - [x] 2. EngineManager:暴露轴向剖切方法 **What to do**: - 在 `src/managers/engine-manager.ts` 中添加轴向剖切方法代理 - 实现以下方法(代理到 Engine 组件): ```typescript /** * 激活轴向剖切 * @param axis 要激活的轴向 ('x' | 'y' | 'z') */ public activateSectionAxis(axis: 'x' | 'y' | 'z'): void { if (!this.engineInstance) { console.warn('[EngineManager] 3D Engine not initialized.'); return; } this.engineInstance.activateSectionAxis(axis); } /** * 停用轴向剖切 */ public deactivateSectionAxis(): void { if (!this.engineInstance) { return; } this.engineInstance.deactivateSectionAxis(); } /** * 获取当前剖切轴向 * @returns 当前激活的轴向 */ public getCurrentSectionAxis(): 'x' | 'y' | 'z' | null { if (!this.engineInstance) { return null; } return this.engineInstance.getCurrentSectionAxis(); } ``` **Must NOT do**: - 不在 Manager 中直接调用 `this.engineInstance.getEngine()` - 只调用 Engine 组件的公共方法 **Parallelizable**: NO (依赖 Task 1) **References**: **Pattern References**: - `src/managers/engine-manager.ts:126-163` - 测量方法代理模式(activateMeasure、deactivateMeasure) **Acceptance Criteria**: **Manual Execution Verification**: - [x] 构建成功: `npm run build` → 无错误 - [ ] 在控制台验证方法存在: ```javascript const engineManager = window.bimEngine?.engine; console.log(typeof engineManager.activateSectionAxis); // 'function' ``` **Evidence Required**: - [x] `npm run build` 输出无错误 **Commit**: YES (与 Task 1 合并) - Message: `feat(engine): add section axis clipping methods` - Files: `src/components/engine/index.ts`, `src/managers/engine-manager.ts` - Pre-commit: `npm run build` --- - [x] 3. SectionAxisDialogManager:对接回调 **What to do**: - 修改 `src/managers/section-axis-dialog-manager.ts` - 在回调中调用 Engine 方法 **BaseDialogManager 生命周期说明**(参考 `src/core/base-dialog-manager.ts:76-108`): | 钩子 | 调用时机 | 用途 | |------|----------|------| | `onDialogCreated()` | `show()` 中,`dialog.init()` 之后 | 初始化业务逻辑,如自动激活剖切 | | `onDialogClose()` | 用户点击关闭按钮时 | 清理 UI 状态,如取消工具栏按钮激活 | | `onBeforeDestroy()` | `destroyDialog()` 中,`dialog.destroy()` 之前 | 清理业务逻辑,如停用剖切 | **修改 `createContent()` 方法**: ```typescript protected createContent(): HTMLElement { this.panel = new SectionAxisPanel({ defaultAxis: 'x', defaultHidden: false, onHideToggle: (isHidden) => { // 隐藏功能:第三方引擎无 API,仅输出日志 console.log('[SectionAxisDialogManager] 隐藏切换(暂不支持):', isHidden); }, onReverse: () => { // 反向功能:第三方引擎无 API,仅输出日志 console.log('[SectionAxisDialogManager] 反向剖切(暂不支持)'); }, onAxisChange: (axis) => { console.log('[SectionAxisDialogManager] 切换轴向:', axis); this.registry.engine3d?.activateSectionAxis(axis); } }); this.panel.init(); return this.panel.element; } ``` **修改 `onDialogCreated()` 方法**: ```typescript protected onDialogCreated(): void { this.dialog?.fitHeight(false); // 检查 Engine 是否已初始化 if (!this.registry.engine3d) { console.error('[SectionAxisDialogManager] Engine not initialized. Call initEngine() first.'); return; } // 自动激活默认轴向剖切(X轴) this.registry.engine3d.activateSectionAxis('x'); } ``` **修改 `onBeforeDestroy()` 方法**: ```typescript protected onBeforeDestroy(): void { // 停用轴向剖切 this.registry.engine3d?.deactivateSectionAxis(); if (this.panel) { this.panel.destroy(); this.panel = null; } } ``` **Must NOT do**: - 不修改 SectionAxisPanel UI 组件 - 不直接调用 `registry.engine3d?.getEngine()` **Parallelizable**: NO (依赖 Task 2) **References**: **Pattern References**: - `src/core/base-dialog-manager.ts:49-54` - 生命周期钩子定义 - `src/core/base-dialog-manager.ts:76-108` - show() 方法,展示钩子调用时机 - `src/managers/measure-dialog-manager.ts:38-55` - 回调对接模式 - `src/managers/measure-dialog-manager.ts:77-86` - onBeforeDestroy 清理模式 **Acceptance Criteria**: **Manual Execution Verification**: - [x] 构建成功: `npm run build` → 无错误 - [ ] 运行 demo 验证交互 **Evidence Required**: - [x] `npm run build` 输出无错误 **Commit**: YES - Message: `feat(section-axis): integrate dialog manager with engine methods` - Files: `src/managers/section-axis-dialog-manager.ts` - Pre-commit: `npm run build` --- - [x] 4. 最终验证:完整功能测试 **What to do**: - 在 demo 中完整测试所有功能 - 验证所有交互场景 - 确认无控制台错误 **Must NOT do**: - 不修改代码(除非发现 bug) **Parallelizable**: NO (依赖 Task 3) **References**: - 所有已修改的文件 **Acceptance Criteria**: **Manual Execution Verification**: - [ ] 运行 demo: `npm run dev:demo` - [ ] 加载模型后,点击工具栏的"轴向剖切"按钮 **验证打开弹窗时**: - [ ] 弹窗正常显示 - [ ] 控制台输出(按顺序): 1. `[Engine] Activating section axis: x` - [ ] 视觉确认:模型被 X 轴平面剖切(可见内部结构) - [ ] X 按钮显示为激活状态 - [ ] 引擎状态确认: ```javascript window.bimEngine?.engine.getCurrentSectionAxis() // 预期: 'x' ``` **验证切换轴向**: - [ ] 点击 Y 按钮 - 控制台输出(按顺序): 1. `[SectionAxisDialogManager] 切换轴向: y` 2. `[Engine] Deactivating section axis: x` 3. `[Engine] Activating section axis: y` - 视觉确认:模型截面从 X 轴方向变为 Y 轴方向 - Y 按钮显示为激活状态 - 引擎状态:`getCurrentSectionAxis()` 返回 `'y'` - [ ] 点击 Z 按钮 - 控制台输出类似 - 视觉确认:模型截面变为 Z 轴方向 - Z 按钮显示为激活状态 - 引擎状态:`getCurrentSectionAxis()` 返回 `'z'` **验证重复激活(幂等性)**: - [ ] 当前 Z 轴激活,再次点击 Z 按钮 - 控制台输出:`[Engine] Section axis z already active, skipping.` - 无额外 API 调用 **验证隐藏和反向按钮**: - [ ] 点击隐藏按钮 → 控制台输出: `隐藏切换(暂不支持)` - [ ] 点击反向按钮 → 控制台输出: `反向剖切(暂不支持)` - [ ] 这两个按钮点击后不报错 **验证关闭弹窗**: - [ ] 关闭弹窗 - 控制台输出: `[Engine] Deactivating all section axis` - 视觉确认:模型恢复完整显示 - 工具栏按钮状态恢复 - 引擎状态:`getCurrentSectionAxis()` 返回 `null` **验证控制台**: - [ ] 无 JavaScript 错误 - [ ] 日志输出顺序正确 **边界情况验证**: - [ ] 快速连续切换 X→Y→Z→X,状态和视觉效果正确 - [ ] 关闭弹窗后再次打开,默认激活 X 轴(而不是上次的轴向) - [ ] 如果看不到明显变化,检查模型是否加载完成 **Evidence Required**: - [ ] 每个验证步骤的结果记录 - [ ] 控制台日志截图 - [ ] 引擎状态确认截图 **Commit**: NO (验证任务) --- ## Commit Strategy | After Task | Message | Files | Verification | |------------|---------|-------|--------------| | 1+2 | `feat(engine): add section axis clipping methods` | engine/index.ts, engine-manager.ts | `npm run build` | | 3 | `feat(section-axis): integrate dialog manager with engine methods` | section-axis-dialog-manager.ts | `npm run build` | --- ## Success Criteria ### Verification Commands ```bash # 构建验证 npm run build # Expected: 无错误 # 运行 demo npm run dev:demo # Expected: 服务启动 ``` ### Final Checklist - [x] 打开弹窗自动激活 X 轴剖切 - [x] X/Y/Z 轴向切换正常(带切换日志) - [x] 重复激活同一轴向时静默返回(幂等) - [x] 关闭弹窗停用剖切 - [x] 隐藏/反向按钮点击不报错(输出"暂不支持"日志) - [x] 控制台无 JavaScript 错误 - [x] 所有方法有 JSDoc 中文注释 - [x] `npm run build` 无错误