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/editor的Editor实例包装成一个命令式(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的公开 API:Driver不触碰内部状态,而是通过editor.dispatch(...)、editor.setCamera(...)、editor.sideEffects等公开接口工作,因此它不会绕过编辑器自身的工具状态机与撤销/重做体系; - 所有方法返回
this:天然支持链式调用(fluent chaining),一条语句即可表达一次连续交互。
导出口也很精简,见 src/index.ts:运行时只导出Driver类,类型上导出PointerEventInit与EventModifiers两个辅助类型。
二、安装
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 展示了这类构造方式——先用createTLStore、createTLSchema与defaultShapeUtils等装配一个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: 1、button: 0、isPen: false、point: { 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: true;keyUp则按释放语义做特殊处理,保证释放键对应的标志位被清除。
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通过getShapePageTransform与getShapeGeometry把图形几何中心的局部坐标变换到页面坐标(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),分别用createShapeId与PageRecordType.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"的落地证据:
测试编辑器的统一封装。在 TestEditor.ts 中,编辑器构造完成后即执行
this.controller = new Driver(this)(TestEditor.ts),随后整个TestEditor类用Parameters<Driver[...]>的方式把click、pointerDown、keyPress、wheel、pan、pinchStart、rotateSelection、translateSelection、resizeSelection、copy/cut/paste、getPageCenter、getLastCreatedShapes等一整套方法全部委托给 Driver(TestEditor.ts)。也就是说 tldraw 自己的编辑器测试实际上就是通过Driver来"手把手"操作编辑器的。真实指针驱动的工具测试。在 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);rightClick以button: 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),仅供参考