# Bug Fix: Transforms API Error > Date: 2025-12-08 > Branch: feat/08-multifunctional-toolbox > Status: ✅ Fixed --- ## 🐛 问题描述 ### 错误信息 用户点击工具箱中的工具时,浏览器控制台抛出错误: ``` with-tool.ts:85 Uncaught TypeError: Transforms.insertNodes is not a function ``` ### 错误位置 文件: `packages/drawnix/src/plugins/with-tool.ts` 受影响的方法: - `ToolTransforms.insertTool` (第 85 行) - `ToolTransforms.resizeTool` (第 110 行) - `ToolTransforms.moveTool` (第 136 行) - `ToolTransforms.rotateTool` (第 155 行) - `ToolTransforms.removeTool` (第 169 行) - `ToolTransforms.updateToolUrl` (第 184 行) - `ToolTransforms.updateToolMetadata` (第 212 行) --- ## 🔍 根因分析 ### 错误原因 使用了错误的 Plait Transforms API: ```typescript // ❌ 错误 - 这些方法不存在 Transforms.insertNodes(board, [element], { at: [index] }) Transforms.setNodes(board, data, { at: [path] }) Transforms.removeNodes(board, { at: [path] }) ``` ### 正确 API 通过检查 Plait 源码和项目中的其他用法 (如 `with-freehand-create.ts`, `property.ts`, `video.ts`),发现正确的 API 是: ```typescript // ✅ 正确 - 单数形式 Transforms.insertNode(board, element, [index]) Transforms.setNode(board, data, [path]) Transforms.removeNode(board, [path]) ``` **关键区别**: 1. **单数 vs 复数**: Plait 使用 `insertNode` 而不是 `insertNodes` 2. **参数格式**: 直接传递元素和路径数组,而不是使用 `{ at: [...] }` 对象 --- ## ✅ 修复方案 ### 修复的代码变更 #### 1. insertTool (第 85 行) ```diff - Transforms.insertNodes(board, [toolElement], { - at: [board.children.length], - }); + Transforms.insertNode(board, toolElement, [board.children.length]); ``` #### 2. resizeTool (第 110 行) ```diff - Transforms.setNodes(board, newElement, { at: [path] }); + Transforms.setNode(board, newElement, [path]); ``` #### 3. moveTool (第 136 行) ```diff - Transforms.setNodes(board, newElement, { at: [path] }); + Transforms.setNode(board, newElement, [path]); ``` #### 4. rotateTool (第 155 行) ```diff - Transforms.setNodes(board, newElement, { at: [path] }); + Transforms.setNode(board, newElement, [path]); ``` #### 5. removeTool (第 169 行) ```diff - Transforms.removeNodes(board, { at: [path] }); + Transforms.removeNode(board, [path]); ``` #### 6. updateToolUrl (第 184 行) ```diff - Transforms.setNodes(board, { url: newUrl } as Partial, { - at: [path], - }); + Transforms.setNode(board, { url: newUrl } as Partial, [path]); ``` #### 7. updateToolMetadata (第 212 行) ```diff - Transforms.setNodes( - board, - { metadata: newMetadata } as Partial, - { at: [path] } - ); + Transforms.setNode( + board, + { metadata: newMetadata } as Partial, + [path] + ); ``` --- ## 📚 参考代码 ### 正确的 Plait API 用法示例 从项目中已有的正确实现: **with-freehand-create.ts:44** (插入元素): ```typescript Transforms.insertNode(board, temporaryElement, [board.children.length]); ``` **property.ts:41** (更新元素): ```typescript Transforms.setNode(board, { fill: newFill }, path); ``` **video.ts:44** (删除元素): ```typescript Transforms.removeNode(board, [path]); ``` --- ## 🧪 验证 ### 编译检查 修复后,TypeScript 编译器的 6 个错误已全部消失: ``` ✅ 之前的错误: ✘ [Line 110:18] Property 'setNodes' does not exist... Did you mean 'setNode'? ✘ [Line 136:18] Property 'setNodes' does not exist... Did you mean 'setNode'? ✘ [Line 155:18] Property 'setNodes' does not exist... Did you mean 'setNode'? ✘ [Line 169:18] Property 'removeNodes' does not exist... Did you mean 'removeNode'? ✘ [Line 184:18] Property 'setNodes' does not exist... Did you mean 'setNode'? ✘ [Line 212:20] Property 'setNodes' does not exist... Did you mean 'setNode'? ✅ 修复后: 无编译错误 ``` ### 运行时测试 开发服务器成功启动,HMR 正常工作: ```bash ✅ npm start ✅ Vite dev server running at http://localhost:7200 ✅ HMR updates applied successfully ``` --- ## 📝 经验总结 ### 教训 1. **API 使用前先查阅文档**: 在使用 Plait Transforms API 之前,应该查看: - Plait 官方文档 - 项目中的已有实现 (如 `with-freehand-create.ts`) - TypeScript 类型定义 2. **不要假设 API 命名规则**: 虽然 Slate.js 使用 `Transforms.insertNodes` (复数),但 Plait 使用 `Transforms.insertNode` (单数) 3. **利用 TypeScript 提示**: TypeScript 编译器已经提示 `Did you mean 'setNode'?`,应该立即重视这些提示 ### 最佳实践 1. **参考已有代码**: 在实现新功能时,优先参考项目中已有的类似实现 2. **增量开发测试**: 每完成一个 API 调用就应该测试,而不是等到全部完成 3. **关注 TypeScript 错误**: 编译时错误通常能提前发现运行时问题 --- ## 🎯 后续工作 修复完成后,工具箱功能应该能够正常工作: - ✅ 点击工具卡片插入到画布 - ✅ 工具元素在画布中心位置 - ✅ 支持拖拽、缩放、旋转操作 - ⚠️ 需要测试 iframe 内容是否正确加载 (某些 URL 可能有 X-Frame-Options 限制) --- ## 🐛 后续发现的问题 (已修复) ### 问题 2: `Cannot set properties of undefined (setting 'context')` **错误信息**: ``` TypeError: Cannot set properties of undefined (setting 'context') at ListRender.update ``` **根因**: ToolComponent 缺少 `activeGenerator`,导致: 1. 元素没有 `context` 属性 2. 无法计算元素的矩形边界 (`getRectangle`) 3. 选中状态没有高亮边框 ### 问题 3: `can not get rectangle of element` **错误信息**: ``` can not get rectangle of element: {id: 'tool_...', type: 'tool', ...} ``` **根因**: ToolComponent 没有实现 `getRectangle` 方法,Plait 无法获取元素的边界框。 ### 修复方案 参考 `FreehandComponent` 的实现,为 ToolComponent 添加: 1. **添加 activeGenerator**: 使用 `createActiveGenerator` 创建选中状态生成器 2. **实现 getRectangle**: 通过 `RectangleClient.getRectangleByPoints(element.points)` 计算边界 3. **更新 onContextChanged**: 调用 `activeGenerator.processDrawing` 更新选中状态 4. **更新 destroy**: 清理 `activeGenerator` 资源 **修改的代码** (`tool.component.ts`): ```diff import { PlaitBoard, PlaitPluginElementContext, OnContextChanged, + RectangleClient, + ACTIVE_STROKE_WIDTH, } from '@plait/core'; -import { CommonElementFlavour } from '@plait/common'; +import { + CommonElementFlavour, + ActiveGenerator, + createActiveGenerator, + hasResizeHandle, +} from '@plait/common'; export class ToolComponent ... { toolGenerator!: ToolGenerator; + activeGenerator!: ActiveGenerator; initializeGenerator(): void { + // 初始化选中状态生成器 + this.activeGenerator = createActiveGenerator(this.board, { + getRectangle: (element: PlaitTool) => { + return RectangleClient.getRectangleByPoints(element.points); + }, + getStrokeWidth: () => ACTIVE_STROKE_WIDTH, + getStrokeOpacity: () => 1, + hasResizeHandle: () => { + return hasResizeHandle(this.board, this.element); + }, + }); this.toolGenerator = new ToolGenerator(this.board); } initialize(): void { + super.initialize(); this.initializeGenerator(); ... } onContextChanged(...): void { if (value.element !== previous.element || value.hasThemeChanged) { // 更新工具渲染 ... + // 更新选中状态高亮 + this.activeGenerator.processDrawing( + this.element, + PlaitBoard.getActiveHost(this.board), + { selected: this.selected } + ); } else { + // 只有选中状态改变时,只更新高亮 + const needUpdate = value.selected !== previous.selected; + if (needUpdate || value.selected) { + this.activeGenerator.processDrawing( + this.element, + PlaitBoard.getActiveHost(this.board), + { selected: this.selected } + ); + } } } destroy(): void { super.destroy(); + if (this.activeGenerator) { + this.activeGenerator.destroy(); + } ... } } ``` ### 验证结果 ✅ 修复后: - `context` 属性错误消失 - 元素矩形边界正确计算 - 选中状态高亮边框正常显示 - HMR 热更新成功 --- **Created by**: Claude Code **Fixed on**: 2025-12-08 **Status**: ✅ All Issues Resolved