@tldraw/commenting 0.0.0-bootstrap

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 (242) hide show
  1. package/README.md +11 -0
  2. package/commenting.css +832 -0
  3. package/dist-cjs/canvas/cluster-input.js +40 -0
  4. package/dist-cjs/canvas/cluster-input.js.map +7 -0
  5. package/dist-cjs/canvas/comment-body.js +31 -0
  6. package/dist-cjs/canvas/comment-body.js.map +7 -0
  7. package/dist-cjs/canvas/comment-render.js +72 -0
  8. package/dist-cjs/canvas/comment-render.js.map +7 -0
  9. package/dist-cjs/canvas/comment-store.js +48 -0
  10. package/dist-cjs/canvas/comment-store.js.map +7 -0
  11. package/dist-cjs/canvas/comment-tool.js +160 -0
  12. package/dist-cjs/canvas/comment-tool.js.map +7 -0
  13. package/dist-cjs/canvas/comments-filter-menu.js +109 -0
  14. package/dist-cjs/canvas/comments-filter-menu.js.map +7 -0
  15. package/dist-cjs/canvas/comments-menu-item.js +42 -0
  16. package/dist-cjs/canvas/comments-menu-item.js.map +7 -0
  17. package/dist-cjs/canvas/comments-overflow-menu.js +64 -0
  18. package/dist-cjs/canvas/comments-overflow-menu.js.map +7 -0
  19. package/dist-cjs/canvas/comments-overlay.js +1081 -0
  20. package/dist-cjs/canvas/comments-overlay.js.map +7 -0
  21. package/dist-cjs/canvas/comments-sidebar.js +134 -0
  22. package/dist-cjs/canvas/comments-sidebar.js.map +7 -0
  23. package/dist-cjs/canvas/hooks.js +45 -0
  24. package/dist-cjs/canvas/hooks.js.map +7 -0
  25. package/dist-cjs/canvas/license.js +28 -0
  26. package/dist-cjs/canvas/license.js.map +7 -0
  27. package/dist-cjs/canvas/options.js +45 -0
  28. package/dist-cjs/canvas/options.js.map +7 -0
  29. package/dist-cjs/canvas/region-options.js +49 -0
  30. package/dist-cjs/canvas/region-options.js.map +7 -0
  31. package/dist-cjs/canvas/rich-text.js +28 -0
  32. package/dist-cjs/canvas/rich-text.js.map +7 -0
  33. package/dist-cjs/canvas/sidebar-filters.js +30 -0
  34. package/dist-cjs/canvas/sidebar-filters.js.map +7 -0
  35. package/dist-cjs/canvas/state.js +76 -0
  36. package/dist-cjs/canvas/state.js.map +7 -0
  37. package/dist-cjs/canvas/thread-state.js +81 -0
  38. package/dist-cjs/canvas/thread-state.js.map +7 -0
  39. package/dist-cjs/clustering/computeClusterTable.js +83 -0
  40. package/dist-cjs/clustering/computeClusterTable.js.map +7 -0
  41. package/dist-cjs/clustering/mst.js +117 -0
  42. package/dist-cjs/clustering/mst.js.map +7 -0
  43. package/dist-cjs/clustering/replay.js +275 -0
  44. package/dist-cjs/clustering/replay.js.map +7 -0
  45. package/dist-cjs/clustering/runtime.js +134 -0
  46. package/dist-cjs/clustering/runtime.js.map +7 -0
  47. package/dist-cjs/clustering/schedule.js +170 -0
  48. package/dist-cjs/clustering/schedule.js.map +7 -0
  49. package/dist-cjs/clustering/types.js +17 -0
  50. package/dist-cjs/clustering/types.js.map +7 -0
  51. package/dist-cjs/index.d.ts +868 -0
  52. package/dist-cjs/index.js +126 -0
  53. package/dist-cjs/index.js.map +7 -0
  54. package/dist-cjs/ui/avatar.js +43 -0
  55. package/dist-cjs/ui/avatar.js.map +7 -0
  56. package/dist-cjs/ui/byline.js +35 -0
  57. package/dist-cjs/ui/byline.js.map +7 -0
  58. package/dist-cjs/ui/comment-card.js +37 -0
  59. package/dist-cjs/ui/comment-card.js.map +7 -0
  60. package/dist-cjs/ui/comment-composer.js +136 -0
  61. package/dist-cjs/ui/comment-composer.js.map +7 -0
  62. package/dist-cjs/ui/comment-extensions.js +37 -0
  63. package/dist-cjs/ui/comment-extensions.js.map +7 -0
  64. package/dist-cjs/ui/comment-mention.js +42 -0
  65. package/dist-cjs/ui/comment-mention.js.map +7 -0
  66. package/dist-cjs/ui/comment-pin.js +29 -0
  67. package/dist-cjs/ui/comment-pin.js.map +7 -0
  68. package/dist-cjs/ui/comment-text.js +29 -0
  69. package/dist-cjs/ui/comment-text.js.map +7 -0
  70. package/dist-cjs/ui/comment-thread.js +45 -0
  71. package/dist-cjs/ui/comment-thread.js.map +7 -0
  72. package/dist-cjs/ui/comments-list.js +106 -0
  73. package/dist-cjs/ui/comments-list.js.map +7 -0
  74. package/dist-cjs/ui/count-badge.js +28 -0
  75. package/dist-cjs/ui/count-badge.js.map +7 -0
  76. package/dist-cjs/ui/empty-state.js +31 -0
  77. package/dist-cjs/ui/empty-state.js.map +7 -0
  78. package/dist-cjs/ui/format-time.js +46 -0
  79. package/dist-cjs/ui/format-time.js.map +7 -0
  80. package/dist-cjs/ui/mention-list.js +70 -0
  81. package/dist-cjs/ui/mention-list.js.map +7 -0
  82. package/dist-cjs/ui/mention-suggestion.js +206 -0
  83. package/dist-cjs/ui/mention-suggestion.js.map +7 -0
  84. package/dist-cjs/ui/mention.js +31 -0
  85. package/dist-cjs/ui/mention.js.map +7 -0
  86. package/dist-cjs/ui/reaction.js +31 -0
  87. package/dist-cjs/ui/reaction.js.map +7 -0
  88. package/dist-cjs/ui/reactions.js +34 -0
  89. package/dist-cjs/ui/reactions.js.map +7 -0
  90. package/dist-cjs/ui/render-markdown.js +63 -0
  91. package/dist-cjs/ui/render-markdown.js.map +7 -0
  92. package/dist-cjs/ui/send-button.js +28 -0
  93. package/dist-cjs/ui/send-button.js.map +7 -0
  94. package/dist-esm/canvas/cluster-input.mjs +20 -0
  95. package/dist-esm/canvas/cluster-input.mjs.map +7 -0
  96. package/dist-esm/canvas/comment-body.mjs +11 -0
  97. package/dist-esm/canvas/comment-body.mjs.map +7 -0
  98. package/dist-esm/canvas/comment-render.mjs +52 -0
  99. package/dist-esm/canvas/comment-render.mjs.map +7 -0
  100. package/dist-esm/canvas/comment-store.mjs +28 -0
  101. package/dist-esm/canvas/comment-store.mjs.map +7 -0
  102. package/dist-esm/canvas/comment-tool.mjs +142 -0
  103. package/dist-esm/canvas/comment-tool.mjs.map +7 -0
  104. package/dist-esm/canvas/comments-filter-menu.mjs +99 -0
  105. package/dist-esm/canvas/comments-filter-menu.mjs.map +7 -0
  106. package/dist-esm/canvas/comments-menu-item.mjs +22 -0
  107. package/dist-esm/canvas/comments-menu-item.mjs.map +7 -0
  108. package/dist-esm/canvas/comments-overflow-menu.mjs +53 -0
  109. package/dist-esm/canvas/comments-overflow-menu.mjs.map +7 -0
  110. package/dist-esm/canvas/comments-overlay.mjs +1094 -0
  111. package/dist-esm/canvas/comments-overlay.mjs.map +7 -0
  112. package/dist-esm/canvas/comments-sidebar.mjs +120 -0
  113. package/dist-esm/canvas/comments-sidebar.mjs.map +7 -0
  114. package/dist-esm/canvas/hooks.mjs +25 -0
  115. package/dist-esm/canvas/hooks.mjs.map +7 -0
  116. package/dist-esm/canvas/license.mjs +8 -0
  117. package/dist-esm/canvas/license.mjs.map +7 -0
  118. package/dist-esm/canvas/options.mjs +27 -0
  119. package/dist-esm/canvas/options.mjs.map +7 -0
  120. package/dist-esm/canvas/region-options.mjs +29 -0
  121. package/dist-esm/canvas/region-options.mjs.map +7 -0
  122. package/dist-esm/canvas/rich-text.mjs +8 -0
  123. package/dist-esm/canvas/rich-text.mjs.map +7 -0
  124. package/dist-esm/canvas/sidebar-filters.mjs +10 -0
  125. package/dist-esm/canvas/sidebar-filters.mjs.map +7 -0
  126. package/dist-esm/canvas/state.mjs +60 -0
  127. package/dist-esm/canvas/state.mjs.map +7 -0
  128. package/dist-esm/canvas/thread-state.mjs +61 -0
  129. package/dist-esm/canvas/thread-state.mjs.map +7 -0
  130. package/dist-esm/clustering/computeClusterTable.mjs +63 -0
  131. package/dist-esm/clustering/computeClusterTable.mjs.map +7 -0
  132. package/dist-esm/clustering/mst.mjs +97 -0
  133. package/dist-esm/clustering/mst.mjs.map +7 -0
  134. package/dist-esm/clustering/replay.mjs +255 -0
  135. package/dist-esm/clustering/replay.mjs.map +7 -0
  136. package/dist-esm/clustering/runtime.mjs +114 -0
  137. package/dist-esm/clustering/runtime.mjs.map +7 -0
  138. package/dist-esm/clustering/schedule.mjs +150 -0
  139. package/dist-esm/clustering/schedule.mjs.map +7 -0
  140. package/dist-esm/clustering/types.mjs +1 -0
  141. package/dist-esm/clustering/types.mjs.map +7 -0
  142. package/dist-esm/index.d.mts +868 -0
  143. package/dist-esm/index.mjs +139 -0
  144. package/dist-esm/index.mjs.map +7 -0
  145. package/dist-esm/ui/avatar.mjs +23 -0
  146. package/dist-esm/ui/avatar.mjs.map +7 -0
  147. package/dist-esm/ui/byline.mjs +15 -0
  148. package/dist-esm/ui/byline.mjs.map +7 -0
  149. package/dist-esm/ui/comment-card.mjs +17 -0
  150. package/dist-esm/ui/comment-card.mjs.map +7 -0
  151. package/dist-esm/ui/comment-composer.mjs +116 -0
  152. package/dist-esm/ui/comment-composer.mjs.map +7 -0
  153. package/dist-esm/ui/comment-extensions.mjs +17 -0
  154. package/dist-esm/ui/comment-extensions.mjs.map +7 -0
  155. package/dist-esm/ui/comment-mention.mjs +22 -0
  156. package/dist-esm/ui/comment-mention.mjs.map +7 -0
  157. package/dist-esm/ui/comment-pin.mjs +9 -0
  158. package/dist-esm/ui/comment-pin.mjs.map +7 -0
  159. package/dist-esm/ui/comment-text.mjs +9 -0
  160. package/dist-esm/ui/comment-text.mjs.map +7 -0
  161. package/dist-esm/ui/comment-thread.mjs +25 -0
  162. package/dist-esm/ui/comment-thread.mjs.map +7 -0
  163. package/dist-esm/ui/comments-list.mjs +86 -0
  164. package/dist-esm/ui/comments-list.mjs.map +7 -0
  165. package/dist-esm/ui/count-badge.mjs +8 -0
  166. package/dist-esm/ui/count-badge.mjs.map +7 -0
  167. package/dist-esm/ui/empty-state.mjs +11 -0
  168. package/dist-esm/ui/empty-state.mjs.map +7 -0
  169. package/dist-esm/ui/format-time.mjs +26 -0
  170. package/dist-esm/ui/format-time.mjs.map +7 -0
  171. package/dist-esm/ui/mention-list.mjs +50 -0
  172. package/dist-esm/ui/mention-list.mjs.map +7 -0
  173. package/dist-esm/ui/mention-suggestion.mjs +186 -0
  174. package/dist-esm/ui/mention-suggestion.mjs.map +7 -0
  175. package/dist-esm/ui/mention.mjs +11 -0
  176. package/dist-esm/ui/mention.mjs.map +7 -0
  177. package/dist-esm/ui/reaction.mjs +11 -0
  178. package/dist-esm/ui/reaction.mjs.map +7 -0
  179. package/dist-esm/ui/reactions.mjs +14 -0
  180. package/dist-esm/ui/reactions.mjs.map +7 -0
  181. package/dist-esm/ui/render-markdown.mjs +45 -0
  182. package/dist-esm/ui/render-markdown.mjs.map +7 -0
  183. package/dist-esm/ui/send-button.mjs +8 -0
  184. package/dist-esm/ui/send-button.mjs.map +7 -0
  185. package/package.json +71 -0
  186. package/src/canvas/canvas.css +148 -0
  187. package/src/canvas/cluster-input.test.ts +223 -0
  188. package/src/canvas/cluster-input.ts +29 -0
  189. package/src/canvas/comment-body.tsx +21 -0
  190. package/src/canvas/comment-render.test.ts +90 -0
  191. package/src/canvas/comment-render.ts +92 -0
  192. package/src/canvas/comment-store.ts +55 -0
  193. package/src/canvas/comment-tool.tsx +200 -0
  194. package/src/canvas/comments-filter-menu.tsx +104 -0
  195. package/src/canvas/comments-menu-item.tsx +24 -0
  196. package/src/canvas/comments-overflow-menu.tsx +62 -0
  197. package/src/canvas/comments-overlay.tsx +1460 -0
  198. package/src/canvas/comments-sidebar.tsx +184 -0
  199. package/src/canvas/hooks.ts +28 -0
  200. package/src/canvas/license.ts +15 -0
  201. package/src/canvas/options.test.ts +127 -0
  202. package/src/canvas/options.ts +111 -0
  203. package/src/canvas/region-options.ts +57 -0
  204. package/src/canvas/rich-text.ts +15 -0
  205. package/src/canvas/sidebar-filters.ts +25 -0
  206. package/src/canvas/state.ts +102 -0
  207. package/src/canvas/thread-state.ts +91 -0
  208. package/src/clustering/computeClusterTable.test.ts +299 -0
  209. package/src/clustering/computeClusterTable.ts +82 -0
  210. package/src/clustering/locality.test.ts +189 -0
  211. package/src/clustering/mst.test.ts +383 -0
  212. package/src/clustering/mst.ts +134 -0
  213. package/src/clustering/replay.test.ts +547 -0
  214. package/src/clustering/replay.ts +326 -0
  215. package/src/clustering/runtime.test.ts +596 -0
  216. package/src/clustering/runtime.ts +153 -0
  217. package/src/clustering/schedule.test.ts +514 -0
  218. package/src/clustering/schedule.ts +184 -0
  219. package/src/clustering/types.ts +102 -0
  220. package/src/index.ts +96 -0
  221. package/src/ui/avatar.tsx +31 -0
  222. package/src/ui/byline.tsx +23 -0
  223. package/src/ui/comment-card.tsx +32 -0
  224. package/src/ui/comment-composer.tsx +179 -0
  225. package/src/ui/comment-extensions.ts +25 -0
  226. package/src/ui/comment-mention.ts +47 -0
  227. package/src/ui/comment-pin.tsx +21 -0
  228. package/src/ui/comment-text.tsx +12 -0
  229. package/src/ui/comment-thread.tsx +56 -0
  230. package/src/ui/comments-list.tsx +139 -0
  231. package/src/ui/comments.css +678 -0
  232. package/src/ui/count-badge.tsx +9 -0
  233. package/src/ui/empty-state.tsx +16 -0
  234. package/src/ui/format-time.ts +29 -0
  235. package/src/ui/mention-list.tsx +106 -0
  236. package/src/ui/mention-suggestion.test.ts +18 -0
  237. package/src/ui/mention-suggestion.tsx +285 -0
  238. package/src/ui/mention.tsx +9 -0
  239. package/src/ui/reaction.tsx +16 -0
  240. package/src/ui/reactions.tsx +15 -0
  241. package/src/ui/render-markdown.tsx +72 -0
  242. package/src/ui/send-button.tsx +15 -0
@@ -0,0 +1,326 @@
1
+ import type { ClusterNode, LeafInput, MstEdge, RawMergeEvent } from './types'
2
+
3
+ export const D_FLOOR = 1e-9
4
+
5
+ export function cappedReplay(
6
+ leaves: readonly LeafInput[],
7
+ edges: readonly MstEdge[],
8
+ opts: { Tc: number; Dmax: number }
9
+ ): RawMergeEvent[] {
10
+ validateOptions(opts)
11
+
12
+ if (edges.length === 0) return []
13
+
14
+ const clusters = new ClusterState(leaves)
15
+ const heap = new EdgeMaxHeap(edges, leaves)
16
+ // Edges incident to each current cluster root, for eager repricing: a merge
17
+ // moves the result's centroid, which can RAISE an incident edge's key (the
18
+ // centroid may move toward a neighbor), so the lazy pop-time recheck alone
19
+ // is not enough — every price change pushes a fresh heap entry.
20
+ const incident: number[][] = leaves.map(() => [])
21
+ for (let i = 0; i < edges.length; i++) {
22
+ heap.push({ edgeIndex: i, z: zForEdge(edges[i], clusters, opts) })
23
+ incident[edges[i].a].push(i)
24
+ incident[edges[i].b].push(i)
25
+ }
26
+
27
+ const events: RawMergeEvent[] = []
28
+ // Emitted thresholds are clamped non-increasing: with centroid pricing a
29
+ // later merge's raw price can in principle exceed an earlier one's, and the
30
+ // table's descending sort is load-bearing for the runtime cursor.
31
+ let lastZ = Number.POSITIVE_INFINITY
32
+ while (events.length < edges.length) {
33
+ const entry = heap.pop()
34
+ if (!entry) break
35
+
36
+ const edge = edges[entry.edgeIndex]
37
+ const aRoot = clusters.find(edge.a)
38
+ const bRoot = clusters.find(edge.b)
39
+ if (aRoot === bRoot) continue // stale duplicate of an already-fired edge
40
+
41
+ const current = { edgeIndex: entry.edgeIndex, z: zForRoots(edge, aRoot, bRoot, clusters, opts) }
42
+ const next = heap.peek()
43
+ if (next && heap.higherPriority(next, current)) {
44
+ heap.push(current)
45
+ continue
46
+ }
47
+
48
+ const z = Math.min(current.z, lastZ)
49
+ lastZ = z
50
+ events.push(clusters.merge(aRoot, bRoot, z))
51
+
52
+ // eager reprice: the new cluster's centroid and bbox changed, so every
53
+ // surviving incident edge gets a fresh entry at its current price
54
+ const root = clusters.find(edge.a)
55
+ const other = root === aRoot ? bRoot : aRoot
56
+ const survivors: number[] = []
57
+ for (const idx of [...incident[root], ...incident[other]]) {
58
+ const e2 = edges[idx]
59
+ const ra = clusters.find(e2.a)
60
+ const rb = clusters.find(e2.b)
61
+ if (ra === rb) continue
62
+ survivors.push(idx)
63
+ heap.push({ edgeIndex: idx, z: zForRoots(e2, ra, rb, clusters, opts) })
64
+ }
65
+ incident[root] = survivors
66
+ }
67
+
68
+ return events
69
+ }
70
+
71
+ function validateOptions(opts: { Tc: number; Dmax: number }) {
72
+ if (!Number.isFinite(opts.Tc) || opts.Tc <= 0) {
73
+ throw new Error('Tc must be greater than 0')
74
+ }
75
+ if (!Number.isFinite(opts.Dmax) || opts.Dmax <= 0) {
76
+ throw new Error('Dmax must be greater than 0')
77
+ }
78
+ if (opts.Dmax < opts.Tc) {
79
+ throw new Error('Dmax must be greater than or equal to Tc')
80
+ }
81
+ }
82
+
83
+ function zForEdge(
84
+ edge: MstEdge,
85
+ clusters: ClusterState,
86
+ opts: { Tc: number; Dmax: number }
87
+ ): number {
88
+ return zForRoots(edge, clusters.find(edge.a), clusters.find(edge.b), clusters, opts)
89
+ }
90
+
91
+ function zForRoots(
92
+ edge: MstEdge,
93
+ aRoot: number,
94
+ bRoot: number,
95
+ clusters: ClusterState,
96
+ opts: { Tc: number; Dmax: number }
97
+ ): number {
98
+ if (edge.d < D_FLOOR) return Number.POSITIVE_INFINITY
99
+ // Badge-anchored gap pricing: clusters render as badges at their centroids,
100
+ // so the merge is priced by the distance between the rendered centers, not
101
+ // the nearest members. For leaves the two are identical; for clusters the
102
+ // centroid distance is larger, so groups merge later than their closest
103
+ // members would suggest — matching what the user actually sees.
104
+ // (Coincident CENTROIDS with non-coincident members leave the gap term
105
+ // Infinity and the fit term finite — the min stays finite, no special case.)
106
+ const gap = opts.Tc / clusters.centroidDistance(aRoot, bRoot)
107
+ const fit = opts.Dmax / clusters.unionBboxDiag(aRoot, bRoot)
108
+ return Math.min(gap, fit)
109
+ }
110
+
111
+ class ClusterState {
112
+ private readonly parent: Int32Array
113
+ private readonly minX: Float64Array
114
+ private readonly minY: Float64Array
115
+ private readonly maxX: Float64Array
116
+ private readonly maxY: Float64Array
117
+ private readonly centroidX: Float64Array
118
+ private readonly centroidY: Float64Array
119
+ private readonly counts: Int32Array
120
+ private readonly nodes: ClusterNode[]
121
+ private readonly memberLists: string[][]
122
+ private readonly minMemberIds: string[]
123
+
124
+ constructor(leaves: readonly LeafInput[]) {
125
+ const n = leaves.length
126
+ this.parent = new Int32Array(n)
127
+ this.minX = new Float64Array(n)
128
+ this.minY = new Float64Array(n)
129
+ this.maxX = new Float64Array(n)
130
+ this.maxY = new Float64Array(n)
131
+ this.centroidX = new Float64Array(n)
132
+ this.centroidY = new Float64Array(n)
133
+ this.counts = new Int32Array(n)
134
+ this.nodes = new Array(n)
135
+ this.memberLists = new Array(n)
136
+ this.minMemberIds = new Array(n)
137
+
138
+ for (let i = 0; i < n; i++) {
139
+ const leaf = leaves[i]
140
+ this.parent[i] = i
141
+ this.minX[i] = leaf.point.x
142
+ this.minY[i] = leaf.point.y
143
+ this.maxX[i] = leaf.point.x
144
+ this.maxY[i] = leaf.point.y
145
+ this.centroidX[i] = leaf.point.x
146
+ this.centroidY[i] = leaf.point.y
147
+ this.counts[i] = 1
148
+ this.memberLists[i] = [leaf.id]
149
+ this.minMemberIds[i] = leaf.id
150
+ this.nodes[i] = {
151
+ id: leaf.id,
152
+ centroid: { x: leaf.point.x, y: leaf.point.y },
153
+ count: 1,
154
+ members: [leaf.id],
155
+ }
156
+ }
157
+ }
158
+
159
+ find(index: number): number {
160
+ let root = index
161
+ while (this.parent[root] !== root) {
162
+ root = this.parent[root]
163
+ }
164
+ while (this.parent[index] !== index) {
165
+ const next = this.parent[index]
166
+ this.parent[index] = root
167
+ index = next
168
+ }
169
+ return root
170
+ }
171
+
172
+ centroidDistance(aRoot: number, bRoot: number): number {
173
+ return Math.hypot(
174
+ this.centroidX[aRoot] - this.centroidX[bRoot],
175
+ this.centroidY[aRoot] - this.centroidY[bRoot]
176
+ )
177
+ }
178
+
179
+ unionBboxDiag(aRoot: number, bRoot: number): number {
180
+ const minX = Math.min(this.minX[aRoot], this.minX[bRoot])
181
+ const minY = Math.min(this.minY[aRoot], this.minY[bRoot])
182
+ const maxX = Math.max(this.maxX[aRoot], this.maxX[bRoot])
183
+ const maxY = Math.max(this.maxY[aRoot], this.maxY[bRoot])
184
+ return Math.hypot(maxX - minX, maxY - minY)
185
+ }
186
+
187
+ merge(aRoot: number, bRoot: number, z: number): RawMergeEvent {
188
+ const leftRoot = this.minMemberIds[aRoot] < this.minMemberIds[bRoot] ? aRoot : bRoot
189
+ const rightRoot = leftRoot === aRoot ? bRoot : aRoot
190
+ const left = this.nodes[leftRoot]
191
+ const right = this.nodes[rightRoot]
192
+ const count = this.counts[leftRoot] + this.counts[rightRoot]
193
+ const members = mergeSortedMembers(this.memberLists[leftRoot], this.memberLists[rightRoot])
194
+
195
+ const minX = Math.min(this.minX[leftRoot], this.minX[rightRoot])
196
+ const minY = Math.min(this.minY[leftRoot], this.minY[rightRoot])
197
+ const maxX = Math.max(this.maxX[leftRoot], this.maxX[rightRoot])
198
+ const maxY = Math.max(this.maxY[leftRoot], this.maxY[rightRoot])
199
+ const centroidX =
200
+ (this.counts[leftRoot] * this.centroidX[leftRoot] +
201
+ this.counts[rightRoot] * this.centroidX[rightRoot]) /
202
+ count
203
+ const centroidY =
204
+ (this.counts[leftRoot] * this.centroidY[leftRoot] +
205
+ this.counts[rightRoot] * this.centroidY[rightRoot]) /
206
+ count
207
+
208
+ // Leaf ids are assumed not to start with `cluster:`; see the step 2 contract.
209
+ const result: ClusterNode = {
210
+ id: `cluster:${count}:${members[0]}`,
211
+ centroid: { x: centroidX, y: centroidY },
212
+ count,
213
+ members,
214
+ }
215
+
216
+ this.parent[rightRoot] = leftRoot
217
+ this.minX[leftRoot] = minX
218
+ this.minY[leftRoot] = minY
219
+ this.maxX[leftRoot] = maxX
220
+ this.maxY[leftRoot] = maxY
221
+ this.centroidX[leftRoot] = centroidX
222
+ this.centroidY[leftRoot] = centroidY
223
+ this.counts[leftRoot] = count
224
+ this.nodes[leftRoot] = result
225
+ this.memberLists[leftRoot] = members
226
+ this.minMemberIds[leftRoot] = members[0]
227
+
228
+ return { z, children: [left, right], result }
229
+ }
230
+ }
231
+
232
+ function mergeSortedMembers(a: readonly string[], b: readonly string[]): string[] {
233
+ const out = new Array<string>(a.length + b.length)
234
+ let i = 0
235
+ let j = 0
236
+ let k = 0
237
+ while (i < a.length && j < b.length) {
238
+ if (a[i] < b[j]) {
239
+ out[k++] = a[i++]
240
+ } else {
241
+ out[k++] = b[j++]
242
+ }
243
+ }
244
+ while (i < a.length) out[k++] = a[i++]
245
+ while (j < b.length) out[k++] = b[j++]
246
+ return out
247
+ }
248
+
249
+ interface HeapEntry {
250
+ edgeIndex: number
251
+ z: number
252
+ }
253
+
254
+ class EdgeMaxHeap {
255
+ private readonly items: HeapEntry[] = []
256
+
257
+ constructor(
258
+ private readonly edges: readonly MstEdge[],
259
+ private readonly leaves: readonly LeafInput[]
260
+ ) {}
261
+
262
+ peek(): HeapEntry | undefined {
263
+ return this.items[0]
264
+ }
265
+
266
+ push(entry: HeapEntry) {
267
+ this.items.push(entry)
268
+ this.siftUp(this.items.length - 1)
269
+ }
270
+
271
+ pop(): HeapEntry | undefined {
272
+ if (this.items.length === 0) return undefined
273
+ const first = this.items[0]
274
+ const last = this.items.pop()!
275
+ if (this.items.length > 0) {
276
+ this.items[0] = last
277
+ this.siftDown(0)
278
+ }
279
+ return first
280
+ }
281
+
282
+ higherPriority(a: HeapEntry, b: HeapEntry): boolean {
283
+ if (a.z > b.z) return true
284
+ if (a.z < b.z) return false
285
+ return edgeIdPairLess(this.edges[a.edgeIndex], this.edges[b.edgeIndex], this.leaves)
286
+ }
287
+
288
+ private siftUp(index: number) {
289
+ while (index > 0) {
290
+ const parent = (index - 1) >> 1
291
+ if (!this.higherPriority(this.items[index], this.items[parent])) break
292
+ ;[this.items[index], this.items[parent]] = [this.items[parent], this.items[index]]
293
+ index = parent
294
+ }
295
+ }
296
+
297
+ private siftDown(index: number) {
298
+ while (true) {
299
+ const left = index * 2 + 1
300
+ const right = left + 1
301
+ let best = index
302
+ if (left < this.items.length && this.higherPriority(this.items[left], this.items[best])) {
303
+ best = left
304
+ }
305
+ if (right < this.items.length && this.higherPriority(this.items[right], this.items[best])) {
306
+ best = right
307
+ }
308
+ if (best === index) break
309
+ ;[this.items[index], this.items[best]] = [this.items[best], this.items[index]]
310
+ index = best
311
+ }
312
+ }
313
+ }
314
+
315
+ function edgeIdPairLess(a: MstEdge, b: MstEdge, leaves: readonly LeafInput[]): boolean {
316
+ const [aLo, aHi] = normalizedEdgeIds(a, leaves)
317
+ const [bLo, bHi] = normalizedEdgeIds(b, leaves)
318
+ if (aLo !== bLo) return aLo < bLo
319
+ return aHi < bHi
320
+ }
321
+
322
+ function normalizedEdgeIds(edge: MstEdge, leaves: readonly LeafInput[]): [string, string] {
323
+ const aId = leaves[edge.a].id
324
+ const bId = leaves[edge.b].id
325
+ return aId < bId ? [aId, bId] : [bId, aId]
326
+ }