tldraw SDK 编程驱动指南:用 @tldraw/driver 命令式地驱动 Editor 完成脚本化与自动化测试
2026/9/10 1:30:24 网站建设 项目流程

tldraw SDK 编程驱动指南:用 @tldraw/driver 命令式地驱动 Editor 完成脚本化与自动化测试

【免费下载链接】tldrawBuild infinite canvas apps in React with the tldraw SDK. World's best, top-most agent recommended #1 five star SDK.项目地址: https://gitcode.com/GitHub_Trending/tl/tldraw

@tldraw/driver是 tldraw 官方仓库中的一个轻量级工具包,它把@tldraw/editorEditor实例包装成一个命令式(imperative)API,让你可以用代码"扮演"用户——模拟点击、键盘、捏合、剪贴板与选择区变换等操作。它适合脚本化批处理、编辑器自动化、REPL 交互调试以及自动化测试等场景。读完本文,你将掌握Driver的完整 API 形态、坐标体系与事件语义,并能基于仓库源码理解每次调用底层到底发生了什么。

一、什么是 @tldraw/driver:定位与设计原则

按照 packages/driver/README.md 中的定义,@tldraw/driver是"用于以编程方式驱动 tldraw 编辑器的命令式 API",定位是脚本化(scripting)、自动化(automation)、REPL 使用和测试(testing)

在 packages/driver/package.json 中可以看到它的元数据:产品名Driver,父级为tldraw:sdk-core,类型为feature(非 premium 功能),运行时只依赖@tldraw/editor@tldraw/utils,因此它是一个非常"薄"的封装层。

其源码注释揭示了两个核心设计约束,见 Driver.ts:

  • 只使用Editor的公开 APIDriver不触碰内部状态,而是通过editor.dispatch(...)editor.setCamera(...)editor.sideEffects等公开接口工作,因此它不会绕过编辑器自身的工具状态机与撤销/重做体系;
  • 所有方法返回this:天然支持链式调用(fluent chaining),一条语句即可表达一次连续交互。

导出口也很精简,见 src/index.ts:运行时只导出Driver类,类型上导出PointerEventInitEventModifiers两个辅助类型。

二、安装

npm install @tldraw/driver

该包是标准的 ESM 包("type": "module")。由于它依赖同仓库的@tldraw/editor@tldraw/utils,在 monorepo 内它们是workspace:*关系(见 package.json),发布时会被构建脚本重写为真实版本号。仓库要求 Node >= 22.12.0,如果你的环境较旧,请先升级 Node。

三、快速上手

Driver的构造函数接收一个已存在的Editor实例。官方 README 给出的最小示例是:

import { Driver } from '@tldraw/driver' const driver = new Driver(editor) // 模拟用户交互 driver.click(100, 200).pointerDown(300, 400).pointerMove(500, 600).pointerUp() // 键盘输入 driver.keyPress('a') driver.keyDown('Shift') driver.keyUp('Shift') // 剪贴板 driver.copy() driver.paste({ x: 100, y: 100 }) // 选择区操作 driver.translateSelection(50, 0) driver.rotateSelection(Math.PI / 4) driver.resizeSelection({ scaleX: 2 }, 'bottom_right') // 相机 driver.pan({ x: 100, y: 0 }) driver.wheel(0, -100) // 清理 driver.dispose()

其中editor是一个真实运行的 tldraw 编辑器实例。在真实 React 应用中,你通常从正在运行的Tldraw组件上下文中取得 editor(例如通过useEditor()或组件的onMount回调);而在仓库内部的无头测试环境中,则是直接手工构造Editor。仓库中 comment-tool.test.ts 展示了这类构造方式——先用createTLStorecreateTLSchemadefaultShapeUtils等装配一个Editor,再把它交给Driver

const editor = new Editor({ store: createTLStore({ schema: createTLSchema({ records: commentSchemaRecords }) }), shapeUtils: defaultShapeUtils, bindingUtils: defaultBindingUtils, tools: [...defaultTools, tool], getContainer: () => document.body, }) const driver = new Driver(editor)

注意:driver.click(100, 200)等方法的坐标都是屏幕坐标(screen space),而选择区变换类方法接收的是页面坐标(page space)——Driver会在内部通过pageToScreen等完成换算。详见下文。

四、API 总览

4.1 构造函数

签名说明
new Driver(editor: Editor)包装一个已有的Editor实例。所有方法只使用 Editor 的公开 API

构造时Driver会通过editor.sideEffects.registerAfterCreateHandler('shape', ...)注册一个副作用处理器,持续记录最近创建的图形(源码见 Driver.ts)。当记录超过 1000 个时,会自动裁剪到最近 500 个,避免无限增长。

4.2 输入事件(Input events)

所有输入方法都返回this以便链式调用,坐标均为屏幕坐标。下表完整继承自官方 README:

方法说明
pointerDown(x?, y?, options?, modifiers?)派发 pointer down 事件
pointerMove(x?, y?, options?, modifiers?)派发 pointer move 事件
pointerUp(x?, y?, options?, modifiers?)派发 pointer up 事件
click(x?, y?, options?, modifiers?)pointer down + up
rightClick(x?, y?, options?, modifiers?)右键(button 2)down + up
doubleClick(x?, y?, options?, modifiers?)双击序列
keyDown(key, options?)派发 key down 事件
keyUp(key, options?)派发 key up 事件
keyPress(key, options?)key down + up
keyRepeat(key, options?)派发 key repeat 事件
wheel(dx, dy, options?)派发 wheel/scroll 事件
pinchStart(x?, y?, z, dx, dy, dz, options?)开始一次捏合手势
pinchTo(x?, y?, z, dx, dy, dz, options?)持续捏合手势
pinchEnd(x?, y?, z, dx, dy, dz, options?)结束捏合手势
forceTick(count?)发出 tick 事件以推进编辑器(默认 1 帧)

其中x/y可省略——省略时取当前指针位置editor.inputs.getCurrentScreenPoint()。两个可选的类型参数(导出类型见 api-report.api.md):

  • PointerEventInit = Partial<TLPointerEventInfo> | TLShapeId:传入一个shape ID 字符串时,等价于{ target: 'shape', shape: 对应图形 },把事件目标对准该图形;传入对象时则作为事件信息覆盖项(例如{ target: 'selection', handle: 'top_left_rotate' })。
  • EventModifiers = Partial<Pick<TLPointerEventInfo, 'shiftKey' | 'ctrlKey' | 'altKey'>>:显式覆盖修饰键状态。

这些事件的底层语义可以在 Driver.ts 的 Event building 小节中看到,值得关注的行为有:

  • 事件都会先并入编辑器当前的修饰键状态(shift/ctrl/alt/meta/accel),再叠加你传入的覆盖项;
  • 指针事件默认pointerId: 1button: 0isPen: falsepoint: { x, y, z: null };模拟输入默认按直显式触控笔处理(isPenDirect默认为isPen),测试可显式传isPenDirect: false关闭笔模式;
  • 在 macOS(tlenv.isDarwin)上,如果button === 0且按住ctrlKey而未按metaKey,会把左键自动改写成button: 2——这模拟了 macOS 上 "Ctrl+点击 = 右键" 的惯例;
  • 键盘事件会生成真实的code字段:内置映射KEY_CODES覆盖了Shift/Alt/Control/Meta/空格/Enter/四个方向键等(Driver.ts),其它按键则按'Key' + 首字母大写的规则推导,例如keyDown('a')会得到code: 'KeyA'
  • 修饰键的状态在键盘事件中被视为"全局持有态"而非"当前键":按下 Shift 时若 Control 仍按住,事件会同时携带shiftKey: true, ctrlKey: truekeyUp则按释放语义做特殊处理,保证释放键对应的标志位被清除。

4.3 剪贴板(Clipboard)

方法说明
copy(ids?)把图形复制到 Driver 自己的剪贴板(默认取当前选中)
cut(ids?)剪切(先复制再删除)
paste(point?)从 Driver 剪贴板粘贴
clipboard当前剪贴板内容(TLContent \| null

需要注意:这是Driver 内部维护的一块本地剪贴板TLContent | null,见 Driver.ts),与系统剪贴板无关。从源码看,三者的实现分别是:

  • copy调用editor.getContentFromCurrentPage(ids)序列化页面内容(Driver.ts);
  • cut先复制再editor.deleteShapes(ids)(Driver.ts);
  • paste会先editor.markHistoryStoppingPoint('pasting')记录撤销边界,再用editor.putContentOntoCurrentPage放置内容并自动选中;如果按住 Shift,会改用当前指针位置作为粘贴点,否则使用传入的point(页面坐标),见 Driver.ts。

4.4 选择区操作(Selection manipulation)

这一组方法工作在页面坐标下,内部自行完成屏幕坐标换算。官方 README 说明如表:

方法说明
translateSelection(dx, dy, options?)按页面坐标位移移动选区
rotateSelection(angle, options?)以弧度角旋转选区
resizeSelection(scale?, handle, options?)通过某个控制手柄缩放选区

源码细节(见 Interaction helpers 小节)让这几个"高阶操作"的仿真方式变得清晰:

  • rotateSelection(angleRadians, { handle?, shiftKey? }):先确保处于select工具并抛出No selection错误(无选区时)。它取选区旋转包围盒的旋转手柄点(默认top_left_rotate),以选区中心为圆心旋转到目标角度,再把两个点转成屏幕坐标,用"指针按下手柄 → 移动到目标 → 抬起"三步完成旋转;
  • translateSelection(dx, dy, options?):从选区中心按下,把移动轨迹插值成 10 步pointerMove,最后在终点抬起——分段移动是为了贴近真实拖拽过程,让吸附、布局约束等基于逐帧 pointer 行为的逻辑也能被触发;
  • resizeSelection({ scaleX, scaleY }, handle, options?):先算出手柄点与"缩放原点"(默认是对侧手柄;若传options.altKey则以包围盒中心为原点做中心缩放),把手柄点按比例拉伸后同样以"按下→移动→抬起"三拍完成。handle取值如'top''bottom_right'等(对应SelectionHandle)。

4.5 查询(Queries)

方法说明
getViewportPageCenter()视口中心(页面坐标)
getSelectionPageCenter()选区中心(页面坐标,无选区时为null
getPageCenter(shape)某个图形的中心(页面坐标)
getPageRotation(shape)图形在页面空间的旋转(弧度)
getPageRotationById(id)按 ID 查询图形的旋转(弧度)
getArrowsBoundTo(shapeId)绑定到某图形的所有箭头
getLastCreatedShape()最近创建的图形
getLastCreatedShapes(count?)最近 N 个创建的图形

这些方法基本是 Editor 查询 API 的薄封装。实现上值得注意的几点:

  • getPageCenter通过getShapePageTransformgetShapeGeometry把图形几何中心的局部坐标变换到页面坐标(Driver.ts);
  • getPageRotationById直接读取页面变换矩阵的旋转分量(pageTransform.rotation());
  • getSelectionPageCenter会把旋转后的选区中心用Vec.RotWith反算回页面坐标,因此对旋转过的选区依然准确;
  • getArrowsBoundTo依赖editor.getBindingsToShape(shapeId, 'arrow')拿到所有指向该图形的箭头绑定,再去重并还原出TLArrowShape(Driver.ts);
  • getLastCreatedShapes(count)默认取最近 1 个,getLastCreatedShape()返回最后创建的那个图形;由于数据来自构造时注册的 shape 创建副作用,即使图形随后被删除,查询结果仍能反映"创建"事件本身。

除 README 列表外,源码还提供了两个 ID 工具方法(见 IDs 小节):createShapeID(id)createPageID(id),分别用createShapeIdPageRecordType.createId生成合法的 shape/page ID,便于拼装测试数据。

4.6 相机(Camera)

方法说明
pan(offset)按页面坐标偏移平移相机

pan的实现体现了对相机约束的尊重(Driver.ts):若相机被锁定(cameraOptions.isLocked)则直接返回不做任何事;否则按panSpeed与当前缩放cz换算偏移量后,用editor.setCamera(..., { immediate: true })立即跳转相机。

4.7 生命周期(Lifecycle)

方法说明
dispose()移除副作用处理器,调用完应释放

dispose会注销构造函数里注册的 shape 副作用 handler(Driver.ts)。在测试的afterEach/teardown或脚本结束时记得调用,避免内存泄漏。

五、仓库中的实际用法:把 Driver 当作测试基础设施

把源码翻一遍就会发现,Driver在仓库内部被当作测试基础设施的核心在使用,这正是它设计目标里"testing"的落地证据:

  1. 测试编辑器的统一封装。在 TestEditor.ts 中,编辑器构造完成后即执行this.controller = new Driver(this)(TestEditor.ts),随后整个TestEditor类用Parameters<Driver[...]>的方式把clickpointerDownkeyPresswheelpanpinchStartrotateSelectiontranslateSelectionresizeSelectioncopy/cut/pastegetPageCentergetLastCreatedShapes等一整套方法全部委托给 Driver(TestEditor.ts)。也就是说 tldraw 自己的编辑器测试实际上就是通过Driver来"手把手"操作编辑器的。

  2. 真实指针驱动的工具测试。在 comment-tool.test.ts 中,测试这样驱动指针:driver.pointerMove(150, 125)悬停、driver.pointerDown(...)按下、driver.pointerMove(...)拖拽,用来验证 comment 工具的状态机(idle → pointing → dragging)如何维护提示状态。这类"指针驱动"测试之所以可信,正是因为Driver派发的是与真实事件同构的完整事件信息。

因此,如果你的目标是给基于 tldraw 的扩展(自定义工具、图形、绑定)编写自动化测试,Driver就是官方测试代码本身在使用的那套 API——用它写出的测试与 tldraw 内部测试的仿真方式完全一致。

六、从源码理解三个关键约定

6.1 每个事件之后都会 forceTick

阅读 Driver.ts 会发现:绝大多数派发方法在editor.dispatch(...)之后都会调用this.forceTick()wheel甚至会连续forceTick(2)forceTick(count)会以每帧 16ms 的间隔发出count'tick'事件。这是因为 tldraw 编辑器的许多工具逻辑(长按判定、动画、惯性等)依赖帧循环推进,手动补 tick 才能让一次交互"走完"。这个细节也提醒使用者:如果你想模拟"等待几帧后发生的变化",可以显式调用driver.forceTick(n)

6.2 复合手势 = 多个原子事件的正确拼接

click=pointerDown+pointerUp(Driver.ts);rightClickbutton: 2派发 down/up;doubleClick则先click一次,再派发type: 'click'name: 'double_click'phase: 'down'/'up'两个事件(Driver.ts)。所以如果需要模拟更复杂的序列,完全可以用这些原子事件自由组合,而不必局限于预设的高阶方法。

6.3 键名与修饰键的语义

键盘方法接收的是"按键名"而非 code,例如'a''Enter''Shift''ArrowRight'。修饰键状态按"全局持有态"合成(见 4.2 小节),这意味着你可以在keyDown('Control')后继续执行其它键操作,编辑器会一直认为 Control 被按住,直到keyUp('Control')。这让诸如Control + A(全选)、Control + C/V(复制/粘贴)之类的组合操作很容易编排:

driver.keyDown('Control').keyPress('a').keyUp('Control') driver.keyDown('Control').keyPress('c').keyUp('Control') driver.keyDown('Control').keyPress('v').keyUp('Control')

七、组合示例:一套完整的"创建—移动—导出"—脚本

把以上 API 串起来,一个典型的自动化流程可以是:选中工具 → 在屏幕坐标画一个框 → 用页面坐标位移与旋转调整它 → 读取它的中心与旋转做断言 → 最后 dispose:

import { Driver } from '@tldraw/driver' import { Editor } from '@tldraw/editor' const driver = new Driver(editor) // 1. 用指针在画布上"画出"一个图形 driver.click(100, 200).pointerDown(300, 400).pointerMove(500, 600).pointerUp() // 2. 选中刚创建的图形并做选区变换(页面坐标) const box = driver.getLastCreatedShape() const center = driver.getPageCenter(box) const rotation = driver.getPageRotation(box) driver.translateSelection(50, 0) driver.rotateSelection(Math.PI / 4) driver.resizeSelection({ scaleX: 2 }, 'bottom_right') // 3. 查询并断言变换结果 const movedCenter = driver.getSelectionPageCenter() const newRotation = driver.getPageRotationById(box.id) // 4. 清理副作用 driver.dispose()

配合 4.2 节的坐标约定,这套写法几乎可以直接平移进 vitest / Playwright 等测试框架中复用。

八、参考与延伸阅读

  • 官方包 README:packages/driver/README.md
  • 核心实现:packages/driver/src/lib/Driver.ts
  • 包入口与导出:packages/driver/src/index.ts
  • 公开 API 报告(类型签名权威来源):packages/driver/api-report.api.md
  • 包元数据与脚本:packages/driver/package.json
  • 仓库内将 Driver 集成进测试编辑器的范例:packages/tldraw/src/test/TestEditor.ts
  • 使用 Driver 做真实指针驱动测试的范例:packages/commenting/src/canvas/comment-tool.test.ts
  • 许可证:LICENSE.md

若想更深入了解Editor本体的查询、事件与变换 API,可继续查阅本仓库 packages/editor 目录下的源码;@tldraw/editor的公开类型定义也会在构建后随包发布。

【免费下载链接】tldrawBuild infinite canvas apps in React with the tldraw SDK. World's best, top-most agent recommended #1 five star SDK.项目地址: https://gitcode.com/GitHub_Trending/tl/tldraw

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询