tldraw 评论聚簇详解:CanvasComments 如何将邻近评论 Pin 折叠为计数徽章

发布时间:2026/9/7 4:03:23
tldraw 评论聚簇详解:CanvasComments 如何将邻近评论 Pin 折叠为计数徽章 tldraw 评论聚簇详解CanvasComments 如何将邻近评论 Pin 折叠为计数徽章【免费下载链接】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 官方示例 Comment clustering示例说明展开当画布上评论 Pin 密集时CanvasComments会在缩小画布时把邻近的锚点合并为一枚带计数的徽章放大时再拆回独立 Pin且拆分阈值比合并阈值更宽避免 Pin 在阈值附近闪烁点击徽章则自动放大到“刚好越过拆分点”的位置。读完本文你将掌握如何复现该示例、如何用CommentTool.configure({ enableClustering: false })关闭聚簇并理解tldraw/commenting内部从最小生成树、有上限回放capped replay、迟滞调度hysteresis到运行时游标runtime cursor的完整算法管线。效果与行为约定当一块画板被评论填满时距离很近的 Pin 在低缩放级别下会互相重叠而不可读。聚簇clustering解决的就是这个问题其对外可观察的行为约定为缩小zoom out时合并邻近的评论线程锚点折叠进同一枚计数徽章放大zoom in时拆分徽章重新裂解为各自独立的 Pin迟滞hysteresis拆分发生在比合并更宽的间距上源码中Tu Tc是硬性校验因此 Pin 不会在阈值处反复闪烁点击徽章展开点击计数徽章会把相机动画放大到“刚好越过该簇首次拆分点”的缩放级别源码中定义为拆分点再乘 1.05 的 5% 过冲动画时长 450mscluster-model.ts 中的CLUSTER_EXPAND_ZOOM_MS与CLUSTER_SPLIT_ZOOM_FACTOR。聚簇由CanvasComments默认开启不需要任何配置关闭方式为CommentTool.configure({ enableClustering: false })此时所有 Pin 在每个缩放级别都单独渲染默认值可在 options.ts 的defaultCommentingOptions中确认enableClustering: true。复现示例种子两组评论线程官方示例 CommentClusteringExample.tsx 的核心是“播种两簇松散的线程组”然后缩小画布观察每组各自折叠成一枚徽章、放大再裂开。示例代码做了三件事创建带评论 schema 的 storecreateTLStore({ schema: createTLSchema({ records: commentSchemaRecords }) })其中commentSchemaRecords来自tldraw在onMount中播种线程对每个{ point, by, text }用createCommentThread创建一个锚点类型为point的线程用createComment创建首条评论body: toRichText(text)再经putCommentRecords一次性写入挂载评论图层通过TLComponents的InFrontOfTheCanvas槽位渲染CanvasComments currentUserIdme resolveAuthor{resolveAuthor} /并在Tldraw上注册tools{commentTools}与overrides{[commentToolOverrides]}。种子数据刻意分成两组// [1] 两组 Pin组内足够近从而相互合并组间足够远从而保持分离 const THREADS: { point: VecLike; by: string; text: string }[] [ { point: { x: 250, y: 140 }, by: ada, text: Should this column be wider? }, { point: { x: 300, y: 170 }, by: grace, text: Agreed, it wraps on mobile. }, { point: { x: 265, y: 220 }, by: ada, text: Same for the row beneath it. }, { point: { x: 800, y: 250 }, by: grace, text: This label reads as a button. }, { point: { x: 845, y: 285 }, by: ada, text: Lets make it a link instead. }, ]第一组三个 Pin 聚集在 (250, 140) 附近第二组两个 Pin 聚集在 (800, 250) 附近两组相距很远因此缩小画布时组内合并、组间保持分离。操作方式用⌘/ctrl-scroll 或双指捏合缩小画布即可观察每组塌缩为单个计数徽章再放大时裂开。作者通过resolveAuthor回调映射为带颜色的CommentAuthorcurrentUserIdme决定当前用户身份。聚簇算法管线从 Pin 位置到事件表tldraw/commenting的聚簇核心位于 packages/commenting/src/clustering/入口函数computeClusterTablecomputeClusterTable.ts把一屏内所有评论线程的页面坐标leaves转换为一张“预计算调度表”ClusterTable包含按zMerge非递增排序的合并事件序列。管线分四步欧氏最小生成树MSTmst.ts 用 Prim 算法在 Pin 锚点集合上构建最小生成树并带严格的前置条件校验——重复的 leaf id 或非有限坐标NaN/Infinity会直接抛错。边的顺序按“距离、再按规范化端点 id”排序保证同一输入产生确定性结果有上限回放capped replayreplay.ts 沿 MST 边从近到远回放合并。每对相邻簇的有效合并阈值zEff min(Tc / 质心距, Dmax / 并包围盒对角线)即同时受“合并距离 Tc”与“簇出生时最大屏幕直径 Dmax”两个约束收缩contractschedule.ts 的contract把在(1 eps)倍窗口内连续发生的二元合并链折叠为单个多路multi-way合并事件避免低缩放级别下徽章逐级“滚雪球”式地一次只减一定稿finalize为每个事件写入迟滞对(zMerge, zSplit)其中zSplit min(zMerge · Tu/Tc, maxZoom)并受zMerge ≤ maxSplitZoom / (Tu/Tc)的封顶保护——保证“无论成员多近最大maxSplitZoom倍缩放时所有簇都会拆分”包括完全重合的锚点schedule.ts。关键参数及其默认值types.ts 与 computeClusterTable.ts 的resolveOptions参数含义默认值约束Tc合并聚簇距离屏幕像素22有限且 0Tu拆分解聚距离屏幕像素1.2 · Tc即 26.4px必须 Tceps收缩窗口比例0.7≥ 0Dmax簇出生时的最大屏幕直径像素3.75 · Tc≥TcminZoom/maxZoom相机缩放边界必填由编辑器相机选项推导maxZoom minZoom 0maxSplitZoom保证所有簇在此缩放前必然拆分的上限6600% 0minZoom/maxZoom来自编辑器真实的相机配置cluster-model.ts 的getClusterZoomBounds读取editor.getCameraOptions()在有zoomSteps时取zoomSteps[0] * baseZoom与zoomSteps[last] * baseZoom——即聚簇调度表与编辑器允许的缩放档位严格对齐。迟滞Tu 1.2 · Tc正是 README 中“拆分发生在比合并更宽的间距上”的底层来源一个簇在zoom zMerge时合并在zoom zSplit时才拆回两个阈值之间的区间内保持原状态不变。运行时游标为什么只有缩放会“重排”整个文档调度表构建好后runtime.ts 的ClusterRuntime用一个“游标 k”在线性事件表上前进/后退来表达当前缩放级别下的分区seed(zoom)冷启动时把游标推到满足zMerge zoom的事件处一次性应用onCamera(zoom)相机移动时按新缩放推进/回退游标若缩放落在所有迟滞带内部则为无操作no-op——这就是阈值附近不闪烁的直接机制seedFrom(zoom, previous)当重建调度表例如新增/删除评论时把旧分区的“已合并/已拆分”状态精确迁移到新表上保证仅换表本身不会引起任何视觉变化detachLeaves(ids)把若干 leaf 从当前显示的徽章中就地移除只产生一个补丁、一次版本自增画布其余部分不动。React 侧的状态机在 cluster-model.ts 的useClusterModel中其注释明确写出了核心不变量“唯一会全篇重排聚簇的东西是缩放”。其余变化都走局部路径输入门槛评论回复、表情回应、解决线程等只触碰评论记录而不移动 Pin 的操作经clusterInputEqual值比较后直接复用旧输入不触发 O(N²) 级别的调度表重建拖拽冻结当选区工具正在移动形状select.translating等状态路径时输入按 id 相等即冻结唯一的例外是“已经折进徽章里的 leaf 被拖动”——徽章无法跟随成员移动徽章位置是烘焙进模型的此时该 Pin 弹出为“held thread”以实时 Pin 形式跟随锚点并在下一次缩小画布时重新加入聚簇孤儿线程orphan threads显示分区里尚未代表的新评论、重开的线程等先以普通 Pin 渲染等到下一次 zoom-out 才被折叠进簇合并只发生在缩小的方向上。点击徽章放大到“刚好越过拆分点”点击计数徽章触发的展开动画由 cluster-model.ts 的zoomToClusterSplit实现其逻辑值得注意产生当前可见簇的那次合并事件恰好就是把它拆开的事件并且它的zSplit是簇内部所有事件中最小的——因此“该簇内评论的第一次拆分点”就是它自己的zSplit目标缩放为zSplit * 1.055% 过冲避免落回阈值上导致再次闪烁并夹取在相机minZoom/maxZoom之间动画以 450ms 时长驱动相机动画过程中缩放变化会像普通手动缩放一样驱动运行时游标。同理当侧边栏或列表点击某条评论请求“显示 Pin”时revealThreadPin会定位到该线程的直接父合并事件的zSplit以同样带过冲的方式放大并居中若该线程未处于簇中或聚簇已关闭则只做居中不缩放cluster-model.ts。相机居中还会叠加commentCenterScreenOffset让 Pin 落在未被打开的侧边栏遮挡的可视区中央。关闭与定制聚簇聚簇是CommentingOptions上的一个特性开关options.ts静态配置通过工具注册时一次性传入Tldraw tools{[CommentTool.configure({ enableClustering: false })]} /关闭后每个 Pin 在每个缩放级别都独立渲染点击 Pin 的“放大到拆分点”逻辑也随之跳过revealThreadPin中显式判断options.enableClustering。选项读取走getCommentingOptions(editor)从已注册评论工具的状态上取options工具未注册时回退到defaultCommentingOptionsenableClustering的默认值即trueoptions.ts。小结Comment clustering 示例看似只是“缩小看徽章”的视觉糖背后却是一条完整的确定性算法链MST 定序、有上限回放定合并时机、收缩折叠多路合并、迟滞带抑制闪烁、运行时游标保证“只有缩放重排全篇”以及 5% 过冲的点击展开动画。参数Tc22、Tu1.2·Tc、eps0.7、Dmax3.75·Tc、maxSplitZoom6在tldraw/commenting内部使用对CanvasComments用户零配置即可生效需要完全禁用时用CommentTool.configure({ enableClustering: false })一行关闭。所有行为均可在 computeClusterTable.ts、schedule.ts、runtime.ts 及各自的.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),仅供参考