8.5 KiB
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:
// ❌ 错误 - 这些方法不存在
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 是:
// ✅ 正确 - 单数形式
Transforms.insertNode(board, element, [index])
Transforms.setNode(board, data, [path])
Transforms.removeNode(board, [path])
关键区别:
- 单数 vs 复数: Plait 使用
insertNode而不是insertNodes - 参数格式: 直接传递元素和路径数组,而不是使用
{ at: [...] }对象
✅ 修复方案
修复的代码变更
1. insertTool (第 85 行)
- Transforms.insertNodes(board, [toolElement], {
- at: [board.children.length],
- });
+ Transforms.insertNode(board, toolElement, [board.children.length]);
2. resizeTool (第 110 行)
- Transforms.setNodes(board, newElement, { at: [path] });
+ Transforms.setNode(board, newElement, [path]);
3. moveTool (第 136 行)
- Transforms.setNodes(board, newElement, { at: [path] });
+ Transforms.setNode(board, newElement, [path]);
4. rotateTool (第 155 行)
- Transforms.setNodes(board, newElement, { at: [path] });
+ Transforms.setNode(board, newElement, [path]);
5. removeTool (第 169 行)
- Transforms.removeNodes(board, { at: [path] });
+ Transforms.removeNode(board, [path]);
6. updateToolUrl (第 184 行)
- Transforms.setNodes(board, { url: newUrl } as Partial<PlaitTool>, {
- at: [path],
- });
+ Transforms.setNode(board, { url: newUrl } as Partial<PlaitTool>, [path]);
7. updateToolMetadata (第 212 行)
- Transforms.setNodes(
- board,
- { metadata: newMetadata } as Partial<PlaitTool>,
- { at: [path] }
- );
+ Transforms.setNode(
+ board,
+ { metadata: newMetadata } as Partial<PlaitTool>,
+ [path]
+ );
📚 参考代码
正确的 Plait API 用法示例
从项目中已有的正确实现:
with-freehand-create.ts:44 (插入元素):
Transforms.insertNode(board, temporaryElement, [board.children.length]);
property.ts:41 (更新元素):
Transforms.setNode(board, { fill: newFill }, path);
video.ts:44 (删除元素):
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 正常工作:
✅ npm start
✅ Vite dev server running at http://localhost:7200
✅ HMR updates applied successfully
📝 经验总结
教训
-
API 使用前先查阅文档: 在使用 Plait Transforms API 之前,应该查看:
- Plait 官方文档
- 项目中的已有实现 (如
with-freehand-create.ts) - TypeScript 类型定义
-
不要假设 API 命名规则: 虽然 Slate.js 使用
Transforms.insertNodes(复数),但 Plait 使用Transforms.insertNode(单数) -
利用 TypeScript 提示: TypeScript 编译器已经提示
Did you mean 'setNode'?,应该立即重视这些提示
最佳实践
- 参考已有代码: 在实现新功能时,优先参考项目中已有的类似实现
- 增量开发测试: 每完成一个 API 调用就应该测试,而不是等到全部完成
- 关注 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,导致:
- 元素没有
context属性 - 无法计算元素的矩形边界 (
getRectangle) - 选中状态没有高亮边框
问题 3: can not get rectangle of element
错误信息:
can not get rectangle of element: {id: 'tool_...', type: 'tool', ...}
根因: ToolComponent 没有实现 getRectangle 方法,Plait 无法获取元素的边界框。
修复方案
参考 FreehandComponent 的实现,为 ToolComponent 添加:
- 添加 activeGenerator: 使用
createActiveGenerator创建选中状态生成器 - 实现 getRectangle: 通过
RectangleClient.getRectangleByPoints(element.points)计算边界 - 更新 onContextChanged: 调用
activeGenerator.processDrawing更新选中状态 - 更新 destroy: 清理
activeGenerator资源
修改的代码 (tool.component.ts):
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<PlaitTool>;
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