Files
TrueGrowth/specs/008-multifunctional-toolbox/BUGFIX_TRANSFORMS_API.md

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])

关键区别:

  1. 单数 vs 复数: Plait 使用 insertNode 而不是 insertNodes
  2. 参数格式: 直接传递元素和路径数组,而不是使用 { 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

📝 经验总结

教训

  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):

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