@tldraw/commenting 0.0.0-bootstrap → 5.3.0-canary.04044ed9e96d

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (338) hide show
  1. package/README.md +0 -2
  2. package/commenting.css +1152 -350
  3. package/dist-cjs/canvas/anchor-lifecycle.js +148 -0
  4. package/dist-cjs/canvas/anchor-lifecycle.js.map +7 -0
  5. package/dist-cjs/{ui/avatar.js → canvas/canvas-events.js} +14 -22
  6. package/dist-cjs/canvas/canvas-events.js.map +7 -0
  7. package/dist-cjs/canvas/cluster-badge.js +121 -0
  8. package/dist-cjs/canvas/cluster-badge.js.map +7 -0
  9. package/dist-cjs/canvas/cluster-fade.js +85 -0
  10. package/dist-cjs/canvas/cluster-fade.js.map +7 -0
  11. package/dist-cjs/canvas/cluster-input.js +53 -3
  12. package/dist-cjs/canvas/cluster-input.js.map +2 -2
  13. package/dist-cjs/canvas/cluster-model.js +252 -0
  14. package/dist-cjs/canvas/cluster-model.js.map +7 -0
  15. package/dist-cjs/canvas/comment-body.js +1 -1
  16. package/dist-cjs/canvas/comment-body.js.map +2 -2
  17. package/dist-cjs/canvas/comment-drafts.js +54 -0
  18. package/dist-cjs/canvas/comment-drafts.js.map +7 -0
  19. package/dist-cjs/canvas/comment-mutations.js +152 -0
  20. package/dist-cjs/canvas/comment-mutations.js.map +7 -0
  21. package/dist-cjs/canvas/comment-reactions.js +160 -0
  22. package/dist-cjs/canvas/comment-reactions.js.map +7 -0
  23. package/dist-cjs/canvas/comment-render.js +9 -6
  24. package/dist-cjs/canvas/comment-render.js.map +2 -2
  25. package/dist-cjs/canvas/comment-store.js +19 -9
  26. package/dist-cjs/canvas/comment-store.js.map +2 -2
  27. package/dist-cjs/canvas/comment-tool.js +63 -14
  28. package/dist-cjs/canvas/comment-tool.js.map +2 -2
  29. package/dist-cjs/canvas/comments-filter-menu.js +17 -37
  30. package/dist-cjs/canvas/comments-filter-menu.js.map +2 -2
  31. package/dist-cjs/canvas/comments-overflow-menu.js +25 -22
  32. package/dist-cjs/canvas/comments-overflow-menu.js.map +2 -2
  33. package/dist-cjs/canvas/comments-overlay.js +151 -989
  34. package/dist-cjs/canvas/comments-overlay.js.map +3 -3
  35. package/dist-cjs/canvas/comments-sidebar.js +49 -41
  36. package/dist-cjs/canvas/comments-sidebar.js.map +2 -2
  37. package/dist-cjs/canvas/comments-visibility-toggle.js +45 -0
  38. package/dist-cjs/canvas/comments-visibility-toggle.js.map +7 -0
  39. package/dist-cjs/canvas/context.js +17 -0
  40. package/dist-cjs/canvas/context.js.map +7 -0
  41. package/dist-cjs/canvas/hooks.js +28 -3
  42. package/dist-cjs/canvas/hooks.js.map +2 -2
  43. package/dist-cjs/canvas/license.js.map +1 -1
  44. package/dist-cjs/canvas/mobile-placement.js +91 -0
  45. package/dist-cjs/canvas/mobile-placement.js.map +7 -0
  46. package/dist-cjs/canvas/options.js +57 -2
  47. package/dist-cjs/canvas/options.js.map +2 -2
  48. package/dist-cjs/canvas/pending-composer.js +134 -0
  49. package/dist-cjs/canvas/pending-composer.js.map +7 -0
  50. package/dist-cjs/canvas/pin-stacking.js +81 -0
  51. package/dist-cjs/canvas/pin-stacking.js.map +7 -0
  52. package/dist-cjs/canvas/region-box.js +113 -0
  53. package/dist-cjs/canvas/region-box.js.map +7 -0
  54. package/dist-cjs/canvas/sidebar-filters.js +2 -2
  55. package/dist-cjs/canvas/sidebar-filters.js.map +2 -2
  56. package/dist-cjs/canvas/state.js +28 -14
  57. package/dist-cjs/canvas/state.js.map +2 -2
  58. package/dist-cjs/canvas/thread-pin.js +304 -0
  59. package/dist-cjs/canvas/thread-pin.js.map +7 -0
  60. package/dist-cjs/canvas/thread-preview.js +177 -0
  61. package/dist-cjs/canvas/thread-preview.js.map +7 -0
  62. package/dist-cjs/canvas/thread-stack.js +199 -0
  63. package/dist-cjs/canvas/thread-stack.js.map +7 -0
  64. package/dist-cjs/canvas/thread-state.js +82 -21
  65. package/dist-cjs/canvas/thread-state.js.map +2 -2
  66. package/dist-cjs/canvas/thread-view.js +450 -0
  67. package/dist-cjs/canvas/thread-view.js.map +7 -0
  68. package/dist-cjs/clustering/computeClusterTable.js +6 -6
  69. package/dist-cjs/clustering/computeClusterTable.js.map +2 -2
  70. package/dist-cjs/clustering/replay.js +89 -22
  71. package/dist-cjs/clustering/replay.js.map +3 -3
  72. package/dist-cjs/clustering/runtime.js +126 -7
  73. package/dist-cjs/clustering/runtime.js.map +2 -2
  74. package/dist-cjs/clustering/types.js.map +1 -1
  75. package/dist-cjs/index.d.ts +904 -353
  76. package/dist-cjs/index.js +52 -28
  77. package/dist-cjs/index.js.map +2 -2
  78. package/dist-cjs/ui/byline.js +10 -6
  79. package/dist-cjs/ui/byline.js.map +2 -2
  80. package/dist-cjs/ui/comment-card.js +15 -8
  81. package/dist-cjs/ui/comment-card.js.map +2 -2
  82. package/dist-cjs/ui/comment-composer.js +124 -39
  83. package/dist-cjs/ui/comment-composer.js.map +2 -2
  84. package/dist-cjs/ui/comment-pin.js +11 -2
  85. package/dist-cjs/ui/comment-pin.js.map +2 -2
  86. package/dist-cjs/ui/comment-thread.js +9 -7
  87. package/dist-cjs/ui/comment-thread.js.map +2 -2
  88. package/dist-cjs/ui/comments-list.js +39 -40
  89. package/dist-cjs/ui/comments-list.js.map +2 -2
  90. package/dist-cjs/ui/count-badge.js +3 -2
  91. package/dist-cjs/ui/count-badge.js.map +2 -2
  92. package/dist-cjs/ui/emoji-picker.js +54 -0
  93. package/dist-cjs/ui/emoji-picker.js.map +7 -0
  94. package/dist-cjs/ui/empty-state.js +3 -3
  95. package/dist-cjs/ui/empty-state.js.map +2 -2
  96. package/dist-cjs/ui/format-time.js +16 -1
  97. package/dist-cjs/ui/format-time.js.map +2 -2
  98. package/dist-cjs/ui/icons.js +115 -0
  99. package/dist-cjs/ui/icons.js.map +7 -0
  100. package/dist-cjs/ui/reaction-picker.js +71 -0
  101. package/dist-cjs/ui/reaction-picker.js.map +7 -0
  102. package/dist-cjs/ui/reaction.js +58 -6
  103. package/dist-cjs/ui/reaction.js.map +2 -2
  104. package/dist-cjs/ui/reactions.js +20 -7
  105. package/dist-cjs/ui/reactions.js.map +2 -2
  106. package/dist-cjs/ui/{mention.js → reply-count.js} +9 -11
  107. package/dist-cjs/ui/reply-count.js.map +7 -0
  108. package/dist-cjs/ui/send-button.js +13 -1
  109. package/dist-cjs/ui/send-button.js.map +2 -2
  110. package/dist-cjs/ui/{comment-text.js → visual-viewport.js} +12 -9
  111. package/dist-cjs/ui/visual-viewport.js.map +7 -0
  112. package/dist-esm/canvas/anchor-lifecycle.mjs +128 -0
  113. package/dist-esm/canvas/anchor-lifecycle.mjs.map +7 -0
  114. package/dist-esm/canvas/canvas-events.mjs +15 -0
  115. package/dist-esm/canvas/canvas-events.mjs.map +7 -0
  116. package/dist-esm/canvas/cluster-badge.mjs +106 -0
  117. package/dist-esm/canvas/cluster-badge.mjs.map +7 -0
  118. package/dist-esm/canvas/cluster-fade.mjs +65 -0
  119. package/dist-esm/canvas/cluster-fade.mjs.map +7 -0
  120. package/dist-esm/canvas/cluster-input.mjs +54 -4
  121. package/dist-esm/canvas/cluster-input.mjs.map +2 -2
  122. package/dist-esm/canvas/cluster-model.mjs +236 -0
  123. package/dist-esm/canvas/cluster-model.mjs.map +7 -0
  124. package/dist-esm/canvas/comment-body.mjs +1 -1
  125. package/dist-esm/canvas/comment-body.mjs.map +2 -2
  126. package/dist-esm/canvas/comment-drafts.mjs +34 -0
  127. package/dist-esm/canvas/comment-drafts.mjs.map +7 -0
  128. package/dist-esm/canvas/comment-mutations.mjs +132 -0
  129. package/dist-esm/canvas/comment-mutations.mjs.map +7 -0
  130. package/dist-esm/canvas/comment-reactions.mjs +145 -0
  131. package/dist-esm/canvas/comment-reactions.mjs.map +7 -0
  132. package/dist-esm/canvas/comment-render.mjs +10 -7
  133. package/dist-esm/canvas/comment-render.mjs.map +2 -2
  134. package/dist-esm/canvas/comment-store.mjs +19 -9
  135. package/dist-esm/canvas/comment-store.mjs.map +2 -2
  136. package/dist-esm/canvas/comment-tool.mjs +66 -17
  137. package/dist-esm/canvas/comment-tool.mjs.map +2 -2
  138. package/dist-esm/canvas/comments-filter-menu.mjs +17 -36
  139. package/dist-esm/canvas/comments-filter-menu.mjs.map +2 -2
  140. package/dist-esm/canvas/comments-overflow-menu.mjs +27 -22
  141. package/dist-esm/canvas/comments-overflow-menu.mjs.map +2 -2
  142. package/dist-esm/canvas/comments-overlay.mjs +157 -1010
  143. package/dist-esm/canvas/comments-overlay.mjs.map +3 -3
  144. package/dist-esm/canvas/comments-sidebar.mjs +53 -46
  145. package/dist-esm/canvas/comments-sidebar.mjs.map +2 -2
  146. package/dist-esm/canvas/comments-visibility-toggle.mjs +25 -0
  147. package/dist-esm/canvas/comments-visibility-toggle.mjs.map +7 -0
  148. package/dist-esm/canvas/context.mjs +1 -0
  149. package/dist-esm/canvas/context.mjs.map +7 -0
  150. package/dist-esm/canvas/hooks.mjs +35 -5
  151. package/dist-esm/canvas/hooks.mjs.map +2 -2
  152. package/dist-esm/canvas/license.mjs.map +1 -1
  153. package/dist-esm/canvas/mobile-placement.mjs +71 -0
  154. package/dist-esm/canvas/mobile-placement.mjs.map +7 -0
  155. package/dist-esm/canvas/options.mjs +59 -3
  156. package/dist-esm/canvas/options.mjs.map +2 -2
  157. package/dist-esm/canvas/pending-composer.mjs +126 -0
  158. package/dist-esm/canvas/pending-composer.mjs.map +7 -0
  159. package/dist-esm/canvas/pin-stacking.mjs +61 -0
  160. package/dist-esm/canvas/pin-stacking.mjs.map +7 -0
  161. package/dist-esm/canvas/region-box.mjs +93 -0
  162. package/dist-esm/canvas/region-box.mjs.map +7 -0
  163. package/dist-esm/canvas/sidebar-filters.mjs +2 -2
  164. package/dist-esm/canvas/sidebar-filters.mjs.map +2 -2
  165. package/dist-esm/canvas/state.mjs +29 -19
  166. package/dist-esm/canvas/state.mjs.map +2 -2
  167. package/dist-esm/canvas/thread-pin.mjs +310 -0
  168. package/dist-esm/canvas/thread-pin.mjs.map +7 -0
  169. package/dist-esm/canvas/thread-preview.mjs +163 -0
  170. package/dist-esm/canvas/thread-preview.mjs.map +7 -0
  171. package/dist-esm/canvas/thread-stack.mjs +189 -0
  172. package/dist-esm/canvas/thread-stack.mjs.map +7 -0
  173. package/dist-esm/canvas/thread-state.mjs +86 -22
  174. package/dist-esm/canvas/thread-state.mjs.map +2 -2
  175. package/dist-esm/canvas/thread-view.mjs +461 -0
  176. package/dist-esm/canvas/thread-view.mjs.map +7 -0
  177. package/dist-esm/clustering/computeClusterTable.mjs +6 -6
  178. package/dist-esm/clustering/computeClusterTable.mjs.map +2 -2
  179. package/dist-esm/clustering/replay.mjs +89 -22
  180. package/dist-esm/clustering/replay.mjs.map +3 -3
  181. package/dist-esm/clustering/runtime.mjs +126 -7
  182. package/dist-esm/clustering/runtime.mjs.map +2 -2
  183. package/dist-esm/index.d.mts +904 -353
  184. package/dist-esm/index.mjs +102 -43
  185. package/dist-esm/index.mjs.map +2 -2
  186. package/dist-esm/ui/byline.mjs +11 -7
  187. package/dist-esm/ui/byline.mjs.map +2 -2
  188. package/dist-esm/ui/comment-card.mjs +15 -8
  189. package/dist-esm/ui/comment-card.mjs.map +2 -2
  190. package/dist-esm/ui/comment-composer.mjs +135 -39
  191. package/dist-esm/ui/comment-composer.mjs.map +2 -2
  192. package/dist-esm/ui/comment-pin.mjs +11 -2
  193. package/dist-esm/ui/comment-pin.mjs.map +2 -2
  194. package/dist-esm/ui/comment-thread.mjs +9 -7
  195. package/dist-esm/ui/comment-thread.mjs.map +2 -2
  196. package/dist-esm/ui/comments-list.mjs +51 -50
  197. package/dist-esm/ui/comments-list.mjs.map +2 -2
  198. package/dist-esm/ui/count-badge.mjs +3 -2
  199. package/dist-esm/ui/count-badge.mjs.map +2 -2
  200. package/dist-esm/ui/emoji-picker.mjs +34 -0
  201. package/dist-esm/ui/emoji-picker.mjs.map +7 -0
  202. package/dist-esm/ui/empty-state.mjs +3 -3
  203. package/dist-esm/ui/empty-state.mjs.map +2 -2
  204. package/dist-esm/ui/format-time.mjs +16 -1
  205. package/dist-esm/ui/format-time.mjs.map +2 -2
  206. package/dist-esm/ui/icons.mjs +95 -0
  207. package/dist-esm/ui/icons.mjs.map +7 -0
  208. package/dist-esm/ui/reaction-picker.mjs +59 -0
  209. package/dist-esm/ui/reaction-picker.mjs.map +7 -0
  210. package/dist-esm/ui/reaction.mjs +59 -7
  211. package/dist-esm/ui/reaction.mjs.map +2 -2
  212. package/dist-esm/ui/reactions.mjs +21 -8
  213. package/dist-esm/ui/reactions.mjs.map +2 -2
  214. package/dist-esm/ui/reply-count.mjs +9 -0
  215. package/dist-esm/ui/reply-count.mjs.map +7 -0
  216. package/dist-esm/ui/send-button.mjs +13 -1
  217. package/dist-esm/ui/send-button.mjs.map +2 -2
  218. package/dist-esm/ui/visual-viewport.mjs +12 -0
  219. package/dist-esm/ui/visual-viewport.mjs.map +7 -0
  220. package/package.json +6 -6
  221. package/src/canvas/anchor-lifecycle.test.ts +290 -0
  222. package/src/canvas/anchor-lifecycle.ts +191 -0
  223. package/src/canvas/canvas-events.ts +18 -0
  224. package/src/canvas/canvas.css +464 -55
  225. package/src/canvas/cluster-badge.tsx +133 -0
  226. package/src/canvas/cluster-fade.ts +102 -0
  227. package/src/canvas/cluster-input.test.ts +178 -36
  228. package/src/canvas/cluster-input.ts +95 -9
  229. package/src/canvas/cluster-model.ts +378 -0
  230. package/src/canvas/comment-body.tsx +1 -1
  231. package/src/canvas/comment-drafts.test.ts +55 -0
  232. package/src/canvas/comment-drafts.ts +46 -0
  233. package/src/canvas/comment-mutations.test.ts +452 -0
  234. package/src/canvas/comment-mutations.ts +299 -0
  235. package/src/canvas/comment-reactions.test.ts +154 -0
  236. package/src/canvas/comment-reactions.tsx +246 -0
  237. package/src/canvas/comment-render.test.ts +1 -1
  238. package/src/canvas/comment-render.ts +17 -13
  239. package/src/canvas/comment-store.ts +68 -33
  240. package/src/canvas/comment-tool.test.ts +162 -0
  241. package/src/canvas/comment-tool.tsx +103 -25
  242. package/src/canvas/comments-filter-menu.tsx +20 -36
  243. package/src/canvas/comments-overflow-menu.tsx +17 -19
  244. package/src/canvas/comments-overlay.tsx +252 -1369
  245. package/src/canvas/comments-sidebar.test.ts +45 -0
  246. package/src/canvas/comments-sidebar.tsx +112 -73
  247. package/src/canvas/comments-visibility-toggle.tsx +25 -0
  248. package/src/canvas/context.ts +48 -0
  249. package/src/canvas/hooks.test.ts +94 -0
  250. package/src/canvas/hooks.ts +66 -11
  251. package/src/canvas/license.ts +1 -1
  252. package/src/canvas/mobile-placement.ts +115 -0
  253. package/src/canvas/options.test.ts +176 -4
  254. package/src/canvas/options.ts +276 -18
  255. package/src/canvas/pending-composer.tsx +157 -0
  256. package/src/canvas/pin-stacking.test.ts +224 -0
  257. package/src/canvas/pin-stacking.ts +103 -0
  258. package/src/canvas/region-box.tsx +124 -0
  259. package/src/canvas/sidebar-filters.ts +4 -3
  260. package/src/canvas/state.ts +88 -39
  261. package/src/canvas/thread-pin.tsx +418 -0
  262. package/src/canvas/thread-preview.test.ts +91 -0
  263. package/src/canvas/thread-preview.tsx +294 -0
  264. package/src/canvas/thread-stack.tsx +242 -0
  265. package/src/canvas/thread-state.test.ts +260 -0
  266. package/src/canvas/thread-state.ts +171 -37
  267. package/src/canvas/thread-view.test.ts +72 -0
  268. package/src/canvas/thread-view.tsx +614 -0
  269. package/src/clustering/computeClusterTable.test.ts +24 -23
  270. package/src/clustering/computeClusterTable.ts +17 -8
  271. package/src/clustering/replay.test.ts +0 -7
  272. package/src/clustering/replay.ts +131 -32
  273. package/src/clustering/runtime.test.ts +267 -13
  274. package/src/clustering/runtime.ts +176 -19
  275. package/src/clustering/schedule.test.ts +0 -6
  276. package/src/clustering/screen-offsets.test.ts +171 -0
  277. package/src/clustering/types.ts +19 -9
  278. package/src/index.ts +91 -42
  279. package/src/ui/byline.tsx +21 -9
  280. package/src/ui/comment-card.tsx +20 -10
  281. package/src/ui/comment-composer.tsx +178 -56
  282. package/src/ui/comment-pin.tsx +13 -2
  283. package/src/ui/comment-thread.tsx +13 -6
  284. package/src/ui/comments-list.test.ts +31 -0
  285. package/src/ui/comments-list.tsx +82 -47
  286. package/src/ui/comments.css +577 -294
  287. package/src/ui/count-badge.tsx +9 -2
  288. package/src/ui/emoji-picker.test.ts +25 -0
  289. package/src/ui/emoji-picker.tsx +76 -0
  290. package/src/ui/empty-state.tsx +3 -3
  291. package/src/ui/format-time.test.ts +69 -0
  292. package/src/ui/format-time.ts +27 -2
  293. package/src/ui/icons.tsx +116 -0
  294. package/src/ui/reaction-picker.tsx +102 -0
  295. package/src/ui/reaction.tsx +125 -6
  296. package/src/ui/reactions.tsx +69 -10
  297. package/src/ui/reply-count.ts +16 -0
  298. package/src/ui/send-button.tsx +13 -3
  299. package/src/ui/visual-viewport.test.ts +36 -0
  300. package/src/ui/visual-viewport.ts +27 -0
  301. package/dist-cjs/canvas/region-options.js +0 -49
  302. package/dist-cjs/canvas/region-options.js.map +0 -7
  303. package/dist-cjs/ui/avatar.js.map +0 -7
  304. package/dist-cjs/ui/comment-mention.js +0 -42
  305. package/dist-cjs/ui/comment-mention.js.map +0 -7
  306. package/dist-cjs/ui/comment-text.js.map +0 -7
  307. package/dist-cjs/ui/mention-list.js +0 -70
  308. package/dist-cjs/ui/mention-list.js.map +0 -7
  309. package/dist-cjs/ui/mention-suggestion.js +0 -206
  310. package/dist-cjs/ui/mention-suggestion.js.map +0 -7
  311. package/dist-cjs/ui/mention.js.map +0 -7
  312. package/dist-cjs/ui/render-markdown.js +0 -63
  313. package/dist-cjs/ui/render-markdown.js.map +0 -7
  314. package/dist-esm/canvas/region-options.mjs +0 -29
  315. package/dist-esm/canvas/region-options.mjs.map +0 -7
  316. package/dist-esm/ui/avatar.mjs +0 -23
  317. package/dist-esm/ui/avatar.mjs.map +0 -7
  318. package/dist-esm/ui/comment-mention.mjs +0 -22
  319. package/dist-esm/ui/comment-mention.mjs.map +0 -7
  320. package/dist-esm/ui/comment-text.mjs +0 -9
  321. package/dist-esm/ui/comment-text.mjs.map +0 -7
  322. package/dist-esm/ui/mention-list.mjs +0 -50
  323. package/dist-esm/ui/mention-list.mjs.map +0 -7
  324. package/dist-esm/ui/mention-suggestion.mjs +0 -186
  325. package/dist-esm/ui/mention-suggestion.mjs.map +0 -7
  326. package/dist-esm/ui/mention.mjs +0 -11
  327. package/dist-esm/ui/mention.mjs.map +0 -7
  328. package/dist-esm/ui/render-markdown.mjs +0 -45
  329. package/dist-esm/ui/render-markdown.mjs.map +0 -7
  330. package/src/canvas/region-options.ts +0 -57
  331. package/src/ui/avatar.tsx +0 -31
  332. package/src/ui/comment-mention.ts +0 -47
  333. package/src/ui/comment-text.tsx +0 -12
  334. package/src/ui/mention-list.tsx +0 -106
  335. package/src/ui/mention-suggestion.test.ts +0 -18
  336. package/src/ui/mention-suggestion.tsx +0 -285
  337. package/src/ui/mention.tsx +0 -9
  338. package/src/ui/render-markdown.tsx +0 -72
@@ -86,27 +86,27 @@ describe('computeClusterTable composition', () => {
86
86
  }
87
87
  })
88
88
 
89
- it('resolves all defaults from an empty option set (Tc 40, Tu 60, eps 0.12, Dmax 120)', () => {
89
+ it('resolves all defaults from an empty option set (Tc 22, Tu 26.4, eps 0.7, Dmax 82.5)', () => {
90
90
  const leaves = randomLeaves(30, 7)
91
91
  const table = computeClusterTable(leaves, { ...ZOOM_BOUNDS })
92
92
  const expected = manualPipeline(leaves, {
93
- Tc: 40,
94
- Tu: 60,
95
- eps: 0.12,
96
- Dmax: 120,
93
+ Tc: 22,
94
+ Tu: 26.4,
95
+ eps: 0.7,
96
+ Dmax: 82.5,
97
97
  ...ZOOM_BOUNDS,
98
98
  })
99
99
  expect(tableShapes(table)).toEqual(expected.map(eventShape))
100
100
  })
101
101
 
102
- it('derives Tu and Dmax from a caller-supplied Tc (Tu = 1.5·Tc, Dmax = 3·Tc)', () => {
102
+ it('derives Tu and Dmax from a caller-supplied Tc (Tu = 1.2·Tc, Dmax = 3.75·Tc)', () => {
103
103
  const leaves = randomLeaves(30, 13)
104
104
  const table = computeClusterTable(leaves, { Tc: 50, ...ZOOM_BOUNDS })
105
105
  const expected = manualPipeline(leaves, {
106
106
  Tc: 50,
107
- Tu: 75,
108
- eps: 0.12,
109
- Dmax: 150,
107
+ Tu: 60,
108
+ eps: 0.7,
109
+ Dmax: 187.5,
110
110
  ...ZOOM_BOUNDS,
111
111
  })
112
112
  expect(tableShapes(table)).toEqual(expected.map(eventShape))
@@ -114,11 +114,12 @@ describe('computeClusterTable composition', () => {
114
114
 
115
115
  it('caps coincident pairs at the default maxSplitZoom of 6 (600%)', () => {
116
116
  // Two threads at the same point: raw merge zoom is Infinity (never split), but the
117
- // default cap schedules them to merge at 4 and split at 6 like any ultra-tight pair.
117
+ // default cap schedules them to merge at 5 (maxSplitZoom / (Tu/Tc) = 6 / 1.2) and split
118
+ // at 6 like any ultra-tight pair.
118
119
  const leaves = [leaf('a', 50, 50), leaf('b', 50, 50)]
119
120
  const table = computeClusterTable(leaves, { ...ZOOM_BOUNDS })
120
121
  expect(table.events).toHaveLength(1)
121
- expect(table.events[0].zMerge).toBe(4)
122
+ expect(table.events[0].zMerge).toBe(5)
122
123
  expect(table.events[0].zSplit).toBe(6)
123
124
  })
124
125
 
@@ -126,10 +127,10 @@ describe('computeClusterTable composition', () => {
126
127
  const leaves = randomLeaves(30, 29)
127
128
  const table = computeClusterTable(leaves, { Tu: 100, eps: 0, ...ZOOM_BOUNDS })
128
129
  const expected = manualPipeline(leaves, {
129
- Tc: 40,
130
+ Tc: 22,
130
131
  Tu: 100,
131
132
  eps: 0,
132
- Dmax: 120,
133
+ Dmax: 82.5,
133
134
  ...ZOOM_BOUNDS,
134
135
  })
135
136
  expect(tableShapes(table)).toEqual(expected.map(eventShape))
@@ -193,9 +194,9 @@ describe('computeClusterTable validation', () => {
193
194
  expect(() => computeClusterTable(leaves, { Tc: 0, ...ZOOM_BOUNDS })).toThrow()
194
195
  expect(() => computeClusterTable(leaves, { Tc: -5, ...ZOOM_BOUNDS })).toThrow()
195
196
  expect(() => computeClusterTable(leaves, { Tc: 40, Tu: 40, ...ZOOM_BOUNDS })).toThrow()
196
- expect(() => computeClusterTable(leaves, { Tu: 30, ...ZOOM_BOUNDS })).toThrow() // < default Tc 40
197
+ expect(() => computeClusterTable(leaves, { Tu: 20, ...ZOOM_BOUNDS })).toThrow() // < default Tc 22
197
198
  expect(() => computeClusterTable(leaves, { eps: -0.01, ...ZOOM_BOUNDS })).toThrow()
198
- expect(() => computeClusterTable(leaves, { Dmax: 30, ...ZOOM_BOUNDS })).toThrow() // < default Tc 40
199
+ expect(() => computeClusterTable(leaves, { Dmax: 20, ...ZOOM_BOUNDS })).toThrow() // < default Tc 22
199
200
  expect(() => computeClusterTable(leaves, { Tc: 50, Dmax: 40, ...ZOOM_BOUNDS })).toThrow()
200
201
  expect(() => computeClusterTable(leaves, { minZoom: 0, maxZoom: 8 })).toThrow()
201
202
  expect(() => computeClusterTable(leaves, { minZoom: 8, maxZoom: 8 })).toThrow()
@@ -226,8 +227,8 @@ describe('computeClusterTable end-to-end scenarios', () => {
226
227
  const leaves = Array.from({ length: 8 }, (_, i) => leaf(`p${i + 1}`, i * 10, 0))
227
228
  const table = computeClusterTable(leaves, { ...ZOOM_BOUNDS })
228
229
 
229
- // events (centroid pricing): four pair births at zMerge 4 (zSplit 6), two
230
- // quads at zMerge 2 (zSplit 3), the bridge at zMerge 1 (zSplit 1.5)
230
+ // events (centroid pricing): four pair births at zMerge 2.2 (zSplit 2.64), two
231
+ // quads at zMerge 1.1 (zSplit 1.32), the bridge at zMerge 0.55 (zSplit 0.66)
231
232
  const seedAt = (zoom: number) => {
232
233
  const rt = createClusterRuntime(table)
233
234
  rt.seed(zoom)
@@ -235,12 +236,12 @@ describe('computeClusterTable end-to-end scenarios', () => {
235
236
  }
236
237
  // zoom 8: above every band midpoint → 8 separate pins
237
238
  expect(seedAt(8)).toEqual(['p1', 'p2', 'p3', 'p4', 'p5', 'p6', 'p7', 'p8'])
238
- // zoom 3: below the pair midpoints (√244.9), above the quad midpoint (√6 ≈ 2.45)
239
- expect(seedAt(3)).toEqual(['cluster:2:p1', 'cluster:2:p3', 'cluster:2:p5', 'cluster:2:p7'])
240
- // zoom 2: quads merged, above the bridge midpoint (√1.51.22)
241
- expect(seedAt(2)).toEqual(['cluster:4:p1', 'cluster:4:p5'])
242
- // zoom 1: below everything → one badge of 8
243
- expect(seedAt(1)).toEqual(['cluster:8:p1'])
239
+ // zoom 2: below the pair midpoints (√5.812.41), above the quad midpoint (√1.451.2)
240
+ expect(seedAt(2)).toEqual(['cluster:2:p1', 'cluster:2:p3', 'cluster:2:p5', 'cluster:2:p7'])
241
+ // zoom 1: quads merged, above the bridge midpoint (√0.3630.6)
242
+ expect(seedAt(1)).toEqual(['cluster:4:p1', 'cluster:4:p5'])
243
+ // zoom 0.5: below everything → one badge of 8
244
+ expect(seedAt(0.5)).toEqual(['cluster:8:p1'])
244
245
  })
245
246
 
246
247
  it('keeps far-apart groups exactly independent at eps = 0 (bridge pruned by minZoom)', () => {
@@ -1,7 +1,13 @@
1
1
  import { mstEdges } from './mst'
2
2
  import { cappedReplay } from './replay'
3
3
  import { contract, finalize } from './schedule'
4
- import type { ClusterNode, ClusterOptions, ClusterTable, LeafInput } from './types'
4
+ import type {
5
+ ClusterNode,
6
+ ClusterOptions,
7
+ ClusterTable,
8
+ LeafInput,
9
+ LeafScreenOffsets,
10
+ } from './types'
5
11
 
6
12
  interface ResolvedClusterOptions {
7
13
  Tc: number
@@ -13,10 +19,13 @@ interface ResolvedClusterOptions {
13
19
  maxSplitZoom: number
14
20
  }
15
21
 
16
- /** @public */
22
+ /** @internal */
17
23
  export function computeClusterTable(
18
24
  leaves: readonly LeafInput[],
19
- options: ClusterOptions
25
+ options: ClusterOptions,
26
+ // Render offsets for markers that draw off their anchor (imprecise pins), keyed by leaf id.
27
+ // Omitted or empty, the run is identical — output and code paths — to an offset-unaware one.
28
+ screenOffsets?: LeafScreenOffsets
20
29
  ): ClusterTable {
21
30
  const opts = resolveOptions(options)
22
31
  const leafNodes = leaves.map(leafToNode)
@@ -26,7 +35,7 @@ export function computeClusterTable(
26
35
  return { events: [], leaves: leafNodes }
27
36
  }
28
37
 
29
- const raw = cappedReplay(leaves, edges, { Tc: opts.Tc, Dmax: opts.Dmax })
38
+ const raw = cappedReplay(leaves, edges, { Tc: opts.Tc, Dmax: opts.Dmax }, screenOffsets)
30
39
  const contracted = contract(raw, opts.eps)
31
40
  const events = finalize(contracted, {
32
41
  Tc: opts.Tc,
@@ -40,10 +49,10 @@ export function computeClusterTable(
40
49
  }
41
50
 
42
51
  function resolveOptions(options: ClusterOptions): ResolvedClusterOptions {
43
- const Tc = options.Tc ?? 40
44
- const Tu = options.Tu ?? 1.5 * Tc
45
- const eps = options.eps ?? 0.12
46
- const Dmax = options.Dmax ?? 3 * Tc
52
+ const Tc = options.Tc ?? 22
53
+ const Tu = options.Tu ?? 1.2 * Tc
54
+ const eps = options.eps ?? 0.7
55
+ const Dmax = options.Dmax ?? 3.75 * Tc
47
56
  const maxSplitZoom = options.maxSplitZoom ?? 6
48
57
  const { minZoom, maxZoom } = options
49
58
 
@@ -51,7 +51,6 @@ function eventShapes(events: RawMergeEvent[]) {
51
51
  return events.map(eventShape)
52
52
  }
53
53
 
54
- // --- Reference oracle -------------------------------------------------------
55
54
  // Direct transcription of contract clause 4 (CLUSTERING-STEPS.md step 2): at
56
55
  // each of the n−1 steps, evaluate every unfired MST edge's zEff against the
57
56
  // CURRENT clusters and fire the highest, tie-broken by the edge's normalized
@@ -181,8 +180,6 @@ function referenceReplay(
181
180
  return events
182
181
  }
183
182
 
184
- // --- Shared invariant assertions --------------------------------------------
185
-
186
183
  function expectNonIncreasingZ(events: RawMergeEvent[]) {
187
184
  for (let i = 1; i < events.length; i++) {
188
185
  expect(events[i].z).toBeLessThanOrEqual(events[i - 1].z)
@@ -260,8 +257,6 @@ function expectCentroids(events: RawMergeEvent[], leaves: readonly LeafInput[])
260
257
  }
261
258
  }
262
259
 
263
- // --- Fixtures ----------------------------------------------------------------
264
-
265
260
  describe('cappedReplay fixtures', () => {
266
261
  it('exports the pinned D_FLOOR constant', () => {
267
262
  expect(D_FLOOR).toBe(1e-9)
@@ -409,8 +404,6 @@ describe('cappedReplay fixtures', () => {
409
404
  })
410
405
  })
411
406
 
412
- // --- Properties against the reference oracle ---------------------------------
413
-
414
407
  describe('cappedReplay vs reference simulator', () => {
415
408
  const OPTION_SETS = [
416
409
  { Tc: 40, Dmax: 120 }, // default shape
@@ -1,17 +1,20 @@
1
- import type { ClusterNode, LeafInput, MstEdge, RawMergeEvent } from './types'
1
+ import type { ClusterNode, LeafInput, LeafScreenOffsets, MstEdge, RawMergeEvent } from './types'
2
2
 
3
3
  export const D_FLOOR = 1e-9
4
4
 
5
5
  export function cappedReplay(
6
6
  leaves: readonly LeafInput[],
7
7
  edges: readonly MstEdge[],
8
- opts: { Tc: number; Dmax: number }
8
+ opts: { Tc: number; Dmax: number },
9
+ // Render offsets for markers that draw off their anchor (imprecise pins). Omitted or empty,
10
+ // every code path below is the offset-unaware original — pricing, events, and floats alike.
11
+ screenOffsets?: LeafScreenOffsets
9
12
  ): RawMergeEvent[] {
10
13
  validateOptions(opts)
11
14
 
12
15
  if (edges.length === 0) return []
13
16
 
14
- const clusters = new ClusterState(leaves)
17
+ const clusters = new ClusterState(leaves, screenOffsets)
15
18
  const heap = new EdgeMaxHeap(edges, leaves)
16
19
  // Edges incident to each current cluster root, for eager repricing: a merge
17
20
  // moves the result's centroid, which can RAISE an incident edge's key (the
@@ -95,19 +98,64 @@ function zForRoots(
95
98
  clusters: ClusterState,
96
99
  opts: { Tc: number; Dmax: number }
97
100
  ): number {
98
- if (edge.d < D_FLOOR) return Number.POSITIVE_INFINITY
99
- // Badge-anchored gap pricing: clusters render as badges at their centroids,
100
- // so the merge is priced by the distance between the rendered centers, not
101
- // the nearest members. For leaves the two are identical; for clusters the
102
- // centroid distance is larger, so groups merge later than their closest
103
- // members would suggest matching what the user actually sees.
104
- // (Coincident CENTROIDS with non-coincident members leave the gap term
105
- // Infinity and the fit term finite the min stays finite, no special case.)
106
- const gap = opts.Tc / clusters.centroidDistance(aRoot, bRoot)
101
+ // Null unless offset pricing is active AND this pair's mean render offsets differ. Every
102
+ // other pair including every pair when no offsets were passed — takes the branch below,
103
+ // which is the offset-unaware pricing verbatim.
104
+ const off = clusters.offsetDelta(aRoot, bRoot)
105
+ if (off === null) {
106
+ if (edge.d < D_FLOOR) return Number.POSITIVE_INFINITY
107
+ // Badge-anchored gap pricing: clusters render as badges at their centroids,
108
+ // so the merge is priced by the distance between the rendered centers, not
109
+ // the nearest members. For leaves the two are identical; for clusters the
110
+ // centroid distance is larger, so groups merge later than their closest
111
+ // members would suggest — matching what the user actually sees.
112
+ // (Coincident CENTROIDS with non-coincident members leave the gap term
113
+ // Infinity and the fit term finite — the min stays finite, no special case.)
114
+ const gap = opts.Tc / clusters.centroidDistance(aRoot, bRoot)
115
+ const fit = opts.Dmax / clusters.unionBboxDiag(aRoot, bRoot)
116
+ return Math.min(gap, fit)
117
+ }
118
+
119
+ // Offset-aware gap pricing: these markers render at `z·centroid + meanOffset`, so their
120
+ // visual distance at zoom z is |z·ΔC + Δō| and the merge prices at its Tc crossing. When
121
+ // that distance never reaches Tc (the constant offsets hold the visuals apart harder than
122
+ // the anchors close), the pair prices at 0 — visually never mergeable, pruned by finalize's
123
+ // minZoom cut. No D_FLOOR fast path here: coincident anchors with differing offsets are a
124
+ // constant |Δō| apart on screen, which is exactly the ΔC = 0 case below.
125
+ const dc = clusters.centroidDelta(aRoot, bRoot)
126
+ if (dc.x === 0 && dc.y === 0) {
127
+ return Math.hypot(off.x, off.y) < opts.Tc ? Number.POSITIVE_INFINITY : 0
128
+ }
129
+ const gap = largestVisualCrossing(dc.x, dc.y, off.x, off.y, opts.Tc)
130
+ if (gap === null) return 0
107
131
  const fit = opts.Dmax / clusters.unionBboxDiag(aRoot, bRoot)
108
132
  return Math.min(gap, fit)
109
133
  }
110
134
 
135
+ /**
136
+ * The largest positive root of `|z·ΔC + Δō| = level`, i.e. of
137
+ * `|ΔC|²·z² + 2(ΔC·Δō)·z + (|Δō|² − level²) = 0` — the zoom at which two offset markers are
138
+ * exactly `level` screen px apart, with the distance below the level for every smaller zoom
139
+ * (matching the "merged at z ≤ threshold" model; a lower second crossing, where opposed offsets
140
+ * push the visuals back above the level near z = 0, is deliberately collapsed). Null when the
141
+ * visual distance never reaches the level. Callers handle ΔC = 0 (constant distance) themselves.
142
+ */
143
+ function largestVisualCrossing(
144
+ dcx: number,
145
+ dcy: number,
146
+ dox: number,
147
+ doy: number,
148
+ level: number
149
+ ): number | null {
150
+ const a = dcx * dcx + dcy * dcy
151
+ const b = 2 * (dcx * dox + dcy * doy)
152
+ const c = dox * dox + doy * doy - level * level
153
+ const disc = b * b - 4 * a * c
154
+ if (disc < 0) return null
155
+ const z = (-b + Math.sqrt(disc)) / (2 * a)
156
+ return z > 0 ? z : null
157
+ }
158
+
111
159
  class ClusterState {
112
160
  private readonly parent: Int32Array
113
161
  private readonly minX: Float64Array
@@ -120,8 +168,13 @@ class ClusterState {
120
168
  private readonly nodes: ClusterNode[]
121
169
  private readonly memberLists: string[][]
122
170
  private readonly minMemberIds: string[]
171
+ // Per-cluster sums of member render offsets (screen px), maintained like the centroid sums.
172
+ // Null when no offsets were passed — offsetDelta() then answers null unconditionally, which
173
+ // routes every pricing call down the offset-unaware path.
174
+ private readonly offsetX: Float64Array | null
175
+ private readonly offsetY: Float64Array | null
123
176
 
124
- constructor(leaves: readonly LeafInput[]) {
177
+ constructor(leaves: readonly LeafInput[], screenOffsets?: LeafScreenOffsets) {
125
178
  const n = leaves.length
126
179
  this.parent = new Int32Array(n)
127
180
  this.minX = new Float64Array(n)
@@ -135,6 +188,21 @@ class ClusterState {
135
188
  this.memberLists = new Array(n)
136
189
  this.minMemberIds = new Array(n)
137
190
 
191
+ if (screenOffsets !== undefined && screenOffsets.size > 0) {
192
+ this.offsetX = new Float64Array(n)
193
+ this.offsetY = new Float64Array(n)
194
+ for (let i = 0; i < n; i++) {
195
+ const offset = screenOffsets.get(leaves[i].id)
196
+ if (offset) {
197
+ this.offsetX[i] = offset.x
198
+ this.offsetY[i] = offset.y
199
+ }
200
+ }
201
+ } else {
202
+ this.offsetX = null
203
+ this.offsetY = null
204
+ }
205
+
138
206
  for (let i = 0; i < n; i++) {
139
207
  const leaf = leaves[i]
140
208
  this.parent[i] = i
@@ -176,6 +244,24 @@ class ClusterState {
176
244
  )
177
245
  }
178
246
 
247
+ centroidDelta(aRoot: number, bRoot: number): { x: number; y: number } {
248
+ return {
249
+ x: this.centroidX[aRoot] - this.centroidX[bRoot],
250
+ y: this.centroidY[aRoot] - this.centroidY[bRoot],
251
+ }
252
+ }
253
+
254
+ /** The difference of the two clusters' mean render offsets (screen px), or null when it's
255
+ * zero — including always when no offsets were passed. Null routes pricing down the
256
+ * offset-unaware path. */
257
+ offsetDelta(aRoot: number, bRoot: number): { x: number; y: number } | null {
258
+ if (this.offsetX === null || this.offsetY === null) return null
259
+ const x = this.offsetX[aRoot] / this.counts[aRoot] - this.offsetX[bRoot] / this.counts[bRoot]
260
+ const y = this.offsetY[aRoot] / this.counts[aRoot] - this.offsetY[bRoot] / this.counts[bRoot]
261
+ if (x === 0 && y === 0) return null
262
+ return { x, y }
263
+ }
264
+
179
265
  unionBboxDiag(aRoot: number, bRoot: number): number {
180
266
  const minX = Math.min(this.minX[aRoot], this.minX[bRoot])
181
267
  const minY = Math.min(this.minY[aRoot], this.minY[bRoot])
@@ -224,6 +310,10 @@ class ClusterState {
224
310
  this.nodes[leftRoot] = result
225
311
  this.memberLists[leftRoot] = members
226
312
  this.minMemberIds[leftRoot] = members[0]
313
+ if (this.offsetX !== null && this.offsetY !== null) {
314
+ this.offsetX[leftRoot] += this.offsetX[rightRoot]
315
+ this.offsetY[leftRoot] += this.offsetY[rightRoot]
316
+ }
227
317
 
228
318
  return { z, children: [left, right], result }
229
319
  }
@@ -253,11 +343,29 @@ interface HeapEntry {
253
343
 
254
344
  class EdgeMaxHeap {
255
345
  private readonly items: HeapEntry[] = []
256
-
257
- constructor(
258
- private readonly edges: readonly MstEdge[],
259
- private readonly leaves: readonly LeafInput[]
260
- ) {}
346
+ // Per-edge normalized (lo, hi) id pair for the z tie-break, precomputed once so comparisons
347
+ // allocate nothing. With coincident anchors every edge prices to the same z (+Infinity), so
348
+ // the tie-break runs on nearly every comparison of a rebuild — allocating the pair there
349
+ // churned millions of short-lived tuples.
350
+ private readonly loIds: string[]
351
+ private readonly hiIds: string[]
352
+
353
+ constructor(edges: readonly MstEdge[], leaves: readonly LeafInput[]) {
354
+ const n = edges.length
355
+ this.loIds = new Array(n)
356
+ this.hiIds = new Array(n)
357
+ for (let i = 0; i < n; i++) {
358
+ const aId = leaves[edges[i].a].id
359
+ const bId = leaves[edges[i].b].id
360
+ if (aId < bId) {
361
+ this.loIds[i] = aId
362
+ this.hiIds[i] = bId
363
+ } else {
364
+ this.loIds[i] = bId
365
+ this.hiIds[i] = aId
366
+ }
367
+ }
368
+ }
261
369
 
262
370
  peek(): HeapEntry | undefined {
263
371
  return this.items[0]
@@ -282,7 +390,11 @@ class EdgeMaxHeap {
282
390
  higherPriority(a: HeapEntry, b: HeapEntry): boolean {
283
391
  if (a.z > b.z) return true
284
392
  if (a.z < b.z) return false
285
- return edgeIdPairLess(this.edges[a.edgeIndex], this.edges[b.edgeIndex], this.leaves)
393
+ // Tie-break on the normalized leaf-id pair, ascending.
394
+ const aLo = this.loIds[a.edgeIndex]
395
+ const bLo = this.loIds[b.edgeIndex]
396
+ if (aLo !== bLo) return aLo < bLo
397
+ return this.hiIds[a.edgeIndex] < this.hiIds[b.edgeIndex]
286
398
  }
287
399
 
288
400
  private siftUp(index: number) {
@@ -311,16 +423,3 @@ class EdgeMaxHeap {
311
423
  }
312
424
  }
313
425
  }
314
-
315
- function edgeIdPairLess(a: MstEdge, b: MstEdge, leaves: readonly LeafInput[]): boolean {
316
- const [aLo, aHi] = normalizedEdgeIds(a, leaves)
317
- const [bLo, bHi] = normalizedEdgeIds(b, leaves)
318
- if (aLo !== bLo) return aLo < bLo
319
- return aHi < bHi
320
- }
321
-
322
- function normalizedEdgeIds(edge: MstEdge, leaves: readonly LeafInput[]): [string, string] {
323
- const aId = leaves[edge.a].id
324
- const bId = leaves[edge.b].id
325
- return aId < bId ? [aId, bId] : [bId, aId]
326
- }