tldraw 箭头文字标签完全指南用 richText、labelPosition、labelColor 打造可控标注【免费下载链接】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 官方示例 apps/examples/src/examples/editor-api/arrow-labels 展开讲解如何在箭头arrow形状上创建文字标签并通过labelPosition标签沿箭杆的位置、labelColor独立的标签文字颜色、font字体以及bend弯曲箭头等属性精确控制标签的外观与位置。读完本指南你将掌握基于 tldraw SDK 的toRichText富文本机制以及用editor.createShapes编程式构建标注箭头、驱动交互编辑的完整思路。一句话理解本示例在 tldraw 中箭头标签并不是独立形状而是箭头形状自己的richText属性——创建箭头时通过toRichText(...)传入文本即可。示例代码在画布上排布出一组箭头矩阵逐项演示了四件事labelPosition用 01 之间的小数表示标签在箭杆上的位置0 靠起点、1 靠终点labelColor标签文字颜色独立于箭头本体的colorfont四种字体draw/sans/serif/monobend将箭头弯曲后标签仍会沿曲线而非弦居中摆放。同时示例还展示了两个开箱即用的交互拖动标签沿箭头滑动、双击标签编辑文字。示例文件与运行方式示例代码与清单位于组件实现apps/examples/src/examples/editor-api/arrow-labels/ArrowLabelsExample.tsx示例元信息标题、关键词、优先级apps/examples/src/examples/editor-api/arrow-labels/README.md源码顶部只依赖两个入口import { createShapeId, Tldraw, toRichText } from tldraw import tldraw/tldraw.css也就是说你不需要引入任何额外插件或编辑器内部模块所有能力都通过tldraw包公开的 API 暴露。完整组件用法是export default function ArrowLabelsExample() { return ( div classNametldraw__editor Tldraw onMount{(editor) { if (editor.getCurrentPageShapeIds().size 0) return editor.createShapes([/* ...箭头形状列表... */]) editor.zoomToFit({ animation: { duration: 0 } }) }} / /div ) }onMount中先通过getCurrentPageShapeIds().size 0做幂等保护避免重复挂载时重复创建图形然后一次性批量创建所有箭头最后zoomToFit把画布视野对齐到全部内容方便一眼看到网格布局。标签数据模型richText与toRichText箭头标签并非string而是富文本结构。在 tldraw 的 schema 中TLArrowShapeProps有一个richText: TLRichText字段对应定义见 packages/tlschema/src/shapes/TLArrowShape.ts其验证器如下export const arrowShapeProps: RecordPropsTLArrowShape { // ... bend: T.number, richText: richTextValidator, labelPosition: T.number, // ... }TLRichText采用「文档-块」结构doc → 段落 paragraph → 文本 text验证器定义在 packages/tlschema/src/misc/TLRichText.tsexport const richTextValidator T.object({ type: T.string, content: T.arrayOf(T.unknown), attrs: T.any.optional(), })直接手写这种结构很繁琐所以官方提供了toRichText(text)帮助函数同一文件内实现。它的规则是把输入字符串按\n拆行每一行生成一个 paragraph空行保留为空段落export function toRichText(text: string): TLRichText { const lines text.split(\n) const content lines.map((text) { if (!text) { return { type: paragraph } } return { type: paragraph, content: [{ type: text, text }], } }) return { type: doc, content } }因此多行标签天然得到支持toRichText(第一行\n第二行)会产生两个段落这与箭头标签支持多行排版和回车换行的交互编辑行为是对应的。也正因标签走的是富文本管线它的字号、字体族渲染复用 tldraw 全局的字体测量系统详见下文「字体」小节对editor.textMeasure的引用。最基础的带标签箭头示例中的第一组箭头序号 [1]是最简形态{ id: createShapeId(), type: arrow, x: 100, y: 100, props: { start: { x: 0, y: 0 }, end: { x: 300, y: 0 }, richText: toRichText(Default label), labelPosition: 0.5, }, }要点说明start/end是箭头两端在形状局部坐标系中的点这里从(0,0)画到(300,0)即一条水平箭头注意此时端点类型是自由点坐标箭头不绑定任何其他形状richText: toRichText(Default label)设置标签文本labelPosition: 0.5是默认值把标签放在箭杆正中央。labelPosition的含义是「标签中心在箭杆上的相对位置」0 表示起点端1 表示终点端。schema 中对其默认值的填充也能佐证「居中即默认」在 packages/tlschema/src/shapes/TLArrowShape.ts 的迁移代码里新老数据都统一写入props.labelPosition 0.5。拖动标签labelPosition的交互版用法labelPosition不只是创建参数你可以在运行期把标签当做一个可拖动的「手柄」。示例导读明确建议选中箭头后拖动标签沿箭杆滑动或双击标签直接编辑文字。这两个交互背后由 packages/tldraw/src/lib/shapes/arrow/ArrowShapeUtil.tsx 的 arrow shape util 驱动工具栏中的双击编辑则由 packages/tldraw/src/lib/shapes/arrow/toolStates/Idle.tsx 中startEditingShape(...)触发。从源码结构看packages/tldraw/src/lib/shapes/arrow/arrowLabel.ts渲染系统在把labelPosition映射成屏幕坐标时并非机械地取「整条线段的分数点」而是先用「标签自带尺寸」反推一个可用的位置区间getArrowLabelRange——因为标签占据空间当它靠近箭头起点/终点尤其是带有箭头头arrowhead或绑定了形状时不能被画出箭头端点再用clamp把labelPosition限制在该区间内getClampedPosition例如start/end端有箭头头或绑定时区间为[range.start, range.end]否则为[0, 1]最终通过几何体上的bodyGeom.interpolateAlongEdge(clampedPosition)计算标签中心点。也就是说你写的labelPosition是「期望位置」引擎会保证标签始终留在箭杆有效区段内、不越出端点。对直线与圆弧箭头几何体分别是Edge2d与Arc2d见getArrowBodyGeometry所以圆弧上的标签沿弧长插值拖动时也贴合曲线移动。值得补充的性能细节arrowLabel.ts用createComputedCache缓存了标签尺寸测量结果并且其areRecordsEqual做了特殊优化——如果两个版本之间只有labelPosition变化则跳过尺寸重算见 arrowLabel.ts。这保证了「拖动标签」这个高频操作不会反复触发昂贵的文本测量。独立文字颜色labelColor与color示例第二组序号 [2]展示了labelColor的核心价值——文字颜色与箭杆颜色解耦{ id: createShapeId(), type: arrow, x: 100, y: 200, props: { start: { x: 0, y: 0 }, end: { x: 300, y: 0 }, richText: toRichText(Start), labelPosition: 0.2, color: blue, labelColor: red, }, }这里箭杆设为blue标签文字却是red。示例矩阵里还依次展示了labelColor: red起点附近、labelColor: violet中点、labelColor: green终点附近三种位置组合。官方注释给出的使用动机非常实用当箭杆本身是浅色时独立设置深色标签文字可以保证对比度与可读性。在 schema 层面color与labelColor是两套独立的 StylePropDefaultColorStyle与DefaultLabelColorStyle见 TLArrowShape.ts二者共用 tldraw 的颜色取值集合。这也说明标签颜色只影响文字本身不会改变箭头线段与箭头头的颜色。字体font的四种取值与字号来源示例第三组序号 [3]依次创建了四支横向箭头分别使用font: draw | sans | serif | mono{ id: createShapeId(), type: arrow, x: 550, y: 100, props: { /*...*/ richText: toRichText(Draw font), font: draw } }, { id: createShapeId(), type: arrow, x: 550, y: 200, props: { /*...*/ richText: toRichText(Sans font), font: sans } }, { id: createShapeId(), type: arrow, x: 550, y: 300, props: { /*...*/ richText: toRichText(Serif font), font: serif } }, { id: createShapeId(), type: arrow, x: 550, y: 400, props: { /*...*/ richText: toRichText(Mono font), font: mono } },draw手写风格字体tldraw 默认风格的标志性字体sans/serif常规无衬线与衬线字体mono等宽字体适合代码片段类标签。示例代码末尾的注释还澄清了一个容易混淆的点箭头没有独立的「标签字号」属性标签的字号由size属性决定——而这个size属性同时也是箭杆线宽所共享的STROKE_SIZES根据size映射线宽见getLabelToArrowPadding对STROKE_SIZES[shape.props.size]的引用arrowLabel.ts。渲染标签时实际使用的是由size、scale与主题推导出的labelFontSize、labelLineHeight、labelFontFamily等显示值并通过editor.textMeasure.measureHtml测量文本占据的宽高。弯曲箭头上的标签bendlabelPosition示例最后一支箭头序号 [4]把前面所有能力组合到了一起{ id: createShapeId(), type: arrow, x: 300, y: 475, props: { start: { x: 0, y: 0 }, end: { x: 400, y: 150 }, bend: 50, richText: toRichText(Curved arrow), labelPosition: 0.5, font: sans, color: violet, size: m, }, }这里end不再是水平方向而是斜向下(400,150)bend: 50给箭头施加 50 的弯曲量使它变成一段弧线。重点注释是bendcurves the arrow, and the label is positioned along the curve rather than the chord.即标签的labelPosition: 0.5落在弧线的中点而非连接两端点的直弦中点。这正对应上文提到的实现圆弧箭头的 body geometry 是Arc2dinterpolateAlongEdge(0.5)沿弧长插值从而让标签「贴」在弯曲路径的视觉正中。完整可运行代码以下是整合后的完整示例保留官方代码注释结构可直接作为独立组件的骨架import { createShapeId, Tldraw, toRichText } from tldraw import tldraw/tldraw.css export default function ArrowLabelsExample() { return ( div classNametldraw__editor Tldraw onMount{(editor) { if (editor.getCurrentPageShapeIds().size 0) return editor.createShapes([ // [1] 最简标签居中labelPosition 默认 0.5 { id: createShapeId(), type: arrow, x: 100, y: 100, props: { start: { x: 0, y: 0 }, end: { x: 300, y: 0 }, richText: toRichText(Default label), labelPosition: 0.5, }, }, // [2] 独立的标签颜色 不同 labelPosition { id: createShapeId(), type: arrow, x: 100, y: 200, props: { start: { x: 0, y: 0 }, end: { x: 300, y: 0 }, richText: toRichText(Start), labelPosition: 0.2, color: blue, labelColor: red }, }, { id: createShapeId(), type: arrow, x: 100, y: 300, props: { start: { x: 0, y: 0 }, end: { x: 300, y: 0 }, richText: toRichText(Middle), labelPosition: 0.5, color: blue, labelColor: violet }, }, { id: createShapeId(), type: arrow, x: 100, y: 400, props: { start: { x: 0, y: 0 }, end: { x: 300, y: 0 }, richText: toRichText(End), labelPosition: 0.8, color: blue, labelColor: green }, }, // [3] 四种字体 { id: createShapeId(), type: arrow, x: 550, y: 100, props: { start: { x: 0, y: 0 }, end: { x: 300, y: 0 }, richText: toRichText(Draw font), font: draw }, }, { id: createShapeId(), type: arrow, x: 550, y: 200, props: { start: { x: 0, y: 0 }, end: { x: 300, y: 0 }, richText: toRichText(Sans font), font: sans }, }, { id: createShapeId(), type: arrow, x: 550, y: 300, props: { start: { x: 0, y: 0 }, end: { x: 300, y: 0 }, richText: toRichText(Serif font), font: serif }, }, { id: createShapeId(), type: arrow, x: 550, y: 400, props: { start: { x: 0, y: 0 }, end: { x: 300, y: 0 }, richText: toRichText(Mono font), font: mono }, }, // [4] 弯曲箭头上的标签沿弧线而非弦 { id: createShapeId(), type: arrow, x: 300, y: 475, props: { start: { x: 0, y: 0 }, end: { x: 400, y: 150 }, bend: 50, richText: toRichText(Curved arrow), labelPosition: 0.5, font: sans, color: violet, size: m, }, }, ]) editor.zoomToFit({ animation: { duration: 0 } }) }} / /div ) }空标签与编辑态的实现细节有两点底层行为值得留意均在 arrowLabel.ts 中体现空标签不渲染尺寸当标签文本为空且不在编辑态时getArrowLabelPosition会走短路逻辑把标签盒宽高视为 0仅把中心点放在箭杆中点——视觉上就是「没有标签盒子」编辑态下标签可正常生长一旦进入双击编辑isEditing为 true便走完整测量与换行逻辑文本随输入动态重排。此外测量时会用isEmptyRichText判断空内容并把最小宽度按一个字符i测量见 arrowLabel.ts确保点击空标签也能命中可编辑区域。标签的几何体在ArrowShapeUtil.tsx中通过getArrowLabelPosition计算并放入Group2d的children[1]children[0] 是箭杆路径isOverArrowLabel正是通过命中检测这个子几何体来判断「鼠标是否悬浮在标签上」见 arrowLabel.ts从而把悬停、拖动、双击编辑与箭杆本身的框选/拖拽逻辑区分开。数据演进为什么箭头标签经历过迁移如果浏览 packages/tlschema/src/shapes/TLArrowShape.ts 中的arrowShapeVersions可以看到箭头形状的 schema 演进史export const arrowShapeVersions createShapePropsMigrationIds(arrow, { AddLabelColor: 1, AddIsPrecise: 2, AddLabelPosition: 3, ExtractBindings: 4, AddScale: 5, AddElbow: 6, AddRichText: 7, AddRichTextAttrs: 8, })与标签直接相关的关键版本是AddLabelColor补齐 labelColor默认black、AddLabelPosition新老数据统一写入 0.5以及AddRichText——它把旧的纯文本text字段迁移成富文本richTextprops.richText toRichText(props.text)后删除text见迁移序列 TLArrowShape.ts。这说明本文介绍的richText模型是较新的、面向文档化富文本的存储形态如果你在处理历史存量文档迁移系统会自动完成这类转换。测试佐证仓库中 packages/tldraw/src/lib/shapes/arrow/ArrowShapeUtil.test.ts 覆盖了箭头形状的关键行为例如it(should create an arrow with a label, ...)验证带标签箭头的创建测试夹具中显式构造labelPosition: 0.5等 props见该文件内标签相关用例印证了标签属性在运行期可被正常读写。如果你要为本指南的用法补测试或验证 schema 行为可以从 packages/tlschema/src/migrations.test.ts 与上述 ArrowShapeUtil 测试入手。小结把本示例拆解到底核心可迁移知识只有四条标签数据 箭头形状的richTextprop用toRichText(plainText)构造天然支持多行段落labelPosition是 01 的「沿箭杆相对位置」拖动标签会写回该值渲染引擎负责在端点/箭头头处自动夹紧labelColor与color分离浅色箭杆配深色标签可获得更好的对比度字体由font选择draw/sans/serif/mono字号由size决定与线宽同源bend弯曲后标签沿弧线摆放。这套模式可以直接复用到你自己的标注型应用中——无论是流程图、架构图还是注释工具只需在onMount或运行期用editor.createShapes传入上述 props就能得到与编辑器原生交互完全一致的箭头标签体验。延伸阅读箭头标签定位与测量的运行时实现packages/tldraw/src/lib/shapes/arrow/arrowLabel.ts箭头形状 UI / 渲染 / 手柄逻辑packages/tldraw/src/lib/shapes/arrow/ArrowShapeUtil.tsx箭头形状 schema、验证器与迁移packages/tlschema/src/shapes/TLArrowShape.ts富文本结构定义与toRichText实现packages/tlschema/src/misc/TLRichText.ts示例测试packages/tldraw/src/lib/shapes/arrow/ArrowShapeUtil.test.ts【免费下载链接】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),仅供参考