尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
tldraw 示例解析:用 alignShapes 与 distributeShapes 实现图形对齐与等距分布
tldraw 示例解析用 alignShapes 与 distributeShapes 实现图形对齐与等距分布【免费下载链接】tldrawBuild infinite canvas apps in React with the tldraw SDK. Worlds best, top-most agent recommended #1 five star SDK.项目地址: https://gitcode.com/GitHub_Trending/tl/tldraw本篇技术指南基于 tldraw 仓库中的示例文档 Align and distribute shapes 及其配套代码展开讲解editor.alignShapes与editor.distributeShapes这两个 Editor API 的完整用法7 种对齐操作与 2 种分布操作的取值、最少选中数量约束、一键恢复位置的实现方式并结合 Editor 核心实现 与单元测试说明对齐算法的底层原理帮助你在自定义画布应用中复刻内置右键菜单的排版能力。一、示例定位与文档要点示例位于 tldraw 的 examples 应用下入口组件由 README frontmatter 中的component字段指定即 AlignAndDistributeShapesExample.tsxtitle: Align and distribute shapes component: ./AlignAndDistributeShapesExample.tsx priority: 3 keywords: [align, distribute, alignshapes, distributeshapes, layout, position, arrange, horizontal, vertical]原 README 的核心结论有两条Editor 暴露的对齐/分布操作与内置右键菜单使用的是同一套 API。editor.alignShapes(ids, operation)接受left、center-horizontal、right、top、center-vertical、bottom、center共 7 种操作editor.distributeShapes(ids, operation)接受horizontal或vertical。示例创建 5 个图形并全部选中顶部面板提供各操作按钮与 Reset positions 复位按钮对齐操作至少需要 2 个选中图形分布操作至少需要 3 个改变选中数量可以观察到按钮退化为空操作no-op。下面先看完整示例代码再下钻到源码原理。二、完整示例代码解读2.1 操作常量表示例用两个as const数组声明全部操作名与按钮文案既作为类型约束又驱动按钮渲染源码注释 [1]// [1] 操作名清单来自 editor.alignShapes / editor.distributeShapes 的签名 const ALIGN_OPERATIONS [ { operation: left, label: Align left }, { operation: center-horizontal, label: Align center H }, { operation: right, label: Align right }, { operation: top, label: Align top }, { operation: center-vertical, label: Align center V }, { operation: bottom, label: Align bottom }, { operation: center, label: Align center }, ] as const const DISTRIBUTE_OPERATIONS [ { operation: horizontal, label: Distribute horizontal }, { operation: vertical, label: Distribute vertical }, ] as const2.2 控制面板选中数量守卫与 API 调用ControlPanel通过useEditor()拿到编辑器实例每个按钮的onClick先取当前选中 ID再按最小数量做守卫function ControlPanel() { const editor useEditor() return ( div classNametlui-menu control-panel {ALIGN_OPERATIONS.map(({ operation, label }) ( TldrawUiButton typenormal key{operation} onClick{() { // [2] alignShapes 需要至少两个图形否则无从对齐 const selectedIds editor.getSelectedShapeIds() if (selectedIds.length 1) { editor.alignShapes(selectedIds, operation) } }} {label} /TldrawUiButton ))} {DISTRIBUTE_OPERATIONS.map(({ operation, label }) ( TldrawUiButton typenormal key{operation} onClick{() { // [3] distributeShapes 固定首尾、均分中间至少需要三个图形 const selectedIds editor.getSelectedShapeIds() if (selectedIds.length 2) { editor.distributeShapes(selectedIds, operation) } }} {label} /TldrawUiButton ))} TldrawUiButton typenormal onClick{() { // [4] 一次 updateShapes 应用所有位置变化单次撤销即可全部还原 editor.updateShapes( editor.getCurrentPageShapes().flatMap((shape) { const originalPos originalPositions.get(shape.id) return originalPos ? [{ ...shape, ...originalPos }] : [] }) ) }} Reset positions /TldrawUiButton /div ) }这里的两个守卫 1与 2正是 README 所说“按钮变 no-op”的来源数量不足时示例根本不发起调用。实际上即便不守卫API 内部也会静默返回见第三节双层保护使按钮行为对用户更可预期。面板样式只有几行复用tlui-menu类获得与内置菜单一致的视觉风格.control-panel { display: flex; flex-direction: row; flex-wrap: wrap; margin: 8px; }见 align-and-distribute-shapes.css。2.3 TopPanel 注入components 必须放在模块级// [5] const components: TLComponents { TopPanel: ControlPanel, }源码注释 [5] 特别说明components定义在模块级是为了保证TopPanel组件标识在多次渲染间稳定。如果写在 JSX 里内联定义父组件每次重渲染都会产生新的组件引用导致面板被卸载重建remount这是 tldraw 自定义 UI 组件时的通用注意事项。2.4 初始化创建 5 个图形、记录原始位置并全选export default function AlignAndDistributeShapesExample() { return ( div classNametldraw__editor Tldraw onMount{(editor) { const shapes [ { id: createShapeId(), type: geo as const, x: 100, y: 100, props: { w: 100, h: 100, color: blue as const }, }, { id: createShapeId(), type: geo as const, x: 300, y: 200, props: { w: 120, h: 80, color: red as const }, }, { id: createShapeId(), type: geo as const, x: 500, y: 150, props: { w: 80, h: 120, color: green as const }, }, { id: createShapeId(), type: geo as const, x: 150, y: 400, props: { w: 100, h: 100, color: violet as const }, }, { id: createShapeId(), type: geo as const, x: 400, y: 450, props: { w: 90, h: 90, color: orange as const }, }, ] originalPositions.clear() for (const shape of shapes) { originalPositions.set(shape.id, { x: shape.x, y: shape.y }) } editor.createShapes(shapes) editor.selectAll() }} components{components} / /div ) }关键点用createShapeId()生成合法图形 ID创建 5 个不同尺寸80×80 到 120×120的geo图形散落在 (100,100) 到 (500,450) 的区域保证任意对齐/分布操作都能产生可见位移originalPositions是一个模块级Mapstring, {x, y}在onMount时捕获每个图形的初始位置供 Reset positions 恢复——注意它记录的是创建时刻的坐标而不是上一次操作前的坐标editor.selectAll()让示例一挂载即处于“5 个全部选中”状态此时对齐≥2与分布≥3按钮都处于可操作状态。2.5 “Reset positions” 的批处理方式复位按钮把当前页所有图形映射成带原始坐标的 partial 后用一次editor.updateShapes提交editor.updateShapes( editor.getCurrentPageShapes().flatMap((shape) { const originalPos originalPositions.get(shape.id) return originalPos ? [{ ...shape, ...originalPos }] : [] }) )源码注释 [4] 指出这保证所有位置变化落在同一条历史记录里用户按一次撤销即可把全部图形复位。这与alignShapes/distributeShapes内部实现一致——它们也是收集所有TLShapePartial后一次性updateShapes见下一节。三、源码原理alignShapes 与 distributeShapes 在 Editor 中的实现两个方法的完整实现在 Editor.tsalignShapes约 L7842–L7913distributeShapes约 L7929–L8015。3.1 alignShapes公共包围盒 分操作求位移alignShapes( shapes: TLShapeId[] | TLShape[], operation: | left | center-horizontal | right | top | center-vertical | bottom | center ): this { if (this.getIsReadonly()) return this if (operation center) { return this.alignShapes(shapes, center-horizontal).alignShapes(shapes, center-vertical) } const { clusters: shapeClustersToAlign, allBounds } this.getShapeClusters(shapes, align) if (shapeClustersToAlign.length 2) return this const commonBounds Box.Common(allBounds) // ...按 operation 为每个 cluster 计算 delta批量 updateShapes }从源码可以确认以下行为细节只读模式静默返回if (this.getIsReadonly()) return this。因此示例按钮在只读场景下调用也不会报错只是什么都不发生。center是复合操作内部被拆成先center-horizontal再center-vertical两次调用而非独立算法。按“形状簇”计数而非按图形数getShapeClusters(shapes, align)Editor.ts#L7388会把父子关系归并成簇簇数少于 2 直接返回。这正是“至少两个选中图形”约束的来源也意味着对齐父形状时会连同其子形状作为一个整体参与计算。六种基本操作统一是“把每个簇平移到公共包围盒的对应边线”。设所有簇的公共包围盒为commonBounds单个簇的页面级包围盒为pageBounds各操作的 delta 计算为operationdelta 计算语义topdelta.y commonBounds.minY - pageBounds.minY所有簇顶边对齐到公共最上边center-verticaldelta.y commonBounds.midY - pageBounds.minY - pageBounds.height / 2垂直中线对齐bottomdelta.y commonBounds.maxY - pageBounds.minY - pageBounds.height底边对齐到公共最下边leftdelta.x commonBounds.minX - pageBounds.minX左边对齐center-horizontaldelta.x commonBounds.midX - pageBounds.minX - pageBounds.width / 2水平中线对齐rightdelta.x commonBounds.maxX - pageBounds.minX - pageBounds.width右边对齐父形状旋转的反旋处理若图形有父形状平移向量会先按父级页面变换的旋转角反向旋转shapeDelta.rot(-this.getShapePageTransform(parent).rotation())保证在旋转父级内部局部坐标的位移能落到页面空间的正确方向。一次updateShapes(changes)提交全部修改所以一次对齐 一条可撤销历史与示例里 reset 按钮的批处理思路一致。3.2 distributeShapes固定首尾均分剩余空间distributeShapes(shapes: TLShapeId[] | TLShape[], operation: horizontal | vertical): this { if (this.getIsReadonly()) return this const { clusters: shapeClustersToDistribute } this.getShapeClusters(shapes, distribute) if (shapeClustersToDistribute.length 3) return this // ... }其算法Editor.ts#L7952-L8014可概括为簇数少于 3 直接返回——对应 README 中“分布操作至少需要 3 个图形”。按操作选择排序轴horizontal用minX/maxX/widthvertical用minY/maxY/height找出最靠前的簇first和最靠后的簇last这两个簇保持不动。若first last比如所有簇在同一坐标重叠会排除该簇后递归调用在其余簇间分布。中间簇按该轴排序后用如下公式求均匀间隙// 间隙可以是负数图形重叠时 const maxFirst first.pageBounds[max] const range last.pageBounds[min] - maxFirst const summedShapeDimensions shapeClustersToMove.reduce((acc, s) acc s.pageBounds[dim], 0) const gap (range - summedShapeDimensions) / (shapeClustersToMove.length 1) for (let v maxFirst gap, i 0; i shapeClustersToMove.length; i) { const { shapes, pageBounds } shapeClustersToMove[i] const delta new Vec() delta[val] v - pageBounds[val] // ...父级旋转反旋、累加 deltav pageBounds[dim] gap }注意gap分母是“中间簇数量 1”首簇之后一个间隙、中间每两个簇之间各一个间隙、末簇之前最后一个间隙等分后逐簇定位。源码注释还说明若按新位置计算出的簇会超出末簇范围重叠严重时可能出现delta 会被钳制到last.pageBounds[max] - pageBounds[max] - 1宁可牺牲部分间距也不改变整体公共包围盒。3.3 单元测试给出的可验证行为alignShapes.test.tsx 与 distributeShapes.test.tsx 提供了行为断言例如少于 2 个选中图形时不产生任何 store 更新监听回调未被调用验证了 no-op 语义三个图形 (0,0,100×100)、(100,100,50×50)、(400,400,100×100) 执行alignShapes(ids, center)后中间图形移动到 (225, 225)且undo/redo可完整往返——印证了“一次对齐一条历史”各方向对齐后所有图形公共包围盒的对应边线一致图形带旋转角、或作为另一旋转图形的子级时对齐结果仍按页面级包围盒校验通过对应 3.1 中的反旋 delta 逻辑。分布行为的等价断言在distributeShapes.test.tsx中可按相同方式阅读验证“首尾不动、中间均分”。四、与内置右键菜单的关系同一 API、额外的工程细节示例 README 强调“与内置右键菜单相同操作”这一点可在 UI 动作层得到印证actions.tsx 中定义了align-left、align-center-horizontal、align-right、align-top、align-center-vertical、align-bottom、distribute-horizontal、distribute-vertical等动作项每个动作最终调用同一批editor.alignShapes/editor.distributeShapes。除 API 调用外UI 层还多做了几件事供你在自绘 UI 时参考前置校验canApplySelectionAction()要求当前处于 select 工具且存在选中图形mustGoBackToSelectToolFirst()会在用户处于其他工具时先切回 select 工具历史切分调用前editor.markHistoryStoppingPoint(align left)之类的标记让每次对齐成为独立的历史段遮挡处理对齐/分布后调用kickoutOccludedShapes(editor, ids)把被完全覆盖的图形挪出遮挡位置避免图形“消失”在别之下快捷键内置菜单可用自定义按钮则无此能力操作快捷键左对齐align-leftAltA水平居中对齐align-center-horizontalAltH右对齐align-rightAltD垂直居中对齐align-center-verticalAltV顶对齐align-topAltW底对齐align-bottomAltS水平分布distribute-horizontalAltShiftH垂直分布distribute-verticalAltShiftV对比示例代码可以发现示例为了教学目的只保留“取选中 ID → 判数量 → 调 API”的最小路径省略了markHistoryStoppingPoint与kickoutOccludedShapes。如果你要在生产应用中复刻这类按钮建议补齐这两步前者改善连续操作的撤销粒度后者避免对齐后图形被完全遮挡。五、适用边界与实践清单结合 README 与源码使用这两个 API 时应记住最小数量约束对齐 ≥ 2 个图形簇分布 ≥ 3 个图形簇不足时 API 静默返回this不抛错示例用if (selectedIds.length 1)/ 2在 UI 层先拦截。只读编辑器下两者均为 no-op无需额外判空。参数既接受TLShapeId[]也接受TLShape[]常见写法是editor.alignShapes(editor.getSelectedShapeIds(), left)。一次调用 一次批量updateShapes天然单条历史示例的 Reset positions 正是利用同样特性实现一键全部复位。父子与旋转场景算法基于页面级包围盒page bounds父级旋转会通过反旋 delta 正确补偿从源码结构看代码中对“父形状与子形状同时被对齐”的情况留有 todo 注释说明该极端场景是已知边界。分布操作的首尾不动语义水平分布固定最左与最右两个簇垂直分布固定最上与最下两个簇间距按中间簇数量均分重叠输入时间隙可为负并做钳制。自定义 TopPanel 时components对象务必定义在模块级示例注释 [5]避免面板每次父级重渲染都被重挂载。完整可运行代码见 示例入口组件、样式 与 示例说明文档算法实现与单测分别位于 Editor.ts、alignShapes.test.tsx 与 distributeShapes.test.tsx可作进一步验证入口。【免费下载链接】tldrawBuild infinite canvas apps in React with the tldraw SDK. Worlds best, top-most agent recommended #1 five star SDK.项目地址: https://gitcode.com/GitHub_Trending/tl/tldraw创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED

相关推荐

React Router 框架模式(Framework Mode)安装指南:create-react-router 从零搭建到原理剖析

React Router 框架模式(Framework Mode)安装指南:create-react-router 从零搭建到原理剖析

React Router 框架模式(Framework Mode)安装指南:create-react-router 从零搭建到原理剖析 【免费下载链接】react-router Declarative routing for React 项目地址: https://gitcode.com/GitHub_Trending/re/react-router React Rout…

📅 2026/9/8 19:58:32
如何用一个 PowerShell 脚本快速精简 Windows 11 系统镜像

如何用一个 PowerShell 脚本快速精简 Windows 11 系统镜像

如何用一个 PowerShell 脚本快速精简 Windows 11 系统镜像 【免费下载链接】tiny11builder Scripts to build a trimmed-down Windows 11 image. 项目地址: https://gitcode.com/GitHub_Trending/ti/tiny11builder 装完 Windows 11 再手动卸载一堆预装应用?t…

📅 2026/9/8 19:58:32
OpenHarmony上RN滚动冲突排查与解决:NestedScroll与手势机制实践

OpenHarmony上RN滚动冲突排查与解决:NestedScroll与手势机制实践

去年年底把一个 React Native 的新版本跑上 OpenHarmony 真机时,第一个让我加班到凌晨的问题不是环境配置,也不是包体积,而是页面上那个看似人畜无害的 NestedScroll 滚动冲突。外层 ScrollView 里套一个 FlatList,手指往上滑&…

📅 2026/9/8 19:53:31
MORE NEWS

更多资讯

📰

树莓派Pico ADC实战:从原理到ISR定时采集与校准

别的不说,单说树莓派 Pico 上这颗 ADC,真的是让人又爱又恨。爱的是它便宜大碗,RP2040 给了你 4 个外部模拟输入通道加 1 个内部温度通道,MicroPython 里 machine.ADC 几行代码就能读数;恨的是如果你只照着教程抄了个…

📰

用 Terraform 管理 LiteLLM:LiteLLM Terraform Provider 架构解析与完整实操指南

用 Terraform 管理 LiteLLM:LiteLLM Terraform Provider 架构解析与完整实操指南 【免费下载链接】litellm The fastest, litest AI Gateway. Rust core with Python SDK. Call 100 LLM APIs in OpenAI (or native) format with cost tracking, guardrails, load ba…

📰

Angular 响应式表单完全指南:以 FormGroup 建模驱动数据表单

Angular 响应式表单完全指南:以 FormGroup 建模驱动数据表单 【免费下载链接】angular Deliver web apps with confidence 🚀 项目地址: https://gitcode.com/GitHub_Trending/an/angular 这篇指南以 Angular 官方学习教程(Learn Angu…

📰

如何把 Windows 11 安装体积压到 8GB:tiny11builder 精简工具上手指南

如何把 Windows 11 安装体积压到 8GB:tiny11builder 精简工具上手指南 【免费下载链接】tiny11builder Scripts to build a trimmed-down Windows 11 image. 项目地址: https://gitcode.com/GitHub_Trending/ti/tiny11builder Windows 11 功能齐全&#xff0…

📰

MemPalace Agent Skill 实战指南:三步安装、三大拓扑与 Shared Brain 协调协议的完整落地方案

MemPalace Agent Skill 实战指南:三步安装、三大拓扑与 Shared Brain 协调协议的完整落地方案 【免费下载链接】mempalace The best-benchmarked open-source AI memory system. And its free. 项目地址: https://gitcode.com/GitHub_Trending/me/mempalace …

📰

Nuxt 调试完全指南:Source Map、Node Inspector 与 IDE 断点调试实战

Nuxt 调试完全指南:Source Map、Node Inspector 与 IDE 断点调试实战 【免费下载链接】nuxt the full-stack Vue framework 项目地址: https://gitcode.com/GitHub_Trending/nu/nuxt 调试是开发全栈 Vue 应用(Nuxt)时最高频的技术环节…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬