@tldraw/commenting 5.3.0-next.2fa9c61a8de6 → 5.3.0-next.7654e7ac2a02

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 (238) hide show
  1. package/commenting.css +63 -71
  2. package/dist-cjs/canvas/anchor-lifecycle.js +2 -2
  3. package/dist-cjs/canvas/anchor-lifecycle.js.map +2 -2
  4. package/dist-cjs/{ui/tooltip-button.js → canvas/canvas-events.js} +16 -13
  5. package/dist-cjs/canvas/canvas-events.js.map +7 -0
  6. package/dist-cjs/canvas/cluster-badge.js +121 -0
  7. package/dist-cjs/canvas/cluster-badge.js.map +7 -0
  8. package/dist-cjs/canvas/cluster-fade.js +85 -0
  9. package/dist-cjs/canvas/cluster-fade.js.map +7 -0
  10. package/dist-cjs/canvas/cluster-input.js +51 -1
  11. package/dist-cjs/canvas/cluster-input.js.map +2 -2
  12. package/dist-cjs/canvas/cluster-model.js +252 -0
  13. package/dist-cjs/canvas/cluster-model.js.map +7 -0
  14. package/dist-cjs/canvas/comment-mutations.js +57 -28
  15. package/dist-cjs/canvas/comment-mutations.js.map +2 -2
  16. package/dist-cjs/canvas/comment-reactions.js +4 -7
  17. package/dist-cjs/canvas/comment-reactions.js.map +2 -2
  18. package/dist-cjs/canvas/comment-render.js +4 -3
  19. package/dist-cjs/canvas/comment-render.js.map +2 -2
  20. package/dist-cjs/canvas/comment-store.js.map +2 -2
  21. package/dist-cjs/canvas/comment-tool.js +0 -1
  22. package/dist-cjs/canvas/comment-tool.js.map +2 -2
  23. package/dist-cjs/canvas/comments-filter-menu.js +11 -13
  24. package/dist-cjs/canvas/comments-filter-menu.js.map +2 -2
  25. package/dist-cjs/canvas/comments-overflow-menu.js +10 -2
  26. package/dist-cjs/canvas/comments-overflow-menu.js.map +2 -2
  27. package/dist-cjs/canvas/comments-overlay.js +77 -870
  28. package/dist-cjs/canvas/comments-overlay.js.map +3 -3
  29. package/dist-cjs/canvas/comments-sidebar.js +16 -11
  30. package/dist-cjs/canvas/comments-sidebar.js.map +2 -2
  31. package/dist-cjs/canvas/comments-visibility-toggle.js +5 -25
  32. package/dist-cjs/canvas/comments-visibility-toggle.js.map +2 -2
  33. package/dist-cjs/canvas/context.js.map +1 -1
  34. package/dist-cjs/canvas/hooks.js +8 -2
  35. package/dist-cjs/canvas/hooks.js.map +2 -2
  36. package/dist-cjs/canvas/license.js.map +1 -1
  37. package/dist-cjs/canvas/mobile-placement.js +91 -0
  38. package/dist-cjs/canvas/mobile-placement.js.map +7 -0
  39. package/dist-cjs/canvas/options.js +39 -1
  40. package/dist-cjs/canvas/options.js.map +2 -2
  41. package/dist-cjs/canvas/pending-composer.js +134 -0
  42. package/dist-cjs/canvas/pending-composer.js.map +7 -0
  43. package/dist-cjs/canvas/pin-stacking.js +26 -1
  44. package/dist-cjs/canvas/pin-stacking.js.map +2 -2
  45. package/dist-cjs/canvas/region-box.js +113 -0
  46. package/dist-cjs/canvas/region-box.js.map +7 -0
  47. package/dist-cjs/canvas/state.js.map +2 -2
  48. package/dist-cjs/canvas/thread-pin.js +304 -0
  49. package/dist-cjs/canvas/thread-pin.js.map +7 -0
  50. package/dist-cjs/canvas/thread-preview.js +41 -49
  51. package/dist-cjs/canvas/thread-preview.js.map +2 -2
  52. package/dist-cjs/canvas/thread-stack.js +7 -7
  53. package/dist-cjs/canvas/thread-stack.js.map +2 -2
  54. package/dist-cjs/canvas/thread-state.js +15 -0
  55. package/dist-cjs/canvas/thread-state.js.map +2 -2
  56. package/dist-cjs/canvas/thread-view.js +120 -67
  57. package/dist-cjs/canvas/thread-view.js.map +3 -3
  58. package/dist-cjs/clustering/computeClusterTable.js +2 -2
  59. package/dist-cjs/clustering/computeClusterTable.js.map +2 -2
  60. package/dist-cjs/clustering/replay.js +89 -22
  61. package/dist-cjs/clustering/replay.js.map +3 -3
  62. package/dist-cjs/clustering/runtime.js +19 -12
  63. package/dist-cjs/clustering/runtime.js.map +2 -2
  64. package/dist-cjs/clustering/types.js.map +1 -1
  65. package/dist-cjs/index.d.ts +323 -202
  66. package/dist-cjs/index.js +7 -1
  67. package/dist-cjs/index.js.map +2 -2
  68. package/dist-cjs/ui/byline.js +5 -3
  69. package/dist-cjs/ui/byline.js.map +2 -2
  70. package/dist-cjs/ui/comment-composer.js +2 -18
  71. package/dist-cjs/ui/comment-composer.js.map +2 -2
  72. package/dist-cjs/ui/comment-pin.js +2 -15
  73. package/dist-cjs/ui/comment-pin.js.map +2 -2
  74. package/dist-cjs/ui/comments-list.js +11 -24
  75. package/dist-cjs/ui/comments-list.js.map +2 -2
  76. package/dist-cjs/ui/format-time.js +12 -0
  77. package/dist-cjs/ui/format-time.js.map +2 -2
  78. package/dist-cjs/ui/icons.js +115 -0
  79. package/dist-cjs/ui/icons.js.map +7 -0
  80. package/dist-cjs/ui/reaction-picker.js +11 -13
  81. package/dist-cjs/ui/reaction-picker.js.map +2 -2
  82. package/dist-cjs/ui/reaction.js +3 -4
  83. package/dist-cjs/ui/reaction.js.map +2 -2
  84. package/dist-cjs/ui/send-button.js +2 -9
  85. package/dist-cjs/ui/send-button.js.map +2 -2
  86. package/dist-cjs/ui/visual-viewport.js +32 -0
  87. package/dist-cjs/ui/visual-viewport.js.map +7 -0
  88. package/dist-esm/canvas/anchor-lifecycle.mjs +3 -3
  89. package/dist-esm/canvas/anchor-lifecycle.mjs.map +2 -2
  90. package/dist-esm/canvas/canvas-events.mjs +15 -0
  91. package/dist-esm/canvas/canvas-events.mjs.map +7 -0
  92. package/dist-esm/canvas/cluster-badge.mjs +106 -0
  93. package/dist-esm/canvas/cluster-badge.mjs.map +7 -0
  94. package/dist-esm/canvas/cluster-fade.mjs +65 -0
  95. package/dist-esm/canvas/cluster-fade.mjs.map +7 -0
  96. package/dist-esm/canvas/cluster-input.mjs +52 -2
  97. package/dist-esm/canvas/cluster-input.mjs.map +2 -2
  98. package/dist-esm/canvas/cluster-model.mjs +236 -0
  99. package/dist-esm/canvas/cluster-model.mjs.map +7 -0
  100. package/dist-esm/canvas/comment-mutations.mjs +57 -28
  101. package/dist-esm/canvas/comment-mutations.mjs.map +2 -2
  102. package/dist-esm/canvas/comment-reactions.mjs +5 -12
  103. package/dist-esm/canvas/comment-reactions.mjs.map +2 -2
  104. package/dist-esm/canvas/comment-render.mjs +5 -4
  105. package/dist-esm/canvas/comment-render.mjs.map +2 -2
  106. package/dist-esm/canvas/comment-store.mjs.map +2 -2
  107. package/dist-esm/canvas/comment-tool.mjs +0 -1
  108. package/dist-esm/canvas/comment-tool.mjs.map +2 -2
  109. package/dist-esm/canvas/comments-filter-menu.mjs +12 -13
  110. package/dist-esm/canvas/comments-filter-menu.mjs.map +2 -2
  111. package/dist-esm/canvas/comments-overflow-menu.mjs +11 -2
  112. package/dist-esm/canvas/comments-overflow-menu.mjs.map +2 -2
  113. package/dist-esm/canvas/comments-overlay.mjs +54 -878
  114. package/dist-esm/canvas/comments-overlay.mjs.map +3 -3
  115. package/dist-esm/canvas/comments-sidebar.mjs +18 -14
  116. package/dist-esm/canvas/comments-sidebar.mjs.map +2 -2
  117. package/dist-esm/canvas/comments-visibility-toggle.mjs +5 -25
  118. package/dist-esm/canvas/comments-visibility-toggle.mjs.map +2 -2
  119. package/dist-esm/canvas/hooks.mjs +9 -2
  120. package/dist-esm/canvas/hooks.mjs.map +2 -2
  121. package/dist-esm/canvas/license.mjs.map +1 -1
  122. package/dist-esm/canvas/mobile-placement.mjs +71 -0
  123. package/dist-esm/canvas/mobile-placement.mjs.map +7 -0
  124. package/dist-esm/canvas/options.mjs +39 -1
  125. package/dist-esm/canvas/options.mjs.map +2 -2
  126. package/dist-esm/canvas/pending-composer.mjs +126 -0
  127. package/dist-esm/canvas/pending-composer.mjs.map +7 -0
  128. package/dist-esm/canvas/pin-stacking.mjs +26 -1
  129. package/dist-esm/canvas/pin-stacking.mjs.map +2 -2
  130. package/dist-esm/canvas/region-box.mjs +93 -0
  131. package/dist-esm/canvas/region-box.mjs.map +7 -0
  132. package/dist-esm/canvas/state.mjs.map +2 -2
  133. package/dist-esm/canvas/thread-pin.mjs +310 -0
  134. package/dist-esm/canvas/thread-pin.mjs.map +7 -0
  135. package/dist-esm/canvas/thread-preview.mjs +42 -49
  136. package/dist-esm/canvas/thread-preview.mjs.map +2 -2
  137. package/dist-esm/canvas/thread-stack.mjs +8 -9
  138. package/dist-esm/canvas/thread-stack.mjs.map +2 -2
  139. package/dist-esm/canvas/thread-state.mjs +15 -0
  140. package/dist-esm/canvas/thread-state.mjs.map +2 -2
  141. package/dist-esm/canvas/thread-view.mjs +131 -72
  142. package/dist-esm/canvas/thread-view.mjs.map +3 -3
  143. package/dist-esm/clustering/computeClusterTable.mjs +2 -2
  144. package/dist-esm/clustering/computeClusterTable.mjs.map +2 -2
  145. package/dist-esm/clustering/replay.mjs +89 -22
  146. package/dist-esm/clustering/replay.mjs.map +3 -3
  147. package/dist-esm/clustering/runtime.mjs +19 -12
  148. package/dist-esm/clustering/runtime.mjs.map +2 -2
  149. package/dist-esm/index.d.mts +323 -202
  150. package/dist-esm/index.mjs +16 -3
  151. package/dist-esm/index.mjs.map +2 -2
  152. package/dist-esm/ui/byline.mjs +6 -4
  153. package/dist-esm/ui/byline.mjs.map +2 -2
  154. package/dist-esm/ui/comment-composer.mjs +2 -18
  155. package/dist-esm/ui/comment-composer.mjs.map +2 -2
  156. package/dist-esm/ui/comment-pin.mjs +2 -15
  157. package/dist-esm/ui/comment-pin.mjs.map +2 -2
  158. package/dist-esm/ui/comments-list.mjs +10 -23
  159. package/dist-esm/ui/comments-list.mjs.map +2 -2
  160. package/dist-esm/ui/format-time.mjs +12 -0
  161. package/dist-esm/ui/format-time.mjs.map +2 -2
  162. package/dist-esm/ui/icons.mjs +95 -0
  163. package/dist-esm/ui/icons.mjs.map +7 -0
  164. package/dist-esm/ui/reaction-picker.mjs +12 -13
  165. package/dist-esm/ui/reaction-picker.mjs.map +2 -2
  166. package/dist-esm/ui/reaction.mjs +3 -4
  167. package/dist-esm/ui/reaction.mjs.map +2 -2
  168. package/dist-esm/ui/send-button.mjs +2 -9
  169. package/dist-esm/ui/send-button.mjs.map +2 -2
  170. package/dist-esm/ui/visual-viewport.mjs +12 -0
  171. package/dist-esm/ui/visual-viewport.mjs.map +7 -0
  172. package/package.json +5 -5
  173. package/src/canvas/anchor-lifecycle.test.ts +2 -2
  174. package/src/canvas/anchor-lifecycle.ts +25 -39
  175. package/src/canvas/canvas-events.ts +18 -0
  176. package/src/canvas/canvas.css +5 -11
  177. package/src/canvas/cluster-badge.tsx +133 -0
  178. package/src/canvas/cluster-fade.ts +102 -0
  179. package/src/canvas/cluster-input.test.ts +148 -14
  180. package/src/canvas/cluster-input.ts +92 -5
  181. package/src/canvas/cluster-model.ts +378 -0
  182. package/src/canvas/comment-mutations.test.ts +46 -4
  183. package/src/canvas/comment-mutations.ts +115 -104
  184. package/src/canvas/comment-reactions.test.ts +22 -0
  185. package/src/canvas/comment-reactions.tsx +33 -32
  186. package/src/canvas/comment-render.ts +8 -8
  187. package/src/canvas/comment-store.ts +12 -17
  188. package/src/canvas/comment-tool.tsx +8 -12
  189. package/src/canvas/comments-filter-menu.tsx +9 -16
  190. package/src/canvas/comments-overflow-menu.tsx +8 -3
  191. package/src/canvas/comments-overlay.tsx +154 -1344
  192. package/src/canvas/comments-sidebar.tsx +40 -19
  193. package/src/canvas/comments-visibility-toggle.tsx +6 -30
  194. package/src/canvas/context.ts +4 -3
  195. package/src/canvas/hooks.test.ts +94 -0
  196. package/src/canvas/hooks.ts +17 -3
  197. package/src/canvas/license.ts +1 -1
  198. package/src/canvas/mobile-placement.ts +115 -0
  199. package/src/canvas/options.test.ts +124 -2
  200. package/src/canvas/options.ts +192 -58
  201. package/src/canvas/pending-composer.tsx +157 -0
  202. package/src/canvas/pin-stacking.test.ts +111 -1
  203. package/src/canvas/pin-stacking.ts +46 -0
  204. package/src/canvas/region-box.tsx +124 -0
  205. package/src/canvas/state.ts +20 -28
  206. package/src/canvas/thread-pin.tsx +418 -0
  207. package/src/canvas/thread-preview.tsx +77 -87
  208. package/src/canvas/thread-stack.tsx +19 -29
  209. package/src/canvas/thread-state.test.ts +51 -0
  210. package/src/canvas/thread-state.ts +56 -23
  211. package/src/canvas/thread-view.test.ts +72 -0
  212. package/src/canvas/thread-view.tsx +231 -133
  213. package/src/clustering/computeClusterTable.ts +12 -3
  214. package/src/clustering/replay.test.ts +0 -7
  215. package/src/clustering/replay.ts +131 -32
  216. package/src/clustering/runtime.test.ts +50 -6
  217. package/src/clustering/runtime.ts +42 -39
  218. package/src/clustering/schedule.test.ts +0 -6
  219. package/src/clustering/screen-offsets.test.ts +171 -0
  220. package/src/clustering/types.ts +10 -0
  221. package/src/index.ts +15 -2
  222. package/src/ui/byline.tsx +16 -6
  223. package/src/ui/comment-composer.tsx +25 -69
  224. package/src/ui/comment-pin.tsx +3 -16
  225. package/src/ui/comments-list.tsx +40 -29
  226. package/src/ui/comments.css +58 -60
  227. package/src/ui/format-time.test.ts +69 -0
  228. package/src/ui/format-time.ts +20 -0
  229. package/src/ui/icons.tsx +116 -0
  230. package/src/ui/reaction-picker.tsx +9 -16
  231. package/src/ui/reaction.tsx +10 -6
  232. package/src/ui/send-button.tsx +3 -8
  233. package/src/ui/visual-viewport.test.ts +36 -0
  234. package/src/ui/visual-viewport.ts +27 -0
  235. package/dist-cjs/ui/tooltip-button.js.map +0 -7
  236. package/dist-esm/ui/tooltip-button.mjs +0 -12
  237. package/dist-esm/ui/tooltip-button.mjs.map +0 -7
  238. package/src/ui/tooltip-button.tsx +0 -20
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/canvas/thread-view.tsx"],
4
- "sourcesContent": ["import { ReactNode, useCallback, useEffect, useRef, useState, type CSSProperties } from 'react'\nimport { createPortal } from 'react-dom'\nimport {\n\tcreateComment,\n\tEditor,\n\tTLComment,\n\tTLCommentThread,\n\tTLRichText,\n\tTldrawUiDropdownMenuContent,\n\tTldrawUiDropdownMenuGroup,\n\tTldrawUiDropdownMenuItem,\n\tTldrawUiDropdownMenuRoot,\n\tTldrawUiDropdownMenuTrigger,\n\tTldrawUiIcon,\n\tuseContainer,\n\tusePassThroughMouseOverEvents,\n\tusePassThroughWheelEvents,\n\tuseTranslation,\n} from 'tldraw'\nimport { CommentCard, CommentCardProps } from '../ui/comment-card'\nimport { CommentComposer } from '../ui/comment-composer'\nimport { EMPTY_COMMENT, isCommentEmpty } from '../ui/comment-extensions'\nimport { CommentThread } from '../ui/comment-thread'\nimport { TooltipButton } from '../ui/tooltip-button'\nimport { CommentBody } from './comment-body'\nimport {\n\tclearCommentDraft,\n\tgetCommentDraft,\n\treplyDraftSlot,\n\tsaveCommentDraft,\n} from './comment-drafts'\nimport {\n\tcommitCommentMutation,\n\tdeleteComment,\n\tdeleteThread,\n\teditComment,\n\tputRecordsInCommit,\n\treopenThread,\n\tresolveThread,\n} from './comment-mutations'\nimport { CommentReactionPicker, CommentReactions } from './comment-reactions'\nimport { UNKNOWN_AUTHOR, UNKNOWN_COMMENT_AUTHOR } from './comment-render'\nimport { type CommentingContext } from './context'\nimport { useThreadComments } from './hooks'\nimport { type CommentingComponents, useCanComment, useCommentingOptions } from './options'\nimport { openThreadId } from './state'\n\nconst stop = (e: { stopPropagation(): void }) => e.stopPropagation()\n\n/**\n * A name-only view of an author resolver, for the mention/rich-text paths. Stable identity, so\n * `CommentBody`'s memoized render doesn't recompute on every render of its host.\n */\nexport function useResolveName(resolveAuthor: CommentingContext['resolveAuthor']) {\n\treturn useCallback((id: string) => resolveAuthor(id)?.name, [resolveAuthor])\n}\n\nexport function toCardProps(\n\tcomment: TLComment,\n\tprops: Pick<CommentingContext, 'currentUserId' | 'resolveAuthor'>,\n\tcomponents: CommentingComponents,\n\tresolveName: (id: string) => string | undefined\n): CommentCardProps {\n\tconst Body = components.CommentBody\n\t// The `CommentBody` component slot overrides the built-in rich-text default (which resolves\n\t// mention ids to names).\n\tconst body = Body ? (\n\t\t<Body comment={comment} />\n\t) : (\n\t\t<CommentBody richText={comment.body} resolveName={resolveName} />\n\t)\n\treturn {\n\t\tauthor: props.resolveAuthor(comment.authorId) ?? UNKNOWN_COMMENT_AUTHOR,\n\t\tbody,\n\t\tdate: new Date(comment.createdAt).toISOString(),\n\t\tyou: comment.authorId === props.currentUserId,\n\t\tedited: comment.editedAt != null,\n\t}\n}\n\n/** A pin is this square (mirrors `--tlui-cmt-pin-size`). Needed because the two marker kinds are\n * different sizes *and* anchor at different points, and lining their previews up means\n * correcting for that. Keep in sync with the stylesheet. */\nconst PIN_SIZE = 28\n\n/** A cluster/stack badge is this square (`--tlui-cmt-marker-size`) \u2014 pin-sized, so the marker\n * kinds read as one family. Keep in sync with the stylesheet. */\nconst MARKER_SIZE = PIN_SIZE\n\n/** Where a popover's top card sits vertically, measured from the marker visual's middle. */\nconst CARD_TOP_Y = -28\n\n/** The horizontal gap between a marker visual's right edge and its popover/preview panel. The\n * preview's hover bridge spans exactly this (see `--tlui-cmt-preview-bridge`). */\nconst PREVIEW_GAP = 6\n\n/**\n * Where a marker's popover sits relative to the marker's anchor point.\n *\n * The hover preview places itself at these same origins, so the two views of a thread differ only\n * by the header the popover has \u2014 which its own stylesheet then compensates for. Moving a popover\n * here moves its preview with it.\n *\n * Both marker kinds hang off their point the same way (`translate(0, -100%)`): the anchor is the\n * visual's bottom-left corner. They differ only in size, so each offset clears its own width plus\n * the shared gap, and re-bases the shared card-top measure from its bottom anchor to its middle \u2014\n * keeping the two previews' top cards on the same line.\n */\nexport const POPOVER_OFFSET = {\n\tthread: { x: PIN_SIZE + PREVIEW_GAP, y: CARD_TOP_Y - PIN_SIZE / 2 },\n\tlist: { x: MARKER_SIZE + PREVIEW_GAP, y: CARD_TOP_Y - MARKER_SIZE / 2 },\n} as const\n\n/** The open thread's popover container, portaled above the UI panels. Over it, wheel and hover\n * events pass through to the canvas (unless it scrolls its own content), like tldraw's panels. */\nexport function ThreadPopover({\n\tcontainer,\n\tstyle,\n\tchildren,\n}: {\n\tcontainer: HTMLElement\n\tstyle: CSSProperties\n\tchildren: ReactNode\n}) {\n\tconst ref = useRef<HTMLDivElement>(null)\n\tusePassThroughWheelEvents(ref)\n\tusePassThroughMouseOverEvents(ref)\n\treturn createPortal(\n\t\t// contextmenu also stops here: portals bubble React events to the canvas's context-menu\n\t\t// trigger (the layer mounts inside it), which would open the canvas menu over this panel.\n\t\t<div\n\t\t\tref={ref}\n\t\t\tclassName=\"tlui-cmt-canvas-popover\"\n\t\t\tstyle={style}\n\t\t\tonPointerDown={stop}\n\t\t\tonContextMenu={stop}\n\t\t>\n\t\t\t{children}\n\t\t</div>,\n\t\tcontainer\n\t)\n}\n\n/**\n * One thread's interactive view: its comments, the reply composer, edit-in-place on your own\n * comments, and the resolve/delete actions. Reads and writes comment records via the editor's\n * store; read receipts are reported for every unread comment while mounted, so only mount it\n * where the thread is actually being shown.\n */\nexport function ThreadView({\n\teditor,\n\tthread,\n\t...props\n}: CommentingContext & { editor: Editor; thread: TLCommentThread }) {\n\tconst {\n\t\tcurrentUserId,\n\t\tresolveAuthor,\n\t\tonPostComment,\n\t\tisCommentUnread,\n\t\tonCommentRead,\n\t\tgetMentionSuggestions,\n\t\trenderMentionSuggestion,\n\t} = props\n\tconst options = useCommentingOptions()\n\tconst comments = useThreadComments(editor, thread.id)\n\tconst msg = useTranslation()\n\tconst resolveName = useResolveName(resolveAuthor)\n\tconst me = currentUserId ? resolveAuthor(currentUserId) : undefined\n\t// Composing, editing, deleting, and resolving are all commenting writes: gated on the viewer's\n\t// permission. Where it's withheld the composer gives way to the ComposerFallback slot (a\n\t// sign-in prompt, say) and the action affordances are hidden.\n\tconst canComment = useCanComment(currentUserId)\n\tconst ComposerFallback = options.components.ComposerFallback\n\t// An unsent reply survives closing the thread (saved on every change, keyed by thread id) \u2014\n\t// the flip side of dismissing without a discard warning.\n\tconst [reply, setReply] = useState<TLRichText>(\n\t\t() => getCommentDraft(replyDraftSlot(thread.id)) ?? EMPTY_COMMENT\n\t)\n\tconst [editingId, setEditingId] = useState<string | null>(null)\n\tconst [editText, setEditText] = useState<TLRichText>(EMPTY_COMMENT)\n\tconst canReply = canComment && !thread.resolved\n\tconst container = useContainer()\n\t// Where focus goes when the edit composer closes, resolved at that moment rather than held as an\n\t// element: the card (and its edit button with it) unmounts while the composer stands in its\n\t// place, so the node captured on the way in is gone by the way out.\n\tconst editReturnFocus = useRef<(() => HTMLElement | null) | null>(null)\n\n\t// Tab from the canvas drops the caret in the reply box. Clicking a pin opens the thread but\n\t// leaves focus on the editor container, so the first Tab would otherwise walk the app's own UI\n\t// instead of the panel the click just opened. Capture phase, ahead of the editor's own handling.\n\t// Fires once per open thread, so Tab inside the thread stays plain tab-through.\n\tconst [focusReply, setFocusReply] = useState(false)\n\tconst tabTaken = useRef(false)\n\tconst swallowTabUp = useRef(false)\n\tuseEffect(() => {\n\t\tif (!canReply) {\n\t\t\t// Resolving unmounts the reply box under an open thread. Reopening it should feel like a\n\t\t\t// freshly opened thread: no autoFocus left armed from the last Tab, and Tab available again.\n\t\t\tsetFocusReply(false)\n\t\t\ttabTaken.current = false\n\t\t\treturn\n\t\t}\n\t\tconst doc = container.ownerDocument\n\t\tconst onKeyDown = (e: KeyboardEvent) => {\n\t\t\tif (e.key !== 'Tab' || e.shiftKey || e.metaKey || e.ctrlKey || e.altKey) return\n\t\t\tif (e.defaultPrevented || tabTaken.current) return\n\t\t\t// Focus rests on the container (or nothing at all) after a pin click. Anywhere else means\n\t\t\t// it's already somewhere deliberate \u2014 the thread's own controls, the sidebar, a panel \u2014\n\t\t\t// and Tab belongs to whatever holds it.\n\t\t\tif (e.target !== container && e.target !== doc.body) return\n\t\t\ttabTaken.current = true\n\t\t\tswallowTabUp.current = true\n\t\t\tsetFocusReply(true)\n\t\t\te.preventDefault()\n\t\t\te.stopPropagation()\n\t\t}\n\t\t// The select tool navigates shapes on Tab's *keyup*, and the composer isn't focused until the\n\t\t// next frame \u2014 so a quick tap with shapes selected would both focus the reply and step the\n\t\t// selection. Swallow the release of the press we took, and only that one.\n\t\tconst onKeyUp = (e: KeyboardEvent) => {\n\t\t\tif (e.key !== 'Tab' || !swallowTabUp.current) return\n\t\t\tswallowTabUp.current = false\n\t\t\te.preventDefault()\n\t\t\te.stopPropagation()\n\t\t}\n\t\tdoc.addEventListener('keydown', onKeyDown, true)\n\t\tdoc.addEventListener('keyup', onKeyUp, true)\n\t\treturn () => {\n\t\t\tdoc.removeEventListener('keydown', onKeyDown, true)\n\t\t\tdoc.removeEventListener('keyup', onKeyUp, true)\n\t\t}\n\t}, [canReply, container])\n\n\t// Leaving the edit composer \u2014 Escape, or a save \u2014 unmounts it while it holds focus, and the\n\t// browser drops focus on the editor container. That's outside the thread, so a Tab from there\n\t// walks the app's UI instead of carrying on from where the edit left off (the thread's one\n\t// Tab-into-the-reply-box has usually been spent by then). Hand focus back to whatever opened the\n\t// composer instead: the card's edit button, or the reply box when the edit came from arrow-up.\n\t// The card's actions row reveals itself on `:focus-within`, so the button focus is visible.\n\tuseEffect(() => {\n\t\tif (editingId !== null) return\n\t\tconst resolve = editReturnFocus.current\n\t\teditReturnFocus.current = null\n\t\t// Runs after React has swapped the card back in, so the target is mounted again by now.\n\t\tresolve?.()?.focus()\n\t}, [editingId])\n\n\t// Every unread comment on display gets reported read \u2014 including replies that arrive while\n\t// the view stays mounted, since the effect re-runs as `comments` changes. The host's receipt\n\t// write flips isCommentUnread to false, so re-runs find nothing to report.\n\tuseEffect(() => {\n\t\tif (!isCommentUnread || !onCommentRead) return\n\t\tfor (const comment of comments) {\n\t\t\tif (isCommentUnread(comment.id)) {\n\t\t\t\tonCommentRead(comment.id)\n\t\t\t}\n\t\t}\n\t}, [comments, isCommentUnread, onCommentRead])\n\n\tconst postReply = () => {\n\t\tif (isCommentEmpty(reply) || !currentUserId) return\n\t\tcommitCommentMutation(editor, () => {\n\t\t\tconst comment = createComment({\n\t\t\t\tthreadId: thread.id,\n\t\t\t\tpageId: thread.pageId,\n\t\t\t\tauthorId: currentUserId,\n\t\t\t\tbody: reply,\n\t\t\t})\n\t\t\tputRecordsInCommit(editor, [comment])\n\t\t\tif (onPostComment) onPostComment(comment)\n\t\t})\n\t\tsetReply(EMPTY_COMMENT)\n\t\tclearCommentDraft(replyDraftSlot(thread.id))\n\t}\n\n\tconst toggleResolve = () => {\n\t\tif (!currentUserId) return\n\t\tif (thread.resolved) reopenThread(editor, thread)\n\t\telse resolveThread(editor, thread, currentUserId)\n\t}\n\n\tconst removeThread = () => {\n\t\tif (!currentUserId) return\n\t\tdeleteThread(editor, thread)\n\t}\n\n\tconst startEdit = (comment: TLComment, { fromMoreMenu = false } = {}) => {\n\t\t// The \u22EF menu's button is the thing to come back to when Edit opened the composer \u2014 looked up\n\t\t// again on the way out, since the menu item that was clicked (and the card) unmount while the\n\t\t// composer stands in their place. Otherwise come back to whatever held focus: arrow-up-to-edit\n\t\t// comes straight from the reply box, which stays put. The document body and the editor\n\t\t// container are where focus falls when nothing holds it, so neither is somewhere to return to.\n\t\tconst active = container.ownerDocument.activeElement\n\t\teditReturnFocus.current = fromMoreMenu\n\t\t\t? () => container.querySelector<HTMLElement>(`[data-cmt-more-for=\"${comment.id}\"]`)\n\t\t\t: active instanceof HTMLElement && active !== container && active !== document.body\n\t\t\t\t? () => (active.isConnected ? active : null)\n\t\t\t\t: null\n\t\tsetEditingId(comment.id)\n\t\tsetEditText(comment.body)\n\t}\n\n\tconst saveEdit = () => {\n\t\tconst comment = comments.find((c) => c.id === editingId)\n\t\tif (!comment || isCommentEmpty(editText)) return\n\t\teditComment(editor, comment, editText)\n\t\tsetEditingId(null)\n\t}\n\n\t// Swap a comment for a pre-filled composer while it's being edited; otherwise show the card,\n\t// with an edit affordance on your own comments.\n\tconst renderComment = (card: CommentCardProps, index: number): ReactNode => {\n\t\tconst comment = comments[index]\n\t\tif (editingId === comment.id) {\n\t\t\treturn (\n\t\t\t\t<div\n\t\t\t\t\tclassName=\"tlui-cmt-editing\"\n\t\t\t\t\tonKeyDown={(e) => {\n\t\t\t\t\t\tif (e.key === 'Escape') {\n\t\t\t\t\t\t\tsetEditingId(null)\n\t\t\t\t\t\t\te.stopPropagation()\n\t\t\t\t\t\t}\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t<CommentComposer\n\t\t\t\t\t\tauthor={card.author}\n\t\t\t\t\t\tplaceholder={msg('comments.edit-placeholder')}\n\t\t\t\t\t\tvalue={editText}\n\t\t\t\t\t\tonChange={setEditText}\n\t\t\t\t\t\tonSubmit={saveEdit}\n\t\t\t\t\t\tsendLabel={msg('comments.save')}\n\t\t\t\t\t\tdisabled={isCommentEmpty(editText)}\n\t\t\t\t\t\tgetMentionSuggestions={getMentionSuggestions}\n\t\t\t\t\t\trenderMentionSuggestion={renderMentionSuggestion}\n\t\t\t\t\t\tautoFocus\n\t\t\t\t\t/>\n\t\t\t\t</div>\n\t\t\t)\n\t\t}\n\t\treturn (\n\t\t\t<CommentCard\n\t\t\t\t{...card}\n\t\t\t\tfooter={\n\t\t\t\t\t<CommentReactions\n\t\t\t\t\t\tcomment={comment}\n\t\t\t\t\t\t// Reacting is a commenting write: without `canComment` the tally renders\n\t\t\t\t\t\t// read-only (no identity \u2192 pills aren't clickable).\n\t\t\t\t\t\tcurrentUserId={canComment ? currentUserId : null}\n\t\t\t\t\t\tresolveName={resolveName}\n\t\t\t\t\t/>\n\t\t\t\t}\n\t\t\t\tactions={\n\t\t\t\t\tcanComment && (\n\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t<CommentReactionPicker comment={comment} currentUserId={currentUserId} />\n\t\t\t\t\t\t\t{comment.authorId === currentUserId && (\n\t\t\t\t\t\t\t\t<TldrawUiDropdownMenuRoot id={`comment-actions-${comment.id}`}>\n\t\t\t\t\t\t\t\t\t<TldrawUiDropdownMenuTrigger>\n\t\t\t\t\t\t\t\t\t\t<TooltipButton\n\t\t\t\t\t\t\t\t\t\t\ttooltip={msg('comments.more-options')}\n\t\t\t\t\t\t\t\t\t\t\tclassName=\"tlui-cmt-thread__action\"\n\t\t\t\t\t\t\t\t\t\t\tdata-cmt-more-for={comment.id}\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t<TldrawUiIcon\n\t\t\t\t\t\t\t\t\t\t\t\ticon=\"dots-vertical\"\n\t\t\t\t\t\t\t\t\t\t\t\tlabel={msg('comments.more-options')}\n\t\t\t\t\t\t\t\t\t\t\t\tsmall\n\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t</TooltipButton>\n\t\t\t\t\t\t\t\t\t</TldrawUiDropdownMenuTrigger>\n\t\t\t\t\t\t\t\t\t<TldrawUiDropdownMenuContent\n\t\t\t\t\t\t\t\t\t\tclassName=\"tlui-cmt-menu\"\n\t\t\t\t\t\t\t\t\t\tside=\"bottom\"\n\t\t\t\t\t\t\t\t\t\talign=\"start\"\n\t\t\t\t\t\t\t\t\t\talignOffset={0}\n\t\t\t\t\t\t\t\t\t\tsideOffset={4}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<TldrawUiDropdownMenuGroup>\n\t\t\t\t\t\t\t\t\t\t\t<TldrawUiDropdownMenuItem>\n\t\t\t\t\t\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"tlui-cmt-menu-item\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tonClick={() => startEdit(comment, { fromMoreMenu: true })}\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<span>{msg('comments.edit')}</span>\n\t\t\t\t\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t\t\t\t\t</TldrawUiDropdownMenuItem>\n\t\t\t\t\t\t\t\t\t\t\t<TldrawUiDropdownMenuItem>\n\t\t\t\t\t\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"tlui-cmt-menu-item tlui-cmt-menu-item--danger\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tonClick={() => deleteComment(editor, comment)}\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<span>{msg('action.delete')}</span>\n\t\t\t\t\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t\t\t\t\t</TldrawUiDropdownMenuItem>\n\t\t\t\t\t\t\t\t\t\t</TldrawUiDropdownMenuGroup>\n\t\t\t\t\t\t\t\t\t</TldrawUiDropdownMenuContent>\n\t\t\t\t\t\t\t\t</TldrawUiDropdownMenuRoot>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t</>\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t/>\n\t\t)\n\t}\n\n\t// Resolve and delete are commenting writes: behind `canComment`, plus the `currentUserId` a\n\t// resolve stamps into `resolved.by`.\n\tconst headerActions = (\n\t\t<>\n\t\t\t{/* Deleting a thread is creator-only (server-enforced), and it's the menu's only item. */}\n\t\t\t{canComment && currentUserId && currentUserId === thread.createdBy && (\n\t\t\t\t<TldrawUiDropdownMenuRoot id={`comment-thread-actions-${thread.id}`}>\n\t\t\t\t\t<TldrawUiDropdownMenuTrigger>\n\t\t\t\t\t\t<TooltipButton\n\t\t\t\t\t\t\ttooltip={msg('comments.more-options')}\n\t\t\t\t\t\t\tclassName=\"tlui-cmt-thread__action\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<TldrawUiIcon icon=\"dots-vertical\" label={msg('comments.more-options')} small />\n\t\t\t\t\t\t</TooltipButton>\n\t\t\t\t\t</TldrawUiDropdownMenuTrigger>\n\t\t\t\t\t<TldrawUiDropdownMenuContent\n\t\t\t\t\t\tclassName=\"tlui-cmt-menu\"\n\t\t\t\t\t\tside=\"bottom\"\n\t\t\t\t\t\talign=\"start\"\n\t\t\t\t\t\talignOffset={0}\n\t\t\t\t\t\tsideOffset={4}\n\t\t\t\t\t>\n\t\t\t\t\t\t<TldrawUiDropdownMenuGroup>\n\t\t\t\t\t\t\t<TldrawUiDropdownMenuItem>\n\t\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\t\tclassName=\"tlui-cmt-menu-item tlui-cmt-menu-item--danger\"\n\t\t\t\t\t\t\t\t\tonClick={removeThread}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<span>{msg('comments.delete')}</span>\n\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t</TldrawUiDropdownMenuItem>\n\t\t\t\t\t\t</TldrawUiDropdownMenuGroup>\n\t\t\t\t\t</TldrawUiDropdownMenuContent>\n\t\t\t\t</TldrawUiDropdownMenuRoot>\n\t\t\t)}\n\t\t\t{canComment && currentUserId && (\n\t\t\t\t<TooltipButton\n\t\t\t\t\ttooltip={msg(thread.resolved ? 'comments.reopen' : 'comments.resolve')}\n\t\t\t\t\tclassName=\"tlui-cmt-thread__action\"\n\t\t\t\t\tonClick={toggleResolve}\n\t\t\t\t>\n\t\t\t\t\t<TldrawUiIcon\n\t\t\t\t\t\ticon=\"check\"\n\t\t\t\t\t\tlabel={msg(thread.resolved ? 'comments.reopen' : 'comments.resolve')}\n\t\t\t\t\t\tsmall\n\t\t\t\t\t/>\n\t\t\t\t</TooltipButton>\n\t\t\t)}\n\t\t\t<TooltipButton\n\t\t\t\ttooltip={msg('comments.dismiss')}\n\t\t\t\tclassName=\"tlui-cmt-thread__action\"\n\t\t\t\tonClick={() => openThreadId.set(editor, null)}\n\t\t\t>\n\t\t\t\t<TldrawUiIcon icon=\"cross-2\" label={msg('comments.dismiss')} small />\n\t\t\t</TooltipButton>\n\t\t</>\n\t)\n\n\treturn (\n\t\t<CommentThread\n\t\t\theader={msg('comments.thread-title')}\n\t\t\theaderActions={headerActions}\n\t\t\trenderComment={renderComment}\n\t\t\tcomments={comments.map((c) => toCardProps(c, props, options.components, resolveName))}\n\t\t\tresolvedBanner={\n\t\t\t\tthread.resolved\n\t\t\t\t\t? msg('comments.resolved-by').replace(\n\t\t\t\t\t\t\t'{name}',\n\t\t\t\t\t\t\tresolveAuthor(thread.resolved.by)?.name ?? UNKNOWN_AUTHOR\n\t\t\t\t\t\t)\n\t\t\t\t\t: undefined\n\t\t\t}\n\t\t\tcomposer={\n\t\t\t\tcanReply\n\t\t\t\t\t? {\n\t\t\t\t\t\t\tauthor: me ?? UNKNOWN_COMMENT_AUTHOR,\n\t\t\t\t\t\t\tplaceholder: msg('comments.reply-placeholder'),\n\t\t\t\t\t\t\tsendLabel: msg('comments.send'),\n\t\t\t\t\t\t\tvalue: reply,\n\t\t\t\t\t\t\tonChange: (value: TLRichText) => {\n\t\t\t\t\t\t\t\tsetReply(value)\n\t\t\t\t\t\t\t\tsaveCommentDraft(replyDraftSlot(thread.id), value)\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tonSubmit: postReply,\n\t\t\t\t\t\t\t// Up in the empty reply box edits the comment directly above it, chat-style \u2014\n\t\t\t\t\t\t\t// only when that comment is yours (the same gate as the Edit link).\n\t\t\t\t\t\t\tonArrowUpWhenEmpty: () => {\n\t\t\t\t\t\t\t\tconst last = comments[comments.length - 1]\n\t\t\t\t\t\t\t\tif (last && last.authorId === currentUserId) startEdit(last)\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t// No user, no author for the record \u2014 dead send button.\n\t\t\t\t\t\t\tdisabled: isCommentEmpty(reply) || !currentUserId,\n\t\t\t\t\t\t\tgetMentionSuggestions,\n\t\t\t\t\t\t\trenderMentionSuggestion,\n\t\t\t\t\t\t\t// Reuses the composer's own focus path, so the caret lands at the end of a\n\t\t\t\t\t\t\t// restored draft rather than in front of it.\n\t\t\t\t\t\t\tautoFocus: focusReply,\n\t\t\t\t\t\t}\n\t\t\t\t\t: undefined\n\t\t\t}\n\t\t\tfooter={\n\t\t\t\t!canComment && !thread.resolved && ComposerFallback ? (\n\t\t\t\t\t<ComposerFallback context=\"thread\" />\n\t\t\t\t) : undefined\n\t\t\t}\n\t\t/>\n\t)\n}\n"],
5
- "mappings": "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",
6
- "names": []
4
+ "sourcesContent": ["import { memo, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport {\n\tcreateComment,\n\tEditor,\n\tEditorPortal,\n\tTLComment,\n\tTLCommentThread,\n\tTLRichText,\n\tTldrawUiButton,\n\tTldrawUiDropdownMenuContent,\n\tTldrawUiDropdownMenuGroup,\n\tTldrawUiDropdownMenuItem,\n\tTldrawUiDropdownMenuRoot,\n\tTldrawUiDropdownMenuTrigger,\n\tTldrawUiIcon,\n\tuseContainer,\n\tusePassThroughWheelEvents,\n\tuseTranslation,\n\tuseValue,\n} from 'tldraw'\nimport { CommentCard, CommentCardProps } from '../ui/comment-card'\nimport { CommentComposer } from '../ui/comment-composer'\nimport { EMPTY_COMMENT, isCommentEmpty } from '../ui/comment-extensions'\nimport { CommentThread } from '../ui/comment-thread'\nimport { CommentBody } from './comment-body'\nimport {\n\tclearCommentDraft,\n\tgetCommentDraft,\n\treplyDraftSlot,\n\tsaveCommentDraft,\n} from './comment-drafts'\nimport {\n\tcommitCommentMutation,\n\tdeleteComment,\n\tdeleteThread,\n\teditComment,\n\treopenThread,\n\tresolveThread,\n} from './comment-mutations'\nimport { CommentReactionPicker, CommentReactions } from './comment-reactions'\nimport { UNKNOWN_AUTHOR, UNKNOWN_COMMENT_AUTHOR } from './comment-render'\nimport { type CommentingContext } from './context'\nimport { useThreadComments } from './hooks'\nimport { useIsMobileCommenting, useMobilePlacement } from './mobile-placement'\nimport {\n\ttype CommentingComponents,\n\tgetCanModifyComment,\n\tuseCanComment,\n\tuseCanModifyComment,\n\tuseCommentingOptions,\n} from './options'\nimport { openThreadId } from './state'\n\nconst stop = (e: { stopPropagation(): void }) => e.stopPropagation()\n\n/**\n * A name-only view of an author resolver, for the mention/rich-text paths. Stable identity, so\n * `CommentBody`'s memoized render doesn't recompute on every render of its host.\n */\nexport function useResolveName(resolveAuthor: CommentingContext['resolveAuthor']) {\n\treturn useCallback((id: string) => resolveAuthor(id)?.name, [resolveAuthor])\n}\n\n/** How long the copy-link item reads \"Link copied\" before reverting. */\nconst LINK_COPIED_MS = 2000\n\n/**\n * What a thread's `getThreadHref` should put on the clipboard. Hosts may hand back a relative href,\n * which is meaningless once pasted elsewhere, so it's resolved against the current document first.\n * An href the URL parser can't make sense of is copied as-is rather than dropped.\n *\n * @internal\n */\nexport function absoluteThreadLink(href: string, base = window.location.href): string {\n\ttry {\n\t\treturn new URL(href, base).toString()\n\t} catch {\n\t\treturn href\n\t}\n}\n\n/**\n * Copy a thread's link, with a flag that stays set briefly afterwards so the control can confirm.\n * Only set once the write resolves, so a withheld clipboard doesn't claim a copy that didn't happen.\n */\nfunction useCopyLink(href: string | undefined) {\n\tconst [copied, setCopied] = useState(false)\n\tuseEffect(() => {\n\t\tif (!copied) return\n\t\tconst timeout = window.setTimeout(() => setCopied(false), LINK_COPIED_MS)\n\t\treturn () => window.clearTimeout(timeout)\n\t}, [copied])\n\tconst copy = useCallback(() => {\n\t\tif (href === undefined) return\n\t\tnavigator.clipboard?.writeText(absoluteThreadLink(href)).then(\n\t\t\t() => setCopied(true),\n\t\t\t() => setCopied(false)\n\t\t)\n\t}, [href])\n\treturn [copied, copy] as const\n}\n\nexport function toCardProps(\n\tcomment: TLComment,\n\tprops: Pick<CommentingContext, 'currentUserId' | 'resolveAuthor'>,\n\tcomponents: CommentingComponents,\n\tresolveName: (id: string) => string | undefined\n): CommentCardProps {\n\tconst Body = components.CommentBody\n\t// The `CommentBody` component slot overrides the built-in rich-text default (which resolves\n\t// mention ids to names).\n\tconst body = Body ? (\n\t\t<Body comment={comment} />\n\t) : (\n\t\t<CommentBody richText={comment.body} resolveName={resolveName} />\n\t)\n\treturn {\n\t\tauthor: props.resolveAuthor(comment.authorId) ?? UNKNOWN_COMMENT_AUTHOR,\n\t\tbody,\n\t\tdate: new Date(comment.createdAt).toISOString(),\n\t\tyou: comment.authorId === props.currentUserId,\n\t\tedited: comment.editedAt != null,\n\t}\n}\n\n/** A pin is this square (mirrors `--tlui-cmt-pin-size`). The two marker kinds differ in size *and*\n * anchor point, so lining their previews up means correcting for both. Keep in sync with the CSS. */\nconst PIN_SIZE = 28\n\n/** A cluster/stack badge is this square (`--tlui-cmt-marker-size`) \u2014 pin-sized, so the marker\n * kinds read as one family. Keep in sync with the stylesheet. */\nconst MARKER_SIZE = PIN_SIZE\n\n/** Where a popover's top card sits vertically, measured from the marker visual's middle. */\nconst CARD_TOP_Y = -28\n\n/** The horizontal gap between a marker visual's right edge and its popover/preview panel. The\n * preview's hover bridge spans exactly this (see `--tlui-cmt-preview-bridge`). */\nconst PREVIEW_GAP = 6\n\n/**\n * Where a marker's popover sits relative to the marker's anchor point. The hover preview uses the\n * same origins, so moving a popover here moves its preview with it.\n *\n * Both marker kinds hang off their point the same way (`translate(0, -100%)`), differing only in\n * size \u2014 so each offset clears its own width plus the shared gap, and re-bases the shared card-top\n * measure from its bottom anchor to its middle.\n */\nexport const POPOVER_OFFSET = {\n\tthread: { x: PIN_SIZE + PREVIEW_GAP, y: CARD_TOP_Y - PIN_SIZE / 2 },\n\tlist: { x: MARKER_SIZE + PREVIEW_GAP, y: CARD_TOP_Y - MARKER_SIZE / 2 },\n} as const\n\n/** The open thread's popover container, portaled above the UI panels. A wheel over it passes\n * through to the canvas (unless it scrolls its own content), like tldraw's panels. */\nexport function ThreadPopover({\n\tbase,\n\tchildren,\n}: {\n\tbase: { x: number; y: number }\n\tchildren: ReactNode\n}) {\n\tconst ref = useRef<HTMLDivElement>(null)\n\tusePassThroughWheelEvents(ref)\n\t// On mobile the popover slides off its fixed offset to stay above the software keyboard and\n\t// on-screen; desktop keeps the fixed offset (base returned unchanged).\n\tconst isMobile = useIsMobileCommenting()\n\tconst placed = useMobilePlacement(ref, base, isMobile)\n\treturn (\n\t\t<EditorPortal>\n\t\t\t{/* contextmenu also stops here: portals bubble React events to the canvas's context-menu\n\t\t\t trigger (the layer mounts inside it), which would open the canvas menu over this panel. */}\n\t\t\t<div\n\t\t\t\tref={ref}\n\t\t\t\tclassName=\"tlui-cmt-canvas-popover\"\n\t\t\t\tstyle={{ left: placed.left, top: placed.top }}\n\t\t\t\tonPointerDown={stop}\n\t\t\t\tonContextMenu={stop}\n\t\t\t>\n\t\t\t\t{children}\n\t\t\t</div>\n\t\t</EditorPortal>\n\t)\n}\n\n/**\n * One thread's interactive view: its comments, the reply composer, edit-in-place on your own\n * comments, and the resolve/delete actions. Read receipts are reported for every unread comment\n * while mounted, so only mount it where the thread is actually being shown.\n *\n * Memoized because the popover position rides the pin's per-frame render point, so the pin\n * re-renders on every camera frame while a thread is open.\n */\nexport const ThreadView = memo(function ThreadView({\n\teditor,\n\tthread,\n\t...props\n}: CommentingContext & { editor: Editor; thread: TLCommentThread }) {\n\tconst {\n\t\tcurrentUserId,\n\t\tresolveAuthor,\n\t\tonPostComment,\n\t\tisCommentUnread,\n\t\tonCommentsRead,\n\t\tgetMentionSuggestions,\n\t\trenderMentionSuggestion,\n\t\tgetThreadHref,\n\t} = props\n\tconst options = useCommentingOptions()\n\tconst comments = useThreadComments(editor, thread.id)\n\tconst msg = useTranslation()\n\tconst resolveName = useResolveName(resolveAuthor)\n\t// Rebuilt only when the comments change, not on every render \u2014 each of which would otherwise\n\t// re-allocate a date string and body element per comment.\n\tconst cards = useMemo(\n\t\t() =>\n\t\t\tcomments.map((c) =>\n\t\t\t\ttoCardProps(c, { currentUserId, resolveAuthor }, options.components, resolveName)\n\t\t\t),\n\t\t[comments, currentUserId, resolveAuthor, options.components, resolveName]\n\t)\n\tconst me = currentUserId ? resolveAuthor(currentUserId) : undefined\n\t// Composing, editing, deleting, and resolving are all gated on the viewer's permission. Where it's\n\t// withheld the composer gives way to the ComposerFallback slot and the affordances are hidden.\n\tconst canComment = useCanComment(currentUserId)\n\t// Editing and deleting are further per-record: the author's to do by default, wider or narrower\n\t// under a `canModifyComment` callback. Computed for the thread's comments in one pass, since\n\t// `renderComment` is a callback rather than a component and can't call a hook per comment.\n\tconst commentPermissions = useValue(\n\t\t'comment permissions',\n\t\t() =>\n\t\t\tnew Map(\n\t\t\t\tcomments.map((comment) => [\n\t\t\t\t\tcomment.id,\n\t\t\t\t\t{\n\t\t\t\t\t\tedit: getCanModifyComment(editor, currentUserId, { action: 'edit-comment', comment }),\n\t\t\t\t\t\tdelete: getCanModifyComment(editor, currentUserId, {\n\t\t\t\t\t\t\taction: 'delete-comment',\n\t\t\t\t\t\t\tcomment,\n\t\t\t\t\t\t}),\n\t\t\t\t\t},\n\t\t\t\t])\n\t\t\t),\n\t\t[editor, currentUserId, comments]\n\t)\n\t// Deleting a thread is its creator's by default (and server-enforced); `canModifyComment` is what\n\t// widens that. Still a commenting write, so `canComment` stays the outer gate.\n\tconst canModifyThread = useCanModifyComment(currentUserId, { action: 'delete-thread', thread })\n\tconst canDeleteThread = canComment && canModifyThread\n\tconst ComposerFallback = options.components.ComposerFallback\n\t// An unsent reply survives closing the thread (saved on every change, keyed by thread id) \u2014\n\t// the flip side of dismissing without a discard warning.\n\tconst [reply, setReply] = useState<TLRichText>(\n\t\t() => getCommentDraft(replyDraftSlot(thread.id)) ?? EMPTY_COMMENT\n\t)\n\tconst [editingId, setEditingId] = useState<string | null>(null)\n\tconst [editText, setEditText] = useState<TLRichText>(EMPTY_COMMENT)\n\tconst canReply = canComment && !thread.resolved\n\tconst container = useContainer()\n\t// Resolved when the composer closes rather than held as an element: the card unmounts while the\n\t// composer stands in its place, so the node captured on the way in is gone by the way out.\n\tconst editReturnFocus = useRef<(() => HTMLElement | null) | null>(null)\n\n\t// Tab from the canvas drops the caret in the reply box: a pin click leaves focus on the editor\n\t// container, so the first Tab would otherwise walk the app's UI. Capture phase, once per open thread.\n\tconst [focusReply, setFocusReply] = useState(false)\n\tconst tabTaken = useRef(false)\n\tconst swallowTabUp = useRef(false)\n\tuseEffect(() => {\n\t\tif (!canReply) {\n\t\t\t// Resolving unmounts the reply box under an open thread. Reopening it should feel like a\n\t\t\t// freshly opened thread: no autoFocus left armed from the last Tab, and Tab available again.\n\t\t\tsetFocusReply(false)\n\t\t\ttabTaken.current = false\n\t\t\treturn\n\t\t}\n\t\tconst doc = container.ownerDocument\n\t\tconst onKeyDown = (e: KeyboardEvent) => {\n\t\t\tif (e.key !== 'Tab' || e.shiftKey || e.metaKey || e.ctrlKey || e.altKey) return\n\t\t\tif (e.defaultPrevented || tabTaken.current) return\n\t\t\t// Focus rests on the container (or nothing) after a pin click. Anywhere else is deliberate, and\n\t\t\t// Tab belongs to whatever holds it.\n\t\t\tif (e.target !== container && e.target !== doc.body) return\n\t\t\ttabTaken.current = true\n\t\t\tswallowTabUp.current = true\n\t\t\tsetFocusReply(true)\n\t\t\te.preventDefault()\n\t\t\te.stopPropagation()\n\t\t}\n\t\t// The select tool navigates shapes on Tab's *keyup*, so a quick tap would both focus the reply and\n\t\t// step the selection. Swallow the release of the press we took, and only that one.\n\t\tconst onKeyUp = (e: KeyboardEvent) => {\n\t\t\tif (e.key !== 'Tab' || !swallowTabUp.current) return\n\t\t\tswallowTabUp.current = false\n\t\t\te.preventDefault()\n\t\t\te.stopPropagation()\n\t\t}\n\t\tdoc.addEventListener('keydown', onKeyDown, true)\n\t\tdoc.addEventListener('keyup', onKeyUp, true)\n\t\treturn () => {\n\t\t\tdoc.removeEventListener('keydown', onKeyDown, true)\n\t\t\tdoc.removeEventListener('keyup', onKeyUp, true)\n\t\t}\n\t}, [canReply, container])\n\n\t// Leaving the edit composer unmounts it while it holds focus, dropping focus to the editor\n\t// container \u2014 outside the thread, so a Tab from there walks the app's UI. Hand focus back to\n\t// whatever opened the composer instead.\n\tuseEffect(() => {\n\t\tif (editingId !== null) return\n\t\tconst resolve = editReturnFocus.current\n\t\teditReturnFocus.current = null\n\t\t// Runs after React has swapped the card back in, so the target is mounted again by now.\n\t\tresolve?.()?.focus()\n\t}, [editingId])\n\n\t// Reported as one batch so the host can record the receipts in a single write. The write flips\n\t// isCommentUnread to false, so re-runs find nothing to report.\n\tuseEffect(() => {\n\t\tif (!isCommentUnread || !onCommentsRead) return\n\t\tconst unreadIds = comments.filter((comment) => isCommentUnread(comment.id)).map((c) => c.id)\n\t\tif (unreadIds.length > 0) {\n\t\t\tonCommentsRead(unreadIds)\n\t\t}\n\t}, [comments, isCommentUnread, onCommentsRead])\n\n\tconst postReply = () => {\n\t\tif (isCommentEmpty(reply) || !currentUserId) return\n\t\tconst comment = commitCommentMutation(editor, ({ put }) => {\n\t\t\tconst comment = createComment({\n\t\t\t\tthreadId: thread.id,\n\t\t\t\tpageId: thread.pageId,\n\t\t\t\tauthorId: currentUserId,\n\t\t\t\tbody: reply,\n\t\t\t})\n\t\t\tput([comment])\n\t\t\treturn comment\n\t\t})\n\t\tsetReply(EMPTY_COMMENT)\n\t\tclearCommentDraft(replyDraftSlot(thread.id))\n\t\t// The host's callback is its own operation, not part of the post's history scope. It runs\n\t\t// last so a throwing host can't strand the composer holding a draft of a posted reply.\n\t\tonPostComment?.(comment)\n\t}\n\n\tconst toggleResolve = () => {\n\t\tif (!currentUserId) return\n\t\tif (thread.resolved) reopenThread(editor, thread)\n\t\telse resolveThread(editor, thread, currentUserId)\n\t}\n\n\t// No `currentUserId` check: unlike a resolve, a delete stamps nothing on the record, so who may\n\t// make it is `canModifyComment`'s call alone (which withholds it from an unidentified viewer by\n\t// default).\n\tconst removeThread = () => {\n\t\tdeleteThread(editor, thread)\n\t}\n\n\tconst ThreadActions = options.components.ThreadActions\n\t// The host's URL for this thread. Its presence is what puts \"copy link\" in the header menu.\n\tconst threadHref = getThreadHref?.(thread.id)\n\tconst [linkCopied, copyThreadLink] = useCopyLink(threadHref)\n\n\tconst startEdit = (comment: TLComment, { fromMoreMenu = false } = {}) => {\n\t\t// Edit from the \u22EF menu comes back to that button, looked up again on the way out since the card\n\t\t// unmounts. Otherwise return to whatever held focus, ignoring the body and the editor container.\n\t\tconst active = container.ownerDocument.activeElement\n\t\teditReturnFocus.current = fromMoreMenu\n\t\t\t? () => container.querySelector<HTMLElement>(`[data-cmt-more-for=\"${comment.id}\"]`)\n\t\t\t: active instanceof HTMLElement && active !== container && active !== document.body\n\t\t\t\t? () => (active.isConnected ? active : null)\n\t\t\t\t: null\n\t\tsetEditingId(comment.id)\n\t\tsetEditText(comment.body)\n\t}\n\n\tconst saveEdit = () => {\n\t\tconst comment = comments.find((c) => c.id === editingId)\n\t\tif (!comment || isCommentEmpty(editText)) return\n\t\teditComment(editor, comment, editText)\n\t\tsetEditingId(null)\n\t}\n\n\t// Swap a comment for a pre-filled composer while it's being edited; otherwise show the card,\n\t// with the affordances the viewer is allowed on it.\n\tconst renderComment = (card: CommentCardProps, index: number): ReactNode => {\n\t\tconst comment = comments[index]\n\t\tconst permissions = commentPermissions.get(comment.id)\n\t\tif (editingId === comment.id) {\n\t\t\treturn (\n\t\t\t\t<div\n\t\t\t\t\tclassName=\"tlui-cmt-editing\"\n\t\t\t\t\tonKeyDown={(e) => {\n\t\t\t\t\t\tif (e.key === 'Escape') {\n\t\t\t\t\t\t\tsetEditingId(null)\n\t\t\t\t\t\t\te.stopPropagation()\n\t\t\t\t\t\t}\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t<CommentComposer\n\t\t\t\t\t\tauthor={card.author}\n\t\t\t\t\t\tplaceholder={msg('comments.edit-placeholder')}\n\t\t\t\t\t\tvalue={editText}\n\t\t\t\t\t\tonChange={setEditText}\n\t\t\t\t\t\tonSubmit={saveEdit}\n\t\t\t\t\t\tsendLabel={msg('comments.save')}\n\t\t\t\t\t\tdisabled={isCommentEmpty(editText)}\n\t\t\t\t\t\tgetMentionSuggestions={getMentionSuggestions}\n\t\t\t\t\t\trenderMentionSuggestion={renderMentionSuggestion}\n\t\t\t\t\t\tautoFocus\n\t\t\t\t\t/>\n\t\t\t\t</div>\n\t\t\t)\n\t\t}\n\t\treturn (\n\t\t\t<CommentCard\n\t\t\t\t{...card}\n\t\t\t\tfooter={\n\t\t\t\t\t<CommentReactions\n\t\t\t\t\t\tcomment={comment}\n\t\t\t\t\t\t// Reacting is a commenting write: without `canComment` the tally renders\n\t\t\t\t\t\t// read-only (no identity \u2192 pills aren't clickable).\n\t\t\t\t\t\tcurrentUserId={canComment ? currentUserId : null}\n\t\t\t\t\t\tresolveName={resolveName}\n\t\t\t\t\t/>\n\t\t\t\t}\n\t\t\t\tactions={\n\t\t\t\t\tcanComment && (\n\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t{(permissions?.edit || permissions?.delete) && (\n\t\t\t\t\t\t\t\t<TldrawUiDropdownMenuRoot id={`comment-actions-${comment.id}`}>\n\t\t\t\t\t\t\t\t\t<TldrawUiDropdownMenuTrigger>\n\t\t\t\t\t\t\t\t\t\t<TldrawUiButton\n\t\t\t\t\t\t\t\t\t\t\ttype=\"icon\"\n\t\t\t\t\t\t\t\t\t\t\ttooltip={msg('comments.more-options')}\n\t\t\t\t\t\t\t\t\t\t\ttitle={msg('comments.more-options')}\n\t\t\t\t\t\t\t\t\t\t\tclassName=\"tlui-cmt-thread__action\"\n\t\t\t\t\t\t\t\t\t\t\tdata-cmt-more-for={comment.id}\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t<TldrawUiIcon\n\t\t\t\t\t\t\t\t\t\t\t\ticon=\"dots-vertical\"\n\t\t\t\t\t\t\t\t\t\t\t\tlabel={msg('comments.more-options')}\n\t\t\t\t\t\t\t\t\t\t\t\tsmall\n\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t</TldrawUiButton>\n\t\t\t\t\t\t\t\t\t</TldrawUiDropdownMenuTrigger>\n\t\t\t\t\t\t\t\t\t<TldrawUiDropdownMenuContent\n\t\t\t\t\t\t\t\t\t\tclassName=\"tlui-cmt-menu\"\n\t\t\t\t\t\t\t\t\t\tside=\"bottom\"\n\t\t\t\t\t\t\t\t\t\talign=\"start\"\n\t\t\t\t\t\t\t\t\t\talignOffset={0}\n\t\t\t\t\t\t\t\t\t\tsideOffset={4}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<TldrawUiDropdownMenuGroup>\n\t\t\t\t\t\t\t\t\t\t\t{permissions?.edit && (\n\t\t\t\t\t\t\t\t\t\t\t\t<TldrawUiDropdownMenuItem>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"tlui-cmt-menu-item\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tonClick={() => startEdit(comment, { fromMoreMenu: true })}\n\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span>{msg('comments.edit')}</span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t\t\t\t\t\t</TldrawUiDropdownMenuItem>\n\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t{permissions?.delete && (\n\t\t\t\t\t\t\t\t\t\t\t\t<TldrawUiDropdownMenuItem>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"tlui-cmt-menu-item tlui-cmt-menu-item--danger\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tonClick={() => deleteComment(editor, comment)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span>{msg('action.delete')}</span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t\t\t\t\t\t</TldrawUiDropdownMenuItem>\n\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t</TldrawUiDropdownMenuGroup>\n\t\t\t\t\t\t\t\t\t</TldrawUiDropdownMenuContent>\n\t\t\t\t\t\t\t\t</TldrawUiDropdownMenuRoot>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t{/* Last so react is always the rightmost pill. */}\n\t\t\t\t\t\t\t<CommentReactionPicker comment={comment} currentUserId={currentUserId} />\n\t\t\t\t\t\t</>\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t/>\n\t\t)\n\t}\n\n\t// Resolve and delete are commenting writes: behind `canComment`, plus the `currentUserId` a\n\t// resolve stamps into `resolved.by`.\n\tconst resolveLabel = msg(thread.resolved ? 'comments.reopen' : 'comments.resolve')\n\tconst headerActions = (\n\t\t<>\n\t\t\t{/* Host verbs \u2014 assign, link a ticket \u2014 sit ahead of the built-in actions. */}\n\t\t\t{ThreadActions && <ThreadActions thread={thread} comments={comments} />}\n\t\t\t{(threadHref !== undefined || canDeleteThread) && (\n\t\t\t\t<TldrawUiDropdownMenuRoot id={`comment-thread-actions-${thread.id}`}>\n\t\t\t\t\t<TldrawUiDropdownMenuTrigger>\n\t\t\t\t\t\t<TldrawUiButton\n\t\t\t\t\t\t\ttype=\"icon\"\n\t\t\t\t\t\t\ttooltip={msg('comments.more-options')}\n\t\t\t\t\t\t\ttitle={msg('comments.more-options')}\n\t\t\t\t\t\t\tclassName=\"tlui-cmt-thread__action\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<TldrawUiIcon icon=\"dots-vertical\" label={msg('comments.more-options')} small />\n\t\t\t\t\t\t</TldrawUiButton>\n\t\t\t\t\t</TldrawUiDropdownMenuTrigger>\n\t\t\t\t\t<TldrawUiDropdownMenuContent\n\t\t\t\t\t\tclassName=\"tlui-cmt-menu\"\n\t\t\t\t\t\tside=\"bottom\"\n\t\t\t\t\t\talign=\"start\"\n\t\t\t\t\t\talignOffset={0}\n\t\t\t\t\t\tsideOffset={4}\n\t\t\t\t\t>\n\t\t\t\t\t\t<TldrawUiDropdownMenuGroup>\n\t\t\t\t\t\t\t{/* A link is a read affordance: offered to anyone who can see the thread,\n\t\t\t\t\t\t\t including a viewer who can't comment. `noClose` keeps the menu up so the\n\t\t\t\t\t\t\t item can confirm the copy in place. */}\n\t\t\t\t\t\t\t{threadHref !== undefined && (\n\t\t\t\t\t\t\t\t<TldrawUiDropdownMenuItem noClose>\n\t\t\t\t\t\t\t\t\t<button type=\"button\" className=\"tlui-cmt-menu-item\" onClick={copyThreadLink}>\n\t\t\t\t\t\t\t\t\t\t<span>{msg(linkCopied ? 'comments.link-copied' : 'comments.copy-link')}</span>\n\t\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t\t</TldrawUiDropdownMenuItem>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t{canDeleteThread && (\n\t\t\t\t\t\t\t\t<TldrawUiDropdownMenuItem>\n\t\t\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\t\t\tclassName=\"tlui-cmt-menu-item tlui-cmt-menu-item--danger\"\n\t\t\t\t\t\t\t\t\t\tonClick={removeThread}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<span>{msg('comments.delete')}</span>\n\t\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t\t</TldrawUiDropdownMenuItem>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t</TldrawUiDropdownMenuGroup>\n\t\t\t\t\t</TldrawUiDropdownMenuContent>\n\t\t\t\t</TldrawUiDropdownMenuRoot>\n\t\t\t)}\n\t\t\t{canComment && currentUserId && (\n\t\t\t\t<TldrawUiButton\n\t\t\t\t\ttype=\"icon\"\n\t\t\t\t\ttooltip={resolveLabel}\n\t\t\t\t\ttitle={resolveLabel}\n\t\t\t\t\tclassName=\"tlui-cmt-thread__action\"\n\t\t\t\t\tonClick={toggleResolve}\n\t\t\t\t>\n\t\t\t\t\t<TldrawUiIcon icon=\"check\" label={resolveLabel} small />\n\t\t\t\t</TldrawUiButton>\n\t\t\t)}\n\t\t\t<TldrawUiButton\n\t\t\t\ttype=\"icon\"\n\t\t\t\ttooltip={msg('comments.dismiss')}\n\t\t\t\ttitle={msg('comments.dismiss')}\n\t\t\t\tclassName=\"tlui-cmt-thread__action\"\n\t\t\t\tonClick={() => openThreadId.set(editor, null)}\n\t\t\t>\n\t\t\t\t<TldrawUiIcon icon=\"cross-2\" label={msg('comments.dismiss')} small />\n\t\t\t</TldrawUiButton>\n\t\t</>\n\t)\n\n\treturn (\n\t\t<CommentThread\n\t\t\theader={msg('comments.thread-title')}\n\t\t\theaderActions={headerActions}\n\t\t\trenderComment={renderComment}\n\t\t\tcomments={cards}\n\t\t\tresolvedBanner={\n\t\t\t\tthread.resolved\n\t\t\t\t\t? msg('comments.resolved-by').replace(\n\t\t\t\t\t\t\t'{name}',\n\t\t\t\t\t\t\tresolveAuthor(thread.resolved.by)?.name ?? UNKNOWN_AUTHOR\n\t\t\t\t\t\t)\n\t\t\t\t\t: undefined\n\t\t\t}\n\t\t\tcomposer={\n\t\t\t\tcanReply\n\t\t\t\t\t? {\n\t\t\t\t\t\t\tauthor: me ?? UNKNOWN_COMMENT_AUTHOR,\n\t\t\t\t\t\t\tplaceholder: msg('comments.reply-placeholder'),\n\t\t\t\t\t\t\tsendLabel: msg('comments.send'),\n\t\t\t\t\t\t\tvalue: reply,\n\t\t\t\t\t\t\tonChange: (value: TLRichText) => {\n\t\t\t\t\t\t\t\tsetReply(value)\n\t\t\t\t\t\t\t\tsaveCommentDraft(replyDraftSlot(thread.id), value)\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tonSubmit: postReply,\n\t\t\t\t\t\t\t// Up in the empty reply box edits the comment directly above it, chat-style \u2014\n\t\t\t\t\t\t\t// only when you're allowed to edit it (the same gate as the Edit link).\n\t\t\t\t\t\t\tonArrowUpWhenEmpty: () => {\n\t\t\t\t\t\t\t\tconst last = comments[comments.length - 1]\n\t\t\t\t\t\t\t\tif (last && commentPermissions.get(last.id)?.edit) startEdit(last)\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t// No user, no author for the record \u2014 dead send button.\n\t\t\t\t\t\t\tdisabled: isCommentEmpty(reply) || !currentUserId,\n\t\t\t\t\t\t\tgetMentionSuggestions,\n\t\t\t\t\t\t\trenderMentionSuggestion,\n\t\t\t\t\t\t\t// Reuses the composer's own focus path, so the caret lands at the end of a\n\t\t\t\t\t\t\t// restored draft rather than in front of it.\n\t\t\t\t\t\t\tautoFocus: focusReply,\n\t\t\t\t\t\t}\n\t\t\t\t\t: undefined\n\t\t\t}\n\t\t\tfooter={\n\t\t\t\t!canComment && !thread.resolved && ComposerFallback ? (\n\t\t\t\t\t<ComposerFallback context=\"thread\" />\n\t\t\t\t) : undefined\n\t\t\t}\n\t\t/>\n\t)\n})\n"],
5
+ "mappings": "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",
6
+ "names": ["ThreadView", "comment"]
7
7
  }
@@ -1,14 +1,14 @@
1
1
  import { mstEdges } from "./mst.mjs";
2
2
  import { cappedReplay } from "./replay.mjs";
3
3
  import { contract, finalize } from "./schedule.mjs";
4
- function computeClusterTable(leaves, options) {
4
+ function computeClusterTable(leaves, options, screenOffsets) {
5
5
  const opts = resolveOptions(options);
6
6
  const leafNodes = leaves.map(leafToNode);
7
7
  const edges = mstEdges(leaves);
8
8
  if (leaves.length < 2) {
9
9
  return { events: [], leaves: leafNodes };
10
10
  }
11
- const raw = cappedReplay(leaves, edges, { Tc: opts.Tc, Dmax: opts.Dmax });
11
+ const raw = cappedReplay(leaves, edges, { Tc: opts.Tc, Dmax: opts.Dmax }, screenOffsets);
12
12
  const contracted = contract(raw, opts.eps);
13
13
  const events = finalize(contracted, {
14
14
  Tc: opts.Tc,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/clustering/computeClusterTable.ts"],
4
- "sourcesContent": ["import { mstEdges } from './mst'\nimport { cappedReplay } from './replay'\nimport { contract, finalize } from './schedule'\nimport type { ClusterNode, ClusterOptions, ClusterTable, LeafInput } from './types'\n\ninterface ResolvedClusterOptions {\n\tTc: number\n\tTu: number\n\teps: number\n\tDmax: number\n\tminZoom: number\n\tmaxZoom: number\n\tmaxSplitZoom: number\n}\n\n/** @internal */\nexport function computeClusterTable(\n\tleaves: readonly LeafInput[],\n\toptions: ClusterOptions\n): ClusterTable {\n\tconst opts = resolveOptions(options)\n\tconst leafNodes = leaves.map(leafToNode)\n\tconst edges = mstEdges(leaves)\n\n\tif (leaves.length < 2) {\n\t\treturn { events: [], leaves: leafNodes }\n\t}\n\n\tconst raw = cappedReplay(leaves, edges, { Tc: opts.Tc, Dmax: opts.Dmax })\n\tconst contracted = contract(raw, opts.eps)\n\tconst events = finalize(contracted, {\n\t\tTc: opts.Tc,\n\t\tTu: opts.Tu,\n\t\tminZoom: opts.minZoom,\n\t\tmaxZoom: opts.maxZoom,\n\t\tmaxSplitZoom: opts.maxSplitZoom,\n\t})\n\n\treturn { events, leaves: leafNodes }\n}\n\nfunction resolveOptions(options: ClusterOptions): ResolvedClusterOptions {\n\tconst Tc = options.Tc ?? 22\n\tconst Tu = options.Tu ?? 1.2 * Tc\n\tconst eps = options.eps ?? 0.7\n\tconst Dmax = options.Dmax ?? 3.75 * Tc\n\tconst maxSplitZoom = options.maxSplitZoom ?? 6\n\tconst { minZoom, maxZoom } = options\n\n\tif (!Number.isFinite(Tc) || Tc <= 0) {\n\t\tthrow new Error('Tc must be finite and greater than 0')\n\t}\n\tif (!Number.isFinite(Tu) || Tu <= Tc) {\n\t\tthrow new Error('Tu must be finite and greater than Tc')\n\t}\n\tif (!Number.isFinite(eps) || eps < 0) {\n\t\tthrow new Error('eps must be finite and greater than or equal to 0')\n\t}\n\tif (!Number.isFinite(Dmax) || Dmax < Tc) {\n\t\tthrow new Error('Dmax must be finite and greater than or equal to Tc')\n\t}\n\tif (!Number.isFinite(minZoom) || minZoom <= 0) {\n\t\tthrow new Error('minZoom must be finite and greater than 0')\n\t}\n\tif (!Number.isFinite(maxZoom) || maxZoom <= minZoom) {\n\t\tthrow new Error('maxZoom must be finite and greater than minZoom')\n\t}\n\tif (!Number.isFinite(maxSplitZoom) || maxSplitZoom <= 0) {\n\t\tthrow new Error('maxSplitZoom must be finite and greater than 0')\n\t}\n\n\treturn { Tc, Tu, eps, Dmax, minZoom, maxZoom, maxSplitZoom }\n}\n\nfunction leafToNode(leaf: LeafInput): ClusterNode {\n\treturn {\n\t\tid: leaf.id,\n\t\tcentroid: { x: leaf.point.x, y: leaf.point.y },\n\t\tcount: 1,\n\t\tmembers: [leaf.id],\n\t}\n}\n"],
5
- "mappings": "AAAA,SAAS,gBAAgB;AACzB,SAAS,oBAAoB;AAC7B,SAAS,UAAU,gBAAgB;AAc5B,SAAS,oBACf,QACA,SACe;AACf,QAAM,OAAO,eAAe,OAAO;AACnC,QAAM,YAAY,OAAO,IAAI,UAAU;AACvC,QAAM,QAAQ,SAAS,MAAM;AAE7B,MAAI,OAAO,SAAS,GAAG;AACtB,WAAO,EAAE,QAAQ,CAAC,GAAG,QAAQ,UAAU;AAAA,EACxC;AAEA,QAAM,MAAM,aAAa,QAAQ,OAAO,EAAE,IAAI,KAAK,IAAI,MAAM,KAAK,KAAK,CAAC;AACxE,QAAM,aAAa,SAAS,KAAK,KAAK,GAAG;AACzC,QAAM,SAAS,SAAS,YAAY;AAAA,IACnC,IAAI,KAAK;AAAA,IACT,IAAI,KAAK;AAAA,IACT,SAAS,KAAK;AAAA,IACd,SAAS,KAAK;AAAA,IACd,cAAc,KAAK;AAAA,EACpB,CAAC;AAED,SAAO,EAAE,QAAQ,QAAQ,UAAU;AACpC;AAEA,SAAS,eAAe,SAAiD;AACxE,QAAM,KAAK,QAAQ,MAAM;AACzB,QAAM,KAAK,QAAQ,MAAM,MAAM;AAC/B,QAAM,MAAM,QAAQ,OAAO;AAC3B,QAAM,OAAO,QAAQ,QAAQ,OAAO;AACpC,QAAM,eAAe,QAAQ,gBAAgB;AAC7C,QAAM,EAAE,SAAS,QAAQ,IAAI;AAE7B,MAAI,CAAC,OAAO,SAAS,EAAE,KAAK,MAAM,GAAG;AACpC,UAAM,IAAI,MAAM,sCAAsC;AAAA,EACvD;AACA,MAAI,CAAC,OAAO,SAAS,EAAE,KAAK,MAAM,IAAI;AACrC,UAAM,IAAI,MAAM,uCAAuC;AAAA,EACxD;AACA,MAAI,CAAC,OAAO,SAAS,GAAG,KAAK,MAAM,GAAG;AACrC,UAAM,IAAI,MAAM,mDAAmD;AAAA,EACpE;AACA,MAAI,CAAC,OAAO,SAAS,IAAI,KAAK,OAAO,IAAI;AACxC,UAAM,IAAI,MAAM,qDAAqD;AAAA,EACtE;AACA,MAAI,CAAC,OAAO,SAAS,OAAO,KAAK,WAAW,GAAG;AAC9C,UAAM,IAAI,MAAM,2CAA2C;AAAA,EAC5D;AACA,MAAI,CAAC,OAAO,SAAS,OAAO,KAAK,WAAW,SAAS;AACpD,UAAM,IAAI,MAAM,iDAAiD;AAAA,EAClE;AACA,MAAI,CAAC,OAAO,SAAS,YAAY,KAAK,gBAAgB,GAAG;AACxD,UAAM,IAAI,MAAM,gDAAgD;AAAA,EACjE;AAEA,SAAO,EAAE,IAAI,IAAI,KAAK,MAAM,SAAS,SAAS,aAAa;AAC5D;AAEA,SAAS,WAAW,MAA8B;AACjD,SAAO;AAAA,IACN,IAAI,KAAK;AAAA,IACT,UAAU,EAAE,GAAG,KAAK,MAAM,GAAG,GAAG,KAAK,MAAM,EAAE;AAAA,IAC7C,OAAO;AAAA,IACP,SAAS,CAAC,KAAK,EAAE;AAAA,EAClB;AACD;",
4
+ "sourcesContent": ["import { mstEdges } from './mst'\nimport { cappedReplay } from './replay'\nimport { contract, finalize } from './schedule'\nimport type {\n\tClusterNode,\n\tClusterOptions,\n\tClusterTable,\n\tLeafInput,\n\tLeafScreenOffsets,\n} from './types'\n\ninterface ResolvedClusterOptions {\n\tTc: number\n\tTu: number\n\teps: number\n\tDmax: number\n\tminZoom: number\n\tmaxZoom: number\n\tmaxSplitZoom: number\n}\n\n/** @internal */\nexport function computeClusterTable(\n\tleaves: readonly LeafInput[],\n\toptions: ClusterOptions,\n\t// Render offsets for markers that draw off their anchor (imprecise pins), keyed by leaf id.\n\t// Omitted or empty, the run is identical \u2014 output and code paths \u2014 to an offset-unaware one.\n\tscreenOffsets?: LeafScreenOffsets\n): ClusterTable {\n\tconst opts = resolveOptions(options)\n\tconst leafNodes = leaves.map(leafToNode)\n\tconst edges = mstEdges(leaves)\n\n\tif (leaves.length < 2) {\n\t\treturn { events: [], leaves: leafNodes }\n\t}\n\n\tconst raw = cappedReplay(leaves, edges, { Tc: opts.Tc, Dmax: opts.Dmax }, screenOffsets)\n\tconst contracted = contract(raw, opts.eps)\n\tconst events = finalize(contracted, {\n\t\tTc: opts.Tc,\n\t\tTu: opts.Tu,\n\t\tminZoom: opts.minZoom,\n\t\tmaxZoom: opts.maxZoom,\n\t\tmaxSplitZoom: opts.maxSplitZoom,\n\t})\n\n\treturn { events, leaves: leafNodes }\n}\n\nfunction resolveOptions(options: ClusterOptions): ResolvedClusterOptions {\n\tconst Tc = options.Tc ?? 22\n\tconst Tu = options.Tu ?? 1.2 * Tc\n\tconst eps = options.eps ?? 0.7\n\tconst Dmax = options.Dmax ?? 3.75 * Tc\n\tconst maxSplitZoom = options.maxSplitZoom ?? 6\n\tconst { minZoom, maxZoom } = options\n\n\tif (!Number.isFinite(Tc) || Tc <= 0) {\n\t\tthrow new Error('Tc must be finite and greater than 0')\n\t}\n\tif (!Number.isFinite(Tu) || Tu <= Tc) {\n\t\tthrow new Error('Tu must be finite and greater than Tc')\n\t}\n\tif (!Number.isFinite(eps) || eps < 0) {\n\t\tthrow new Error('eps must be finite and greater than or equal to 0')\n\t}\n\tif (!Number.isFinite(Dmax) || Dmax < Tc) {\n\t\tthrow new Error('Dmax must be finite and greater than or equal to Tc')\n\t}\n\tif (!Number.isFinite(minZoom) || minZoom <= 0) {\n\t\tthrow new Error('minZoom must be finite and greater than 0')\n\t}\n\tif (!Number.isFinite(maxZoom) || maxZoom <= minZoom) {\n\t\tthrow new Error('maxZoom must be finite and greater than minZoom')\n\t}\n\tif (!Number.isFinite(maxSplitZoom) || maxSplitZoom <= 0) {\n\t\tthrow new Error('maxSplitZoom must be finite and greater than 0')\n\t}\n\n\treturn { Tc, Tu, eps, Dmax, minZoom, maxZoom, maxSplitZoom }\n}\n\nfunction leafToNode(leaf: LeafInput): ClusterNode {\n\treturn {\n\t\tid: leaf.id,\n\t\tcentroid: { x: leaf.point.x, y: leaf.point.y },\n\t\tcount: 1,\n\t\tmembers: [leaf.id],\n\t}\n}\n"],
5
+ "mappings": "AAAA,SAAS,gBAAgB;AACzB,SAAS,oBAAoB;AAC7B,SAAS,UAAU,gBAAgB;AAoB5B,SAAS,oBACf,QACA,SAGA,eACe;AACf,QAAM,OAAO,eAAe,OAAO;AACnC,QAAM,YAAY,OAAO,IAAI,UAAU;AACvC,QAAM,QAAQ,SAAS,MAAM;AAE7B,MAAI,OAAO,SAAS,GAAG;AACtB,WAAO,EAAE,QAAQ,CAAC,GAAG,QAAQ,UAAU;AAAA,EACxC;AAEA,QAAM,MAAM,aAAa,QAAQ,OAAO,EAAE,IAAI,KAAK,IAAI,MAAM,KAAK,KAAK,GAAG,aAAa;AACvF,QAAM,aAAa,SAAS,KAAK,KAAK,GAAG;AACzC,QAAM,SAAS,SAAS,YAAY;AAAA,IACnC,IAAI,KAAK;AAAA,IACT,IAAI,KAAK;AAAA,IACT,SAAS,KAAK;AAAA,IACd,SAAS,KAAK;AAAA,IACd,cAAc,KAAK;AAAA,EACpB,CAAC;AAED,SAAO,EAAE,QAAQ,QAAQ,UAAU;AACpC;AAEA,SAAS,eAAe,SAAiD;AACxE,QAAM,KAAK,QAAQ,MAAM;AACzB,QAAM,KAAK,QAAQ,MAAM,MAAM;AAC/B,QAAM,MAAM,QAAQ,OAAO;AAC3B,QAAM,OAAO,QAAQ,QAAQ,OAAO;AACpC,QAAM,eAAe,QAAQ,gBAAgB;AAC7C,QAAM,EAAE,SAAS,QAAQ,IAAI;AAE7B,MAAI,CAAC,OAAO,SAAS,EAAE,KAAK,MAAM,GAAG;AACpC,UAAM,IAAI,MAAM,sCAAsC;AAAA,EACvD;AACA,MAAI,CAAC,OAAO,SAAS,EAAE,KAAK,MAAM,IAAI;AACrC,UAAM,IAAI,MAAM,uCAAuC;AAAA,EACxD;AACA,MAAI,CAAC,OAAO,SAAS,GAAG,KAAK,MAAM,GAAG;AACrC,UAAM,IAAI,MAAM,mDAAmD;AAAA,EACpE;AACA,MAAI,CAAC,OAAO,SAAS,IAAI,KAAK,OAAO,IAAI;AACxC,UAAM,IAAI,MAAM,qDAAqD;AAAA,EACtE;AACA,MAAI,CAAC,OAAO,SAAS,OAAO,KAAK,WAAW,GAAG;AAC9C,UAAM,IAAI,MAAM,2CAA2C;AAAA,EAC5D;AACA,MAAI,CAAC,OAAO,SAAS,OAAO,KAAK,WAAW,SAAS;AACpD,UAAM,IAAI,MAAM,iDAAiD;AAAA,EAClE;AACA,MAAI,CAAC,OAAO,SAAS,YAAY,KAAK,gBAAgB,GAAG;AACxD,UAAM,IAAI,MAAM,gDAAgD;AAAA,EACjE;AAEA,SAAO,EAAE,IAAI,IAAI,KAAK,MAAM,SAAS,SAAS,aAAa;AAC5D;AAEA,SAAS,WAAW,MAA8B;AACjD,SAAO;AAAA,IACN,IAAI,KAAK;AAAA,IACT,UAAU,EAAE,GAAG,KAAK,MAAM,GAAG,GAAG,KAAK,MAAM,EAAE;AAAA,IAC7C,OAAO;AAAA,IACP,SAAS,CAAC,KAAK,EAAE;AAAA,EAClB;AACD;",
6
6
  "names": []
7
7
  }
@@ -1,8 +1,8 @@
1
1
  const D_FLOOR = 1e-9;
2
- function cappedReplay(leaves, edges, opts) {
2
+ function cappedReplay(leaves, edges, opts, screenOffsets) {
3
3
  validateOptions(opts);
4
4
  if (edges.length === 0) return [];
5
- const clusters = new ClusterState(leaves);
5
+ const clusters = new ClusterState(leaves, screenOffsets);
6
6
  const heap = new EdgeMaxHeap(edges, leaves);
7
7
  const incident = leaves.map(() => []);
8
8
  for (let i = 0; i < edges.length; i++) {
@@ -58,11 +58,31 @@ function zForEdge(edge, clusters, opts) {
58
58
  return zForRoots(edge, clusters.find(edge.a), clusters.find(edge.b), clusters, opts);
59
59
  }
60
60
  function zForRoots(edge, aRoot, bRoot, clusters, opts) {
61
- if (edge.d < D_FLOOR) return Number.POSITIVE_INFINITY;
62
- const gap = opts.Tc / clusters.centroidDistance(aRoot, bRoot);
61
+ const off = clusters.offsetDelta(aRoot, bRoot);
62
+ if (off === null) {
63
+ if (edge.d < D_FLOOR) return Number.POSITIVE_INFINITY;
64
+ const gap2 = opts.Tc / clusters.centroidDistance(aRoot, bRoot);
65
+ const fit2 = opts.Dmax / clusters.unionBboxDiag(aRoot, bRoot);
66
+ return Math.min(gap2, fit2);
67
+ }
68
+ const dc = clusters.centroidDelta(aRoot, bRoot);
69
+ if (dc.x === 0 && dc.y === 0) {
70
+ return Math.hypot(off.x, off.y) < opts.Tc ? Number.POSITIVE_INFINITY : 0;
71
+ }
72
+ const gap = largestVisualCrossing(dc.x, dc.y, off.x, off.y, opts.Tc);
73
+ if (gap === null) return 0;
63
74
  const fit = opts.Dmax / clusters.unionBboxDiag(aRoot, bRoot);
64
75
  return Math.min(gap, fit);
65
76
  }
77
+ function largestVisualCrossing(dcx, dcy, dox, doy, level) {
78
+ const a = dcx * dcx + dcy * dcy;
79
+ const b = 2 * (dcx * dox + dcy * doy);
80
+ const c = dox * dox + doy * doy - level * level;
81
+ const disc = b * b - 4 * a * c;
82
+ if (disc < 0) return null;
83
+ const z = (-b + Math.sqrt(disc)) / (2 * a);
84
+ return z > 0 ? z : null;
85
+ }
66
86
  class ClusterState {
67
87
  parent;
68
88
  minX;
@@ -75,7 +95,12 @@ class ClusterState {
75
95
  nodes;
76
96
  memberLists;
77
97
  minMemberIds;
78
- constructor(leaves) {
98
+ // Per-cluster sums of member render offsets (screen px), maintained like the centroid sums.
99
+ // Null when no offsets were passed — offsetDelta() then answers null unconditionally, which
100
+ // routes every pricing call down the offset-unaware path.
101
+ offsetX;
102
+ offsetY;
103
+ constructor(leaves, screenOffsets) {
79
104
  const n = leaves.length;
80
105
  this.parent = new Int32Array(n);
81
106
  this.minX = new Float64Array(n);
@@ -88,6 +113,20 @@ class ClusterState {
88
113
  this.nodes = new Array(n);
89
114
  this.memberLists = new Array(n);
90
115
  this.minMemberIds = new Array(n);
116
+ if (screenOffsets !== void 0 && screenOffsets.size > 0) {
117
+ this.offsetX = new Float64Array(n);
118
+ this.offsetY = new Float64Array(n);
119
+ for (let i = 0; i < n; i++) {
120
+ const offset = screenOffsets.get(leaves[i].id);
121
+ if (offset) {
122
+ this.offsetX[i] = offset.x;
123
+ this.offsetY[i] = offset.y;
124
+ }
125
+ }
126
+ } else {
127
+ this.offsetX = null;
128
+ this.offsetY = null;
129
+ }
91
130
  for (let i = 0; i < n; i++) {
92
131
  const leaf = leaves[i];
93
132
  this.parent[i] = i;
@@ -126,6 +165,22 @@ class ClusterState {
126
165
  this.centroidY[aRoot] - this.centroidY[bRoot]
127
166
  );
128
167
  }
168
+ centroidDelta(aRoot, bRoot) {
169
+ return {
170
+ x: this.centroidX[aRoot] - this.centroidX[bRoot],
171
+ y: this.centroidY[aRoot] - this.centroidY[bRoot]
172
+ };
173
+ }
174
+ /** The difference of the two clusters' mean render offsets (screen px), or null when it's
175
+ * zero — including always when no offsets were passed. Null routes pricing down the
176
+ * offset-unaware path. */
177
+ offsetDelta(aRoot, bRoot) {
178
+ if (this.offsetX === null || this.offsetY === null) return null;
179
+ const x = this.offsetX[aRoot] / this.counts[aRoot] - this.offsetX[bRoot] / this.counts[bRoot];
180
+ const y = this.offsetY[aRoot] / this.counts[aRoot] - this.offsetY[bRoot] / this.counts[bRoot];
181
+ if (x === 0 && y === 0) return null;
182
+ return { x, y };
183
+ }
129
184
  unionBboxDiag(aRoot, bRoot) {
130
185
  const minX = Math.min(this.minX[aRoot], this.minX[bRoot]);
131
186
  const minY = Math.min(this.minY[aRoot], this.minY[bRoot]);
@@ -163,6 +218,10 @@ class ClusterState {
163
218
  this.nodes[leftRoot] = result;
164
219
  this.memberLists[leftRoot] = members;
165
220
  this.minMemberIds[leftRoot] = members[0];
221
+ if (this.offsetX !== null && this.offsetY !== null) {
222
+ this.offsetX[leftRoot] += this.offsetX[rightRoot];
223
+ this.offsetY[leftRoot] += this.offsetY[rightRoot];
224
+ }
166
225
  return { z, children: [left, right], result };
167
226
  }
168
227
  }
@@ -183,13 +242,29 @@ function mergeSortedMembers(a, b) {
183
242
  return out;
184
243
  }
185
244
  class EdgeMaxHeap {
245
+ items = [];
246
+ // Per-edge normalized (lo, hi) id pair for the z tie-break, precomputed once so comparisons
247
+ // allocate nothing. With coincident anchors every edge prices to the same z (+Infinity), so
248
+ // the tie-break runs on nearly every comparison of a rebuild — allocating the pair there
249
+ // churned millions of short-lived tuples.
250
+ loIds;
251
+ hiIds;
186
252
  constructor(edges, leaves) {
187
- this.edges = edges;
188
- this.leaves = leaves;
253
+ const n = edges.length;
254
+ this.loIds = new Array(n);
255
+ this.hiIds = new Array(n);
256
+ for (let i = 0; i < n; i++) {
257
+ const aId = leaves[edges[i].a].id;
258
+ const bId = leaves[edges[i].b].id;
259
+ if (aId < bId) {
260
+ this.loIds[i] = aId;
261
+ this.hiIds[i] = bId;
262
+ } else {
263
+ this.loIds[i] = bId;
264
+ this.hiIds[i] = aId;
265
+ }
266
+ }
189
267
  }
190
- edges;
191
- leaves;
192
- items = [];
193
268
  peek() {
194
269
  return this.items[0];
195
270
  }
@@ -210,7 +285,10 @@ class EdgeMaxHeap {
210
285
  higherPriority(a, b) {
211
286
  if (a.z > b.z) return true;
212
287
  if (a.z < b.z) return false;
213
- return edgeIdPairLess(this.edges[a.edgeIndex], this.edges[b.edgeIndex], this.leaves);
288
+ const aLo = this.loIds[a.edgeIndex];
289
+ const bLo = this.loIds[b.edgeIndex];
290
+ if (aLo !== bLo) return aLo < bLo;
291
+ return this.hiIds[a.edgeIndex] < this.hiIds[b.edgeIndex];
214
292
  }
215
293
  siftUp(index) {
216
294
  while (index > 0) {
@@ -237,17 +315,6 @@ class EdgeMaxHeap {
237
315
  }
238
316
  }
239
317
  }
240
- function edgeIdPairLess(a, b, leaves) {
241
- const [aLo, aHi] = normalizedEdgeIds(a, leaves);
242
- const [bLo, bHi] = normalizedEdgeIds(b, leaves);
243
- if (aLo !== bLo) return aLo < bLo;
244
- return aHi < bHi;
245
- }
246
- function normalizedEdgeIds(edge, leaves) {
247
- const aId = leaves[edge.a].id;
248
- const bId = leaves[edge.b].id;
249
- return aId < bId ? [aId, bId] : [bId, aId];
250
- }
251
318
  export {
252
319
  D_FLOOR,
253
320
  cappedReplay
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/clustering/replay.ts"],
4
- "sourcesContent": ["import type { ClusterNode, LeafInput, MstEdge, RawMergeEvent } from './types'\n\nexport const D_FLOOR = 1e-9\n\nexport function cappedReplay(\n\tleaves: readonly LeafInput[],\n\tedges: readonly MstEdge[],\n\topts: { Tc: number; Dmax: number }\n): RawMergeEvent[] {\n\tvalidateOptions(opts)\n\n\tif (edges.length === 0) return []\n\n\tconst clusters = new ClusterState(leaves)\n\tconst heap = new EdgeMaxHeap(edges, leaves)\n\t// Edges incident to each current cluster root, for eager repricing: a merge\n\t// moves the result's centroid, which can RAISE an incident edge's key (the\n\t// centroid may move toward a neighbor), so the lazy pop-time recheck alone\n\t// is not enough \u2014 every price change pushes a fresh heap entry.\n\tconst incident: number[][] = leaves.map(() => [])\n\tfor (let i = 0; i < edges.length; i++) {\n\t\theap.push({ edgeIndex: i, z: zForEdge(edges[i], clusters, opts) })\n\t\tincident[edges[i].a].push(i)\n\t\tincident[edges[i].b].push(i)\n\t}\n\n\tconst events: RawMergeEvent[] = []\n\t// Emitted thresholds are clamped non-increasing: with centroid pricing a\n\t// later merge's raw price can in principle exceed an earlier one's, and the\n\t// table's descending sort is load-bearing for the runtime cursor.\n\tlet lastZ = Number.POSITIVE_INFINITY\n\twhile (events.length < edges.length) {\n\t\tconst entry = heap.pop()\n\t\tif (!entry) break\n\n\t\tconst edge = edges[entry.edgeIndex]\n\t\tconst aRoot = clusters.find(edge.a)\n\t\tconst bRoot = clusters.find(edge.b)\n\t\tif (aRoot === bRoot) continue // stale duplicate of an already-fired edge\n\n\t\tconst current = { edgeIndex: entry.edgeIndex, z: zForRoots(edge, aRoot, bRoot, clusters, opts) }\n\t\tconst next = heap.peek()\n\t\tif (next && heap.higherPriority(next, current)) {\n\t\t\theap.push(current)\n\t\t\tcontinue\n\t\t}\n\n\t\tconst z = Math.min(current.z, lastZ)\n\t\tlastZ = z\n\t\tevents.push(clusters.merge(aRoot, bRoot, z))\n\n\t\t// eager reprice: the new cluster's centroid and bbox changed, so every\n\t\t// surviving incident edge gets a fresh entry at its current price\n\t\tconst root = clusters.find(edge.a)\n\t\tconst other = root === aRoot ? bRoot : aRoot\n\t\tconst survivors: number[] = []\n\t\tfor (const idx of [...incident[root], ...incident[other]]) {\n\t\t\tconst e2 = edges[idx]\n\t\t\tconst ra = clusters.find(e2.a)\n\t\t\tconst rb = clusters.find(e2.b)\n\t\t\tif (ra === rb) continue\n\t\t\tsurvivors.push(idx)\n\t\t\theap.push({ edgeIndex: idx, z: zForRoots(e2, ra, rb, clusters, opts) })\n\t\t}\n\t\tincident[root] = survivors\n\t}\n\n\treturn events\n}\n\nfunction validateOptions(opts: { Tc: number; Dmax: number }) {\n\tif (!Number.isFinite(opts.Tc) || opts.Tc <= 0) {\n\t\tthrow new Error('Tc must be greater than 0')\n\t}\n\tif (!Number.isFinite(opts.Dmax) || opts.Dmax <= 0) {\n\t\tthrow new Error('Dmax must be greater than 0')\n\t}\n\tif (opts.Dmax < opts.Tc) {\n\t\tthrow new Error('Dmax must be greater than or equal to Tc')\n\t}\n}\n\nfunction zForEdge(\n\tedge: MstEdge,\n\tclusters: ClusterState,\n\topts: { Tc: number; Dmax: number }\n): number {\n\treturn zForRoots(edge, clusters.find(edge.a), clusters.find(edge.b), clusters, opts)\n}\n\nfunction zForRoots(\n\tedge: MstEdge,\n\taRoot: number,\n\tbRoot: number,\n\tclusters: ClusterState,\n\topts: { Tc: number; Dmax: number }\n): number {\n\tif (edge.d < D_FLOOR) return Number.POSITIVE_INFINITY\n\t// Badge-anchored gap pricing: clusters render as badges at their centroids,\n\t// so the merge is priced by the distance between the rendered centers, not\n\t// the nearest members. For leaves the two are identical; for clusters the\n\t// centroid distance is larger, so groups merge later than their closest\n\t// members would suggest \u2014 matching what the user actually sees.\n\t// (Coincident CENTROIDS with non-coincident members leave the gap term\n\t// Infinity and the fit term finite \u2014 the min stays finite, no special case.)\n\tconst gap = opts.Tc / clusters.centroidDistance(aRoot, bRoot)\n\tconst fit = opts.Dmax / clusters.unionBboxDiag(aRoot, bRoot)\n\treturn Math.min(gap, fit)\n}\n\nclass ClusterState {\n\tprivate readonly parent: Int32Array\n\tprivate readonly minX: Float64Array\n\tprivate readonly minY: Float64Array\n\tprivate readonly maxX: Float64Array\n\tprivate readonly maxY: Float64Array\n\tprivate readonly centroidX: Float64Array\n\tprivate readonly centroidY: Float64Array\n\tprivate readonly counts: Int32Array\n\tprivate readonly nodes: ClusterNode[]\n\tprivate readonly memberLists: string[][]\n\tprivate readonly minMemberIds: string[]\n\n\tconstructor(leaves: readonly LeafInput[]) {\n\t\tconst n = leaves.length\n\t\tthis.parent = new Int32Array(n)\n\t\tthis.minX = new Float64Array(n)\n\t\tthis.minY = new Float64Array(n)\n\t\tthis.maxX = new Float64Array(n)\n\t\tthis.maxY = new Float64Array(n)\n\t\tthis.centroidX = new Float64Array(n)\n\t\tthis.centroidY = new Float64Array(n)\n\t\tthis.counts = new Int32Array(n)\n\t\tthis.nodes = new Array(n)\n\t\tthis.memberLists = new Array(n)\n\t\tthis.minMemberIds = new Array(n)\n\n\t\tfor (let i = 0; i < n; i++) {\n\t\t\tconst leaf = leaves[i]\n\t\t\tthis.parent[i] = i\n\t\t\tthis.minX[i] = leaf.point.x\n\t\t\tthis.minY[i] = leaf.point.y\n\t\t\tthis.maxX[i] = leaf.point.x\n\t\t\tthis.maxY[i] = leaf.point.y\n\t\t\tthis.centroidX[i] = leaf.point.x\n\t\t\tthis.centroidY[i] = leaf.point.y\n\t\t\tthis.counts[i] = 1\n\t\t\tthis.memberLists[i] = [leaf.id]\n\t\t\tthis.minMemberIds[i] = leaf.id\n\t\t\tthis.nodes[i] = {\n\t\t\t\tid: leaf.id,\n\t\t\t\tcentroid: { x: leaf.point.x, y: leaf.point.y },\n\t\t\t\tcount: 1,\n\t\t\t\tmembers: [leaf.id],\n\t\t\t}\n\t\t}\n\t}\n\n\tfind(index: number): number {\n\t\tlet root = index\n\t\twhile (this.parent[root] !== root) {\n\t\t\troot = this.parent[root]\n\t\t}\n\t\twhile (this.parent[index] !== index) {\n\t\t\tconst next = this.parent[index]\n\t\t\tthis.parent[index] = root\n\t\t\tindex = next\n\t\t}\n\t\treturn root\n\t}\n\n\tcentroidDistance(aRoot: number, bRoot: number): number {\n\t\treturn Math.hypot(\n\t\t\tthis.centroidX[aRoot] - this.centroidX[bRoot],\n\t\t\tthis.centroidY[aRoot] - this.centroidY[bRoot]\n\t\t)\n\t}\n\n\tunionBboxDiag(aRoot: number, bRoot: number): number {\n\t\tconst minX = Math.min(this.minX[aRoot], this.minX[bRoot])\n\t\tconst minY = Math.min(this.minY[aRoot], this.minY[bRoot])\n\t\tconst maxX = Math.max(this.maxX[aRoot], this.maxX[bRoot])\n\t\tconst maxY = Math.max(this.maxY[aRoot], this.maxY[bRoot])\n\t\treturn Math.hypot(maxX - minX, maxY - minY)\n\t}\n\n\tmerge(aRoot: number, bRoot: number, z: number): RawMergeEvent {\n\t\tconst leftRoot = this.minMemberIds[aRoot] < this.minMemberIds[bRoot] ? aRoot : bRoot\n\t\tconst rightRoot = leftRoot === aRoot ? bRoot : aRoot\n\t\tconst left = this.nodes[leftRoot]\n\t\tconst right = this.nodes[rightRoot]\n\t\tconst count = this.counts[leftRoot] + this.counts[rightRoot]\n\t\tconst members = mergeSortedMembers(this.memberLists[leftRoot], this.memberLists[rightRoot])\n\n\t\tconst minX = Math.min(this.minX[leftRoot], this.minX[rightRoot])\n\t\tconst minY = Math.min(this.minY[leftRoot], this.minY[rightRoot])\n\t\tconst maxX = Math.max(this.maxX[leftRoot], this.maxX[rightRoot])\n\t\tconst maxY = Math.max(this.maxY[leftRoot], this.maxY[rightRoot])\n\t\tconst centroidX =\n\t\t\t(this.counts[leftRoot] * this.centroidX[leftRoot] +\n\t\t\t\tthis.counts[rightRoot] * this.centroidX[rightRoot]) /\n\t\t\tcount\n\t\tconst centroidY =\n\t\t\t(this.counts[leftRoot] * this.centroidY[leftRoot] +\n\t\t\t\tthis.counts[rightRoot] * this.centroidY[rightRoot]) /\n\t\t\tcount\n\n\t\t// Leaf ids are assumed not to start with `cluster:`; see the step 2 contract.\n\t\tconst result: ClusterNode = {\n\t\t\tid: `cluster:${count}:${members[0]}`,\n\t\t\tcentroid: { x: centroidX, y: centroidY },\n\t\t\tcount,\n\t\t\tmembers,\n\t\t}\n\n\t\tthis.parent[rightRoot] = leftRoot\n\t\tthis.minX[leftRoot] = minX\n\t\tthis.minY[leftRoot] = minY\n\t\tthis.maxX[leftRoot] = maxX\n\t\tthis.maxY[leftRoot] = maxY\n\t\tthis.centroidX[leftRoot] = centroidX\n\t\tthis.centroidY[leftRoot] = centroidY\n\t\tthis.counts[leftRoot] = count\n\t\tthis.nodes[leftRoot] = result\n\t\tthis.memberLists[leftRoot] = members\n\t\tthis.minMemberIds[leftRoot] = members[0]\n\n\t\treturn { z, children: [left, right], result }\n\t}\n}\n\nfunction mergeSortedMembers(a: readonly string[], b: readonly string[]): string[] {\n\tconst out = new Array<string>(a.length + b.length)\n\tlet i = 0\n\tlet j = 0\n\tlet k = 0\n\twhile (i < a.length && j < b.length) {\n\t\tif (a[i] < b[j]) {\n\t\t\tout[k++] = a[i++]\n\t\t} else {\n\t\t\tout[k++] = b[j++]\n\t\t}\n\t}\n\twhile (i < a.length) out[k++] = a[i++]\n\twhile (j < b.length) out[k++] = b[j++]\n\treturn out\n}\n\ninterface HeapEntry {\n\tedgeIndex: number\n\tz: number\n}\n\nclass EdgeMaxHeap {\n\tprivate readonly items: HeapEntry[] = []\n\n\tconstructor(\n\t\tprivate readonly edges: readonly MstEdge[],\n\t\tprivate readonly leaves: readonly LeafInput[]\n\t) {}\n\n\tpeek(): HeapEntry | undefined {\n\t\treturn this.items[0]\n\t}\n\n\tpush(entry: HeapEntry) {\n\t\tthis.items.push(entry)\n\t\tthis.siftUp(this.items.length - 1)\n\t}\n\n\tpop(): HeapEntry | undefined {\n\t\tif (this.items.length === 0) return undefined\n\t\tconst first = this.items[0]\n\t\tconst last = this.items.pop()!\n\t\tif (this.items.length > 0) {\n\t\t\tthis.items[0] = last\n\t\t\tthis.siftDown(0)\n\t\t}\n\t\treturn first\n\t}\n\n\thigherPriority(a: HeapEntry, b: HeapEntry): boolean {\n\t\tif (a.z > b.z) return true\n\t\tif (a.z < b.z) return false\n\t\treturn edgeIdPairLess(this.edges[a.edgeIndex], this.edges[b.edgeIndex], this.leaves)\n\t}\n\n\tprivate siftUp(index: number) {\n\t\twhile (index > 0) {\n\t\t\tconst parent = (index - 1) >> 1\n\t\t\tif (!this.higherPriority(this.items[index], this.items[parent])) break\n\t\t\t;[this.items[index], this.items[parent]] = [this.items[parent], this.items[index]]\n\t\t\tindex = parent\n\t\t}\n\t}\n\n\tprivate siftDown(index: number) {\n\t\twhile (true) {\n\t\t\tconst left = index * 2 + 1\n\t\t\tconst right = left + 1\n\t\t\tlet best = index\n\t\t\tif (left < this.items.length && this.higherPriority(this.items[left], this.items[best])) {\n\t\t\t\tbest = left\n\t\t\t}\n\t\t\tif (right < this.items.length && this.higherPriority(this.items[right], this.items[best])) {\n\t\t\t\tbest = right\n\t\t\t}\n\t\t\tif (best === index) break\n\t\t\t;[this.items[index], this.items[best]] = [this.items[best], this.items[index]]\n\t\t\tindex = best\n\t\t}\n\t}\n}\n\nfunction edgeIdPairLess(a: MstEdge, b: MstEdge, leaves: readonly LeafInput[]): boolean {\n\tconst [aLo, aHi] = normalizedEdgeIds(a, leaves)\n\tconst [bLo, bHi] = normalizedEdgeIds(b, leaves)\n\tif (aLo !== bLo) return aLo < bLo\n\treturn aHi < bHi\n}\n\nfunction normalizedEdgeIds(edge: MstEdge, leaves: readonly LeafInput[]): [string, string] {\n\tconst aId = leaves[edge.a].id\n\tconst bId = leaves[edge.b].id\n\treturn aId < bId ? [aId, bId] : [bId, aId]\n}\n"],
5
- "mappings": "AAEO,MAAM,UAAU;AAEhB,SAAS,aACf,QACA,OACA,MACkB;AAClB,kBAAgB,IAAI;AAEpB,MAAI,MAAM,WAAW,EAAG,QAAO,CAAC;AAEhC,QAAM,WAAW,IAAI,aAAa,MAAM;AACxC,QAAM,OAAO,IAAI,YAAY,OAAO,MAAM;AAK1C,QAAM,WAAuB,OAAO,IAAI,MAAM,CAAC,CAAC;AAChD,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACtC,SAAK,KAAK,EAAE,WAAW,GAAG,GAAG,SAAS,MAAM,CAAC,GAAG,UAAU,IAAI,EAAE,CAAC;AACjE,aAAS,MAAM,CAAC,EAAE,CAAC,EAAE,KAAK,CAAC;AAC3B,aAAS,MAAM,CAAC,EAAE,CAAC,EAAE,KAAK,CAAC;AAAA,EAC5B;AAEA,QAAM,SAA0B,CAAC;AAIjC,MAAI,QAAQ,OAAO;AACnB,SAAO,OAAO,SAAS,MAAM,QAAQ;AACpC,UAAM,QAAQ,KAAK,IAAI;AACvB,QAAI,CAAC,MAAO;AAEZ,UAAM,OAAO,MAAM,MAAM,SAAS;AAClC,UAAM,QAAQ,SAAS,KAAK,KAAK,CAAC;AAClC,UAAM,QAAQ,SAAS,KAAK,KAAK,CAAC;AAClC,QAAI,UAAU,MAAO;AAErB,UAAM,UAAU,EAAE,WAAW,MAAM,WAAW,GAAG,UAAU,MAAM,OAAO,OAAO,UAAU,IAAI,EAAE;AAC/F,UAAM,OAAO,KAAK,KAAK;AACvB,QAAI,QAAQ,KAAK,eAAe,MAAM,OAAO,GAAG;AAC/C,WAAK,KAAK,OAAO;AACjB;AAAA,IACD;AAEA,UAAM,IAAI,KAAK,IAAI,QAAQ,GAAG,KAAK;AACnC,YAAQ;AACR,WAAO,KAAK,SAAS,MAAM,OAAO,OAAO,CAAC,CAAC;AAI3C,UAAM,OAAO,SAAS,KAAK,KAAK,CAAC;AACjC,UAAM,QAAQ,SAAS,QAAQ,QAAQ;AACvC,UAAM,YAAsB,CAAC;AAC7B,eAAW,OAAO,CAAC,GAAG,SAAS,IAAI,GAAG,GAAG,SAAS,KAAK,CAAC,GAAG;AAC1D,YAAM,KAAK,MAAM,GAAG;AACpB,YAAM,KAAK,SAAS,KAAK,GAAG,CAAC;AAC7B,YAAM,KAAK,SAAS,KAAK,GAAG,CAAC;AAC7B,UAAI,OAAO,GAAI;AACf,gBAAU,KAAK,GAAG;AAClB,WAAK,KAAK,EAAE,WAAW,KAAK,GAAG,UAAU,IAAI,IAAI,IAAI,UAAU,IAAI,EAAE,CAAC;AAAA,IACvE;AACA,aAAS,IAAI,IAAI;AAAA,EAClB;AAEA,SAAO;AACR;AAEA,SAAS,gBAAgB,MAAoC;AAC5D,MAAI,CAAC,OAAO,SAAS,KAAK,EAAE,KAAK,KAAK,MAAM,GAAG;AAC9C,UAAM,IAAI,MAAM,2BAA2B;AAAA,EAC5C;AACA,MAAI,CAAC,OAAO,SAAS,KAAK,IAAI,KAAK,KAAK,QAAQ,GAAG;AAClD,UAAM,IAAI,MAAM,6BAA6B;AAAA,EAC9C;AACA,MAAI,KAAK,OAAO,KAAK,IAAI;AACxB,UAAM,IAAI,MAAM,0CAA0C;AAAA,EAC3D;AACD;AAEA,SAAS,SACR,MACA,UACA,MACS;AACT,SAAO,UAAU,MAAM,SAAS,KAAK,KAAK,CAAC,GAAG,SAAS,KAAK,KAAK,CAAC,GAAG,UAAU,IAAI;AACpF;AAEA,SAAS,UACR,MACA,OACA,OACA,UACA,MACS;AACT,MAAI,KAAK,IAAI,QAAS,QAAO,OAAO;AAQpC,QAAM,MAAM,KAAK,KAAK,SAAS,iBAAiB,OAAO,KAAK;AAC5D,QAAM,MAAM,KAAK,OAAO,SAAS,cAAc,OAAO,KAAK;AAC3D,SAAO,KAAK,IAAI,KAAK,GAAG;AACzB;AAEA,MAAM,aAAa;AAAA,EACD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEjB,YAAY,QAA8B;AACzC,UAAM,IAAI,OAAO;AACjB,SAAK,SAAS,IAAI,WAAW,CAAC;AAC9B,SAAK,OAAO,IAAI,aAAa,CAAC;AAC9B,SAAK,OAAO,IAAI,aAAa,CAAC;AAC9B,SAAK,OAAO,IAAI,aAAa,CAAC;AAC9B,SAAK,OAAO,IAAI,aAAa,CAAC;AAC9B,SAAK,YAAY,IAAI,aAAa,CAAC;AACnC,SAAK,YAAY,IAAI,aAAa,CAAC;AACnC,SAAK,SAAS,IAAI,WAAW,CAAC;AAC9B,SAAK,QAAQ,IAAI,MAAM,CAAC;AACxB,SAAK,cAAc,IAAI,MAAM,CAAC;AAC9B,SAAK,eAAe,IAAI,MAAM,CAAC;AAE/B,aAAS,IAAI,GAAG,IAAI,GAAG,KAAK;AAC3B,YAAM,OAAO,OAAO,CAAC;AACrB,WAAK,OAAO,CAAC,IAAI;AACjB,WAAK,KAAK,CAAC,IAAI,KAAK,MAAM;AAC1B,WAAK,KAAK,CAAC,IAAI,KAAK,MAAM;AAC1B,WAAK,KAAK,CAAC,IAAI,KAAK,MAAM;AAC1B,WAAK,KAAK,CAAC,IAAI,KAAK,MAAM;AAC1B,WAAK,UAAU,CAAC,IAAI,KAAK,MAAM;AAC/B,WAAK,UAAU,CAAC,IAAI,KAAK,MAAM;AAC/B,WAAK,OAAO,CAAC,IAAI;AACjB,WAAK,YAAY,CAAC,IAAI,CAAC,KAAK,EAAE;AAC9B,WAAK,aAAa,CAAC,IAAI,KAAK;AAC5B,WAAK,MAAM,CAAC,IAAI;AAAA,QACf,IAAI,KAAK;AAAA,QACT,UAAU,EAAE,GAAG,KAAK,MAAM,GAAG,GAAG,KAAK,MAAM,EAAE;AAAA,QAC7C,OAAO;AAAA,QACP,SAAS,CAAC,KAAK,EAAE;AAAA,MAClB;AAAA,IACD;AAAA,EACD;AAAA,EAEA,KAAK,OAAuB;AAC3B,QAAI,OAAO;AACX,WAAO,KAAK,OAAO,IAAI,MAAM,MAAM;AAClC,aAAO,KAAK,OAAO,IAAI;AAAA,IACxB;AACA,WAAO,KAAK,OAAO,KAAK,MAAM,OAAO;AACpC,YAAM,OAAO,KAAK,OAAO,KAAK;AAC9B,WAAK,OAAO,KAAK,IAAI;AACrB,cAAQ;AAAA,IACT;AACA,WAAO;AAAA,EACR;AAAA,EAEA,iBAAiB,OAAe,OAAuB;AACtD,WAAO,KAAK;AAAA,MACX,KAAK,UAAU,KAAK,IAAI,KAAK,UAAU,KAAK;AAAA,MAC5C,KAAK,UAAU,KAAK,IAAI,KAAK,UAAU,KAAK;AAAA,IAC7C;AAAA,EACD;AAAA,EAEA,cAAc,OAAe,OAAuB;AACnD,UAAM,OAAO,KAAK,IAAI,KAAK,KAAK,KAAK,GAAG,KAAK,KAAK,KAAK,CAAC;AACxD,UAAM,OAAO,KAAK,IAAI,KAAK,KAAK,KAAK,GAAG,KAAK,KAAK,KAAK,CAAC;AACxD,UAAM,OAAO,KAAK,IAAI,KAAK,KAAK,KAAK,GAAG,KAAK,KAAK,KAAK,CAAC;AACxD,UAAM,OAAO,KAAK,IAAI,KAAK,KAAK,KAAK,GAAG,KAAK,KAAK,KAAK,CAAC;AACxD,WAAO,KAAK,MAAM,OAAO,MAAM,OAAO,IAAI;AAAA,EAC3C;AAAA,EAEA,MAAM,OAAe,OAAe,GAA0B;AAC7D,UAAM,WAAW,KAAK,aAAa,KAAK,IAAI,KAAK,aAAa,KAAK,IAAI,QAAQ;AAC/E,UAAM,YAAY,aAAa,QAAQ,QAAQ;AAC/C,UAAM,OAAO,KAAK,MAAM,QAAQ;AAChC,UAAM,QAAQ,KAAK,MAAM,SAAS;AAClC,UAAM,QAAQ,KAAK,OAAO,QAAQ,IAAI,KAAK,OAAO,SAAS;AAC3D,UAAM,UAAU,mBAAmB,KAAK,YAAY,QAAQ,GAAG,KAAK,YAAY,SAAS,CAAC;AAE1F,UAAM,OAAO,KAAK,IAAI,KAAK,KAAK,QAAQ,GAAG,KAAK,KAAK,SAAS,CAAC;AAC/D,UAAM,OAAO,KAAK,IAAI,KAAK,KAAK,QAAQ,GAAG,KAAK,KAAK,SAAS,CAAC;AAC/D,UAAM,OAAO,KAAK,IAAI,KAAK,KAAK,QAAQ,GAAG,KAAK,KAAK,SAAS,CAAC;AAC/D,UAAM,OAAO,KAAK,IAAI,KAAK,KAAK,QAAQ,GAAG,KAAK,KAAK,SAAS,CAAC;AAC/D,UAAM,aACJ,KAAK,OAAO,QAAQ,IAAI,KAAK,UAAU,QAAQ,IAC/C,KAAK,OAAO,SAAS,IAAI,KAAK,UAAU,SAAS,KAClD;AACD,UAAM,aACJ,KAAK,OAAO,QAAQ,IAAI,KAAK,UAAU,QAAQ,IAC/C,KAAK,OAAO,SAAS,IAAI,KAAK,UAAU,SAAS,KAClD;AAGD,UAAM,SAAsB;AAAA,MAC3B,IAAI,WAAW,KAAK,IAAI,QAAQ,CAAC,CAAC;AAAA,MAClC,UAAU,EAAE,GAAG,WAAW,GAAG,UAAU;AAAA,MACvC;AAAA,MACA;AAAA,IACD;AAEA,SAAK,OAAO,SAAS,IAAI;AACzB,SAAK,KAAK,QAAQ,IAAI;AACtB,SAAK,KAAK,QAAQ,IAAI;AACtB,SAAK,KAAK,QAAQ,IAAI;AACtB,SAAK,KAAK,QAAQ,IAAI;AACtB,SAAK,UAAU,QAAQ,IAAI;AAC3B,SAAK,UAAU,QAAQ,IAAI;AAC3B,SAAK,OAAO,QAAQ,IAAI;AACxB,SAAK,MAAM,QAAQ,IAAI;AACvB,SAAK,YAAY,QAAQ,IAAI;AAC7B,SAAK,aAAa,QAAQ,IAAI,QAAQ,CAAC;AAEvC,WAAO,EAAE,GAAG,UAAU,CAAC,MAAM,KAAK,GAAG,OAAO;AAAA,EAC7C;AACD;AAEA,SAAS,mBAAmB,GAAsB,GAAgC;AACjF,QAAM,MAAM,IAAI,MAAc,EAAE,SAAS,EAAE,MAAM;AACjD,MAAI,IAAI;AACR,MAAI,IAAI;AACR,MAAI,IAAI;AACR,SAAO,IAAI,EAAE,UAAU,IAAI,EAAE,QAAQ;AACpC,QAAI,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG;AAChB,UAAI,GAAG,IAAI,EAAE,GAAG;AAAA,IACjB,OAAO;AACN,UAAI,GAAG,IAAI,EAAE,GAAG;AAAA,IACjB;AAAA,EACD;AACA,SAAO,IAAI,EAAE,OAAQ,KAAI,GAAG,IAAI,EAAE,GAAG;AACrC,SAAO,IAAI,EAAE,OAAQ,KAAI,GAAG,IAAI,EAAE,GAAG;AACrC,SAAO;AACR;AAOA,MAAM,YAAY;AAAA,EAGjB,YACkB,OACA,QAChB;AAFgB;AACA;AAAA,EACf;AAAA,EAFe;AAAA,EACA;AAAA,EAJD,QAAqB,CAAC;AAAA,EAOvC,OAA8B;AAC7B,WAAO,KAAK,MAAM,CAAC;AAAA,EACpB;AAAA,EAEA,KAAK,OAAkB;AACtB,SAAK,MAAM,KAAK,KAAK;AACrB,SAAK,OAAO,KAAK,MAAM,SAAS,CAAC;AAAA,EAClC;AAAA,EAEA,MAA6B;AAC5B,QAAI,KAAK,MAAM,WAAW,EAAG,QAAO;AACpC,UAAM,QAAQ,KAAK,MAAM,CAAC;AAC1B,UAAM,OAAO,KAAK,MAAM,IAAI;AAC5B,QAAI,KAAK,MAAM,SAAS,GAAG;AAC1B,WAAK,MAAM,CAAC,IAAI;AAChB,WAAK,SAAS,CAAC;AAAA,IAChB;AACA,WAAO;AAAA,EACR;AAAA,EAEA,eAAe,GAAc,GAAuB;AACnD,QAAI,EAAE,IAAI,EAAE,EAAG,QAAO;AACtB,QAAI,EAAE,IAAI,EAAE,EAAG,QAAO;AACtB,WAAO,eAAe,KAAK,MAAM,EAAE,SAAS,GAAG,KAAK,MAAM,EAAE,SAAS,GAAG,KAAK,MAAM;AAAA,EACpF;AAAA,EAEQ,OAAO,OAAe;AAC7B,WAAO,QAAQ,GAAG;AACjB,YAAM,SAAU,QAAQ,KAAM;AAC9B,UAAI,CAAC,KAAK,eAAe,KAAK,MAAM,KAAK,GAAG,KAAK,MAAM,MAAM,CAAC,EAAG;AAChE,OAAC,KAAK,MAAM,KAAK,GAAG,KAAK,MAAM,MAAM,CAAC,IAAI,CAAC,KAAK,MAAM,MAAM,GAAG,KAAK,MAAM,KAAK,CAAC;AACjF,cAAQ;AAAA,IACT;AAAA,EACD;AAAA,EAEQ,SAAS,OAAe;AAC/B,WAAO,MAAM;AACZ,YAAM,OAAO,QAAQ,IAAI;AACzB,YAAM,QAAQ,OAAO;AACrB,UAAI,OAAO;AACX,UAAI,OAAO,KAAK,MAAM,UAAU,KAAK,eAAe,KAAK,MAAM,IAAI,GAAG,KAAK,MAAM,IAAI,CAAC,GAAG;AACxF,eAAO;AAAA,MACR;AACA,UAAI,QAAQ,KAAK,MAAM,UAAU,KAAK,eAAe,KAAK,MAAM,KAAK,GAAG,KAAK,MAAM,IAAI,CAAC,GAAG;AAC1F,eAAO;AAAA,MACR;AACA,UAAI,SAAS,MAAO;AACnB,OAAC,KAAK,MAAM,KAAK,GAAG,KAAK,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,MAAM,IAAI,GAAG,KAAK,MAAM,KAAK,CAAC;AAC7E,cAAQ;AAAA,IACT;AAAA,EACD;AACD;AAEA,SAAS,eAAe,GAAY,GAAY,QAAuC;AACtF,QAAM,CAAC,KAAK,GAAG,IAAI,kBAAkB,GAAG,MAAM;AAC9C,QAAM,CAAC,KAAK,GAAG,IAAI,kBAAkB,GAAG,MAAM;AAC9C,MAAI,QAAQ,IAAK,QAAO,MAAM;AAC9B,SAAO,MAAM;AACd;AAEA,SAAS,kBAAkB,MAAe,QAAgD;AACzF,QAAM,MAAM,OAAO,KAAK,CAAC,EAAE;AAC3B,QAAM,MAAM,OAAO,KAAK,CAAC,EAAE;AAC3B,SAAO,MAAM,MAAM,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,GAAG;AAC1C;",
6
- "names": []
4
+ "sourcesContent": ["import type { ClusterNode, LeafInput, LeafScreenOffsets, MstEdge, RawMergeEvent } from './types'\n\nexport const D_FLOOR = 1e-9\n\nexport function cappedReplay(\n\tleaves: readonly LeafInput[],\n\tedges: readonly MstEdge[],\n\topts: { Tc: number; Dmax: number },\n\t// Render offsets for markers that draw off their anchor (imprecise pins). Omitted or empty,\n\t// every code path below is the offset-unaware original \u2014 pricing, events, and floats alike.\n\tscreenOffsets?: LeafScreenOffsets\n): RawMergeEvent[] {\n\tvalidateOptions(opts)\n\n\tif (edges.length === 0) return []\n\n\tconst clusters = new ClusterState(leaves, screenOffsets)\n\tconst heap = new EdgeMaxHeap(edges, leaves)\n\t// Edges incident to each current cluster root, for eager repricing: a merge\n\t// moves the result's centroid, which can RAISE an incident edge's key (the\n\t// centroid may move toward a neighbor), so the lazy pop-time recheck alone\n\t// is not enough \u2014 every price change pushes a fresh heap entry.\n\tconst incident: number[][] = leaves.map(() => [])\n\tfor (let i = 0; i < edges.length; i++) {\n\t\theap.push({ edgeIndex: i, z: zForEdge(edges[i], clusters, opts) })\n\t\tincident[edges[i].a].push(i)\n\t\tincident[edges[i].b].push(i)\n\t}\n\n\tconst events: RawMergeEvent[] = []\n\t// Emitted thresholds are clamped non-increasing: with centroid pricing a\n\t// later merge's raw price can in principle exceed an earlier one's, and the\n\t// table's descending sort is load-bearing for the runtime cursor.\n\tlet lastZ = Number.POSITIVE_INFINITY\n\twhile (events.length < edges.length) {\n\t\tconst entry = heap.pop()\n\t\tif (!entry) break\n\n\t\tconst edge = edges[entry.edgeIndex]\n\t\tconst aRoot = clusters.find(edge.a)\n\t\tconst bRoot = clusters.find(edge.b)\n\t\tif (aRoot === bRoot) continue // stale duplicate of an already-fired edge\n\n\t\tconst current = { edgeIndex: entry.edgeIndex, z: zForRoots(edge, aRoot, bRoot, clusters, opts) }\n\t\tconst next = heap.peek()\n\t\tif (next && heap.higherPriority(next, current)) {\n\t\t\theap.push(current)\n\t\t\tcontinue\n\t\t}\n\n\t\tconst z = Math.min(current.z, lastZ)\n\t\tlastZ = z\n\t\tevents.push(clusters.merge(aRoot, bRoot, z))\n\n\t\t// eager reprice: the new cluster's centroid and bbox changed, so every\n\t\t// surviving incident edge gets a fresh entry at its current price\n\t\tconst root = clusters.find(edge.a)\n\t\tconst other = root === aRoot ? bRoot : aRoot\n\t\tconst survivors: number[] = []\n\t\tfor (const idx of [...incident[root], ...incident[other]]) {\n\t\t\tconst e2 = edges[idx]\n\t\t\tconst ra = clusters.find(e2.a)\n\t\t\tconst rb = clusters.find(e2.b)\n\t\t\tif (ra === rb) continue\n\t\t\tsurvivors.push(idx)\n\t\t\theap.push({ edgeIndex: idx, z: zForRoots(e2, ra, rb, clusters, opts) })\n\t\t}\n\t\tincident[root] = survivors\n\t}\n\n\treturn events\n}\n\nfunction validateOptions(opts: { Tc: number; Dmax: number }) {\n\tif (!Number.isFinite(opts.Tc) || opts.Tc <= 0) {\n\t\tthrow new Error('Tc must be greater than 0')\n\t}\n\tif (!Number.isFinite(opts.Dmax) || opts.Dmax <= 0) {\n\t\tthrow new Error('Dmax must be greater than 0')\n\t}\n\tif (opts.Dmax < opts.Tc) {\n\t\tthrow new Error('Dmax must be greater than or equal to Tc')\n\t}\n}\n\nfunction zForEdge(\n\tedge: MstEdge,\n\tclusters: ClusterState,\n\topts: { Tc: number; Dmax: number }\n): number {\n\treturn zForRoots(edge, clusters.find(edge.a), clusters.find(edge.b), clusters, opts)\n}\n\nfunction zForRoots(\n\tedge: MstEdge,\n\taRoot: number,\n\tbRoot: number,\n\tclusters: ClusterState,\n\topts: { Tc: number; Dmax: number }\n): number {\n\t// Null unless offset pricing is active AND this pair's mean render offsets differ. Every\n\t// other pair \u2014 including every pair when no offsets were passed \u2014 takes the branch below,\n\t// which is the offset-unaware pricing verbatim.\n\tconst off = clusters.offsetDelta(aRoot, bRoot)\n\tif (off === null) {\n\t\tif (edge.d < D_FLOOR) return Number.POSITIVE_INFINITY\n\t\t// Badge-anchored gap pricing: clusters render as badges at their centroids,\n\t\t// so the merge is priced by the distance between the rendered centers, not\n\t\t// the nearest members. For leaves the two are identical; for clusters the\n\t\t// centroid distance is larger, so groups merge later than their closest\n\t\t// members would suggest \u2014 matching what the user actually sees.\n\t\t// (Coincident CENTROIDS with non-coincident members leave the gap term\n\t\t// Infinity and the fit term finite \u2014 the min stays finite, no special case.)\n\t\tconst gap = opts.Tc / clusters.centroidDistance(aRoot, bRoot)\n\t\tconst fit = opts.Dmax / clusters.unionBboxDiag(aRoot, bRoot)\n\t\treturn Math.min(gap, fit)\n\t}\n\n\t// Offset-aware gap pricing: these markers render at `z\u00B7centroid + meanOffset`, so their\n\t// visual distance at zoom z is |z\u00B7\u0394C + \u0394\u014D| and the merge prices at its Tc crossing. When\n\t// that distance never reaches Tc (the constant offsets hold the visuals apart harder than\n\t// the anchors close), the pair prices at 0 \u2014 visually never mergeable, pruned by finalize's\n\t// minZoom cut. No D_FLOOR fast path here: coincident anchors with differing offsets are a\n\t// constant |\u0394\u014D| apart on screen, which is exactly the \u0394C = 0 case below.\n\tconst dc = clusters.centroidDelta(aRoot, bRoot)\n\tif (dc.x === 0 && dc.y === 0) {\n\t\treturn Math.hypot(off.x, off.y) < opts.Tc ? Number.POSITIVE_INFINITY : 0\n\t}\n\tconst gap = largestVisualCrossing(dc.x, dc.y, off.x, off.y, opts.Tc)\n\tif (gap === null) return 0\n\tconst fit = opts.Dmax / clusters.unionBboxDiag(aRoot, bRoot)\n\treturn Math.min(gap, fit)\n}\n\n/**\n * The largest positive root of `|z\u00B7\u0394C + \u0394\u014D| = level`, i.e. of\n * `|\u0394C|\u00B2\u00B7z\u00B2 + 2(\u0394C\u00B7\u0394\u014D)\u00B7z + (|\u0394\u014D|\u00B2 \u2212 level\u00B2) = 0` \u2014 the zoom at which two offset markers are\n * exactly `level` screen px apart, with the distance below the level for every smaller zoom\n * (matching the \"merged at z \u2264 threshold\" model; a lower second crossing, where opposed offsets\n * push the visuals back above the level near z = 0, is deliberately collapsed). Null when the\n * visual distance never reaches the level. Callers handle \u0394C = 0 (constant distance) themselves.\n */\nfunction largestVisualCrossing(\n\tdcx: number,\n\tdcy: number,\n\tdox: number,\n\tdoy: number,\n\tlevel: number\n): number | null {\n\tconst a = dcx * dcx + dcy * dcy\n\tconst b = 2 * (dcx * dox + dcy * doy)\n\tconst c = dox * dox + doy * doy - level * level\n\tconst disc = b * b - 4 * a * c\n\tif (disc < 0) return null\n\tconst z = (-b + Math.sqrt(disc)) / (2 * a)\n\treturn z > 0 ? z : null\n}\n\nclass ClusterState {\n\tprivate readonly parent: Int32Array\n\tprivate readonly minX: Float64Array\n\tprivate readonly minY: Float64Array\n\tprivate readonly maxX: Float64Array\n\tprivate readonly maxY: Float64Array\n\tprivate readonly centroidX: Float64Array\n\tprivate readonly centroidY: Float64Array\n\tprivate readonly counts: Int32Array\n\tprivate readonly nodes: ClusterNode[]\n\tprivate readonly memberLists: string[][]\n\tprivate readonly minMemberIds: string[]\n\t// Per-cluster sums of member render offsets (screen px), maintained like the centroid sums.\n\t// Null when no offsets were passed \u2014 offsetDelta() then answers null unconditionally, which\n\t// routes every pricing call down the offset-unaware path.\n\tprivate readonly offsetX: Float64Array | null\n\tprivate readonly offsetY: Float64Array | null\n\n\tconstructor(leaves: readonly LeafInput[], screenOffsets?: LeafScreenOffsets) {\n\t\tconst n = leaves.length\n\t\tthis.parent = new Int32Array(n)\n\t\tthis.minX = new Float64Array(n)\n\t\tthis.minY = new Float64Array(n)\n\t\tthis.maxX = new Float64Array(n)\n\t\tthis.maxY = new Float64Array(n)\n\t\tthis.centroidX = new Float64Array(n)\n\t\tthis.centroidY = new Float64Array(n)\n\t\tthis.counts = new Int32Array(n)\n\t\tthis.nodes = new Array(n)\n\t\tthis.memberLists = new Array(n)\n\t\tthis.minMemberIds = new Array(n)\n\n\t\tif (screenOffsets !== undefined && screenOffsets.size > 0) {\n\t\t\tthis.offsetX = new Float64Array(n)\n\t\t\tthis.offsetY = new Float64Array(n)\n\t\t\tfor (let i = 0; i < n; i++) {\n\t\t\t\tconst offset = screenOffsets.get(leaves[i].id)\n\t\t\t\tif (offset) {\n\t\t\t\t\tthis.offsetX[i] = offset.x\n\t\t\t\t\tthis.offsetY[i] = offset.y\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tthis.offsetX = null\n\t\t\tthis.offsetY = null\n\t\t}\n\n\t\tfor (let i = 0; i < n; i++) {\n\t\t\tconst leaf = leaves[i]\n\t\t\tthis.parent[i] = i\n\t\t\tthis.minX[i] = leaf.point.x\n\t\t\tthis.minY[i] = leaf.point.y\n\t\t\tthis.maxX[i] = leaf.point.x\n\t\t\tthis.maxY[i] = leaf.point.y\n\t\t\tthis.centroidX[i] = leaf.point.x\n\t\t\tthis.centroidY[i] = leaf.point.y\n\t\t\tthis.counts[i] = 1\n\t\t\tthis.memberLists[i] = [leaf.id]\n\t\t\tthis.minMemberIds[i] = leaf.id\n\t\t\tthis.nodes[i] = {\n\t\t\t\tid: leaf.id,\n\t\t\t\tcentroid: { x: leaf.point.x, y: leaf.point.y },\n\t\t\t\tcount: 1,\n\t\t\t\tmembers: [leaf.id],\n\t\t\t}\n\t\t}\n\t}\n\n\tfind(index: number): number {\n\t\tlet root = index\n\t\twhile (this.parent[root] !== root) {\n\t\t\troot = this.parent[root]\n\t\t}\n\t\twhile (this.parent[index] !== index) {\n\t\t\tconst next = this.parent[index]\n\t\t\tthis.parent[index] = root\n\t\t\tindex = next\n\t\t}\n\t\treturn root\n\t}\n\n\tcentroidDistance(aRoot: number, bRoot: number): number {\n\t\treturn Math.hypot(\n\t\t\tthis.centroidX[aRoot] - this.centroidX[bRoot],\n\t\t\tthis.centroidY[aRoot] - this.centroidY[bRoot]\n\t\t)\n\t}\n\n\tcentroidDelta(aRoot: number, bRoot: number): { x: number; y: number } {\n\t\treturn {\n\t\t\tx: this.centroidX[aRoot] - this.centroidX[bRoot],\n\t\t\ty: this.centroidY[aRoot] - this.centroidY[bRoot],\n\t\t}\n\t}\n\n\t/** The difference of the two clusters' mean render offsets (screen px), or null when it's\n\t * zero \u2014 including always when no offsets were passed. Null routes pricing down the\n\t * offset-unaware path. */\n\toffsetDelta(aRoot: number, bRoot: number): { x: number; y: number } | null {\n\t\tif (this.offsetX === null || this.offsetY === null) return null\n\t\tconst x = this.offsetX[aRoot] / this.counts[aRoot] - this.offsetX[bRoot] / this.counts[bRoot]\n\t\tconst y = this.offsetY[aRoot] / this.counts[aRoot] - this.offsetY[bRoot] / this.counts[bRoot]\n\t\tif (x === 0 && y === 0) return null\n\t\treturn { x, y }\n\t}\n\n\tunionBboxDiag(aRoot: number, bRoot: number): number {\n\t\tconst minX = Math.min(this.minX[aRoot], this.minX[bRoot])\n\t\tconst minY = Math.min(this.minY[aRoot], this.minY[bRoot])\n\t\tconst maxX = Math.max(this.maxX[aRoot], this.maxX[bRoot])\n\t\tconst maxY = Math.max(this.maxY[aRoot], this.maxY[bRoot])\n\t\treturn Math.hypot(maxX - minX, maxY - minY)\n\t}\n\n\tmerge(aRoot: number, bRoot: number, z: number): RawMergeEvent {\n\t\tconst leftRoot = this.minMemberIds[aRoot] < this.minMemberIds[bRoot] ? aRoot : bRoot\n\t\tconst rightRoot = leftRoot === aRoot ? bRoot : aRoot\n\t\tconst left = this.nodes[leftRoot]\n\t\tconst right = this.nodes[rightRoot]\n\t\tconst count = this.counts[leftRoot] + this.counts[rightRoot]\n\t\tconst members = mergeSortedMembers(this.memberLists[leftRoot], this.memberLists[rightRoot])\n\n\t\tconst minX = Math.min(this.minX[leftRoot], this.minX[rightRoot])\n\t\tconst minY = Math.min(this.minY[leftRoot], this.minY[rightRoot])\n\t\tconst maxX = Math.max(this.maxX[leftRoot], this.maxX[rightRoot])\n\t\tconst maxY = Math.max(this.maxY[leftRoot], this.maxY[rightRoot])\n\t\tconst centroidX =\n\t\t\t(this.counts[leftRoot] * this.centroidX[leftRoot] +\n\t\t\t\tthis.counts[rightRoot] * this.centroidX[rightRoot]) /\n\t\t\tcount\n\t\tconst centroidY =\n\t\t\t(this.counts[leftRoot] * this.centroidY[leftRoot] +\n\t\t\t\tthis.counts[rightRoot] * this.centroidY[rightRoot]) /\n\t\t\tcount\n\n\t\t// Leaf ids are assumed not to start with `cluster:`; see the step 2 contract.\n\t\tconst result: ClusterNode = {\n\t\t\tid: `cluster:${count}:${members[0]}`,\n\t\t\tcentroid: { x: centroidX, y: centroidY },\n\t\t\tcount,\n\t\t\tmembers,\n\t\t}\n\n\t\tthis.parent[rightRoot] = leftRoot\n\t\tthis.minX[leftRoot] = minX\n\t\tthis.minY[leftRoot] = minY\n\t\tthis.maxX[leftRoot] = maxX\n\t\tthis.maxY[leftRoot] = maxY\n\t\tthis.centroidX[leftRoot] = centroidX\n\t\tthis.centroidY[leftRoot] = centroidY\n\t\tthis.counts[leftRoot] = count\n\t\tthis.nodes[leftRoot] = result\n\t\tthis.memberLists[leftRoot] = members\n\t\tthis.minMemberIds[leftRoot] = members[0]\n\t\tif (this.offsetX !== null && this.offsetY !== null) {\n\t\t\tthis.offsetX[leftRoot] += this.offsetX[rightRoot]\n\t\t\tthis.offsetY[leftRoot] += this.offsetY[rightRoot]\n\t\t}\n\n\t\treturn { z, children: [left, right], result }\n\t}\n}\n\nfunction mergeSortedMembers(a: readonly string[], b: readonly string[]): string[] {\n\tconst out = new Array<string>(a.length + b.length)\n\tlet i = 0\n\tlet j = 0\n\tlet k = 0\n\twhile (i < a.length && j < b.length) {\n\t\tif (a[i] < b[j]) {\n\t\t\tout[k++] = a[i++]\n\t\t} else {\n\t\t\tout[k++] = b[j++]\n\t\t}\n\t}\n\twhile (i < a.length) out[k++] = a[i++]\n\twhile (j < b.length) out[k++] = b[j++]\n\treturn out\n}\n\ninterface HeapEntry {\n\tedgeIndex: number\n\tz: number\n}\n\nclass EdgeMaxHeap {\n\tprivate readonly items: HeapEntry[] = []\n\t// Per-edge normalized (lo, hi) id pair for the z tie-break, precomputed once so comparisons\n\t// allocate nothing. With coincident anchors every edge prices to the same z (+Infinity), so\n\t// the tie-break runs on nearly every comparison of a rebuild \u2014 allocating the pair there\n\t// churned millions of short-lived tuples.\n\tprivate readonly loIds: string[]\n\tprivate readonly hiIds: string[]\n\n\tconstructor(edges: readonly MstEdge[], leaves: readonly LeafInput[]) {\n\t\tconst n = edges.length\n\t\tthis.loIds = new Array(n)\n\t\tthis.hiIds = new Array(n)\n\t\tfor (let i = 0; i < n; i++) {\n\t\t\tconst aId = leaves[edges[i].a].id\n\t\t\tconst bId = leaves[edges[i].b].id\n\t\t\tif (aId < bId) {\n\t\t\t\tthis.loIds[i] = aId\n\t\t\t\tthis.hiIds[i] = bId\n\t\t\t} else {\n\t\t\t\tthis.loIds[i] = bId\n\t\t\t\tthis.hiIds[i] = aId\n\t\t\t}\n\t\t}\n\t}\n\n\tpeek(): HeapEntry | undefined {\n\t\treturn this.items[0]\n\t}\n\n\tpush(entry: HeapEntry) {\n\t\tthis.items.push(entry)\n\t\tthis.siftUp(this.items.length - 1)\n\t}\n\n\tpop(): HeapEntry | undefined {\n\t\tif (this.items.length === 0) return undefined\n\t\tconst first = this.items[0]\n\t\tconst last = this.items.pop()!\n\t\tif (this.items.length > 0) {\n\t\t\tthis.items[0] = last\n\t\t\tthis.siftDown(0)\n\t\t}\n\t\treturn first\n\t}\n\n\thigherPriority(a: HeapEntry, b: HeapEntry): boolean {\n\t\tif (a.z > b.z) return true\n\t\tif (a.z < b.z) return false\n\t\t// Tie-break on the normalized leaf-id pair, ascending.\n\t\tconst aLo = this.loIds[a.edgeIndex]\n\t\tconst bLo = this.loIds[b.edgeIndex]\n\t\tif (aLo !== bLo) return aLo < bLo\n\t\treturn this.hiIds[a.edgeIndex] < this.hiIds[b.edgeIndex]\n\t}\n\n\tprivate siftUp(index: number) {\n\t\twhile (index > 0) {\n\t\t\tconst parent = (index - 1) >> 1\n\t\t\tif (!this.higherPriority(this.items[index], this.items[parent])) break\n\t\t\t;[this.items[index], this.items[parent]] = [this.items[parent], this.items[index]]\n\t\t\tindex = parent\n\t\t}\n\t}\n\n\tprivate siftDown(index: number) {\n\t\twhile (true) {\n\t\t\tconst left = index * 2 + 1\n\t\t\tconst right = left + 1\n\t\t\tlet best = index\n\t\t\tif (left < this.items.length && this.higherPriority(this.items[left], this.items[best])) {\n\t\t\t\tbest = left\n\t\t\t}\n\t\t\tif (right < this.items.length && this.higherPriority(this.items[right], this.items[best])) {\n\t\t\t\tbest = right\n\t\t\t}\n\t\t\tif (best === index) break\n\t\t\t;[this.items[index], this.items[best]] = [this.items[best], this.items[index]]\n\t\t\tindex = best\n\t\t}\n\t}\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["gap", "fit"]
7
7
  }
@@ -15,10 +15,9 @@ class ClusterRuntimeImpl {
15
15
  // time in getVisible().
16
16
  visible = /* @__PURE__ */ new Map();
17
17
  seeded = false;
18
- // Indices (< k) of band events held unmerged by seedFrom carryover. The single cursor can
19
- // only express "merged up to here", but a carried-over partition can be "merged except these"
20
- // — the exceptions live here. Self-draining: an entry leaves via onCamera when the zoom
21
- // crosses its own zMerge (merges) or its zSplit (the split walk retreats past it).
18
+ // Indices (< k) of band events held unmerged by seedFrom carryover. The single cursor can only
19
+ // express "merged up to here", but a carried-over partition can be "merged except these". Self-
20
+ // draining: an entry leaves via onCamera when the zoom crosses its zMerge or its zSplit.
22
21
  suppressed = /* @__PURE__ */ new Set();
23
22
  // Leaves removed from the displayed partition (deleted / popped out / opened). Patches map
24
23
  // each structural node containing a detached leaf to its displayed replacement (or null to
@@ -82,7 +81,7 @@ class ClusterRuntimeImpl {
82
81
  }
83
82
  let changed = false;
84
83
  if (this.suppressed.size > 0) {
85
- for (const i of [...this.suppressed]) {
84
+ for (const i of this.suppressed) {
86
85
  if (zoom <= this.table.events[i].zMerge) {
87
86
  this.suppressed.delete(i);
88
87
  applyEvent(this.visible, this.table.events[i]);
@@ -105,9 +104,18 @@ class ClusterRuntimeImpl {
105
104
  if (changed) this.version++;
106
105
  }
107
106
  detachLeaf(leafId) {
108
- if (this.detached.has(leafId)) return;
109
- if (!this.getLeafById().has(leafId)) return;
110
- this.detached.add(leafId);
107
+ this.detachLeaves([leafId]);
108
+ }
109
+ detachLeaves(leafIds) {
110
+ const leafById = this.getLeafById();
111
+ let changed = false;
112
+ for (const leafId of leafIds) {
113
+ if (this.detached.has(leafId)) continue;
114
+ if (!leafById.has(leafId)) continue;
115
+ this.detached.add(leafId);
116
+ changed = true;
117
+ }
118
+ if (!changed) return;
111
119
  this.rebuildPatches();
112
120
  this.version++;
113
121
  }
@@ -134,10 +142,9 @@ class ClusterRuntimeImpl {
134
142
  }
135
143
  return this.leafById;
136
144
  }
137
- /** Recompute the patch map from the detached set. A node is patched iff it contains a
138
- * detached member; the patch drops those members and recomputes count/centroid, collapsing
139
- * to the surviving leaf node at count 1 and to nothing at count 0. Patched nodes keep their
140
- * structural id, so cursor events keep addressing them. */
145
+ /** Recompute the patch map from the detached set. A node is patched iff it contains a detached
146
+ * member; the patch drops those members and recomputes count/centroid, collapsing to the surviving
147
+ * leaf at count 1 and to nothing at count 0. Patched nodes keep their structural id. */
141
148
  rebuildPatches() {
142
149
  this.patched.clear();
143
150
  const leafById = this.getLeafById();