@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.
- package/README.md +11 -0
- package/commenting.css +832 -0
- package/dist-cjs/canvas/cluster-input.js +40 -0
- package/dist-cjs/canvas/cluster-input.js.map +7 -0
- package/dist-cjs/canvas/comment-body.js +31 -0
- package/dist-cjs/canvas/comment-body.js.map +7 -0
- package/dist-cjs/canvas/comment-render.js +72 -0
- package/dist-cjs/canvas/comment-render.js.map +7 -0
- package/dist-cjs/canvas/comment-store.js +48 -0
- package/dist-cjs/canvas/comment-store.js.map +7 -0
- package/dist-cjs/canvas/comment-tool.js +160 -0
- package/dist-cjs/canvas/comment-tool.js.map +7 -0
- package/dist-cjs/canvas/comments-filter-menu.js +109 -0
- package/dist-cjs/canvas/comments-filter-menu.js.map +7 -0
- package/dist-cjs/canvas/comments-menu-item.js +42 -0
- package/dist-cjs/canvas/comments-menu-item.js.map +7 -0
- package/dist-cjs/canvas/comments-overflow-menu.js +64 -0
- package/dist-cjs/canvas/comments-overflow-menu.js.map +7 -0
- package/dist-cjs/canvas/comments-overlay.js +1081 -0
- package/dist-cjs/canvas/comments-overlay.js.map +7 -0
- package/dist-cjs/canvas/comments-sidebar.js +134 -0
- package/dist-cjs/canvas/comments-sidebar.js.map +7 -0
- package/dist-cjs/canvas/hooks.js +45 -0
- package/dist-cjs/canvas/hooks.js.map +7 -0
- package/dist-cjs/canvas/license.js +28 -0
- package/dist-cjs/canvas/license.js.map +7 -0
- package/dist-cjs/canvas/options.js +45 -0
- package/dist-cjs/canvas/options.js.map +7 -0
- package/dist-cjs/canvas/region-options.js +49 -0
- package/dist-cjs/canvas/region-options.js.map +7 -0
- package/dist-cjs/canvas/rich-text.js +28 -0
- package/dist-cjs/canvas/rich-text.js.map +7 -0
- package/dist-cjs/canvas/sidebar-filters.js +30 -0
- package/dist-cjs/canvas/sidebar-filters.js.map +7 -0
- package/dist-cjs/canvas/state.js +76 -0
- package/dist-cjs/canvas/state.js.map +7 -0
- package/dist-cjs/canvas/thread-state.js +81 -0
- package/dist-cjs/canvas/thread-state.js.map +7 -0
- package/dist-cjs/clustering/computeClusterTable.js +83 -0
- package/dist-cjs/clustering/computeClusterTable.js.map +7 -0
- package/dist-cjs/clustering/mst.js +117 -0
- package/dist-cjs/clustering/mst.js.map +7 -0
- package/dist-cjs/clustering/replay.js +275 -0
- package/dist-cjs/clustering/replay.js.map +7 -0
- package/dist-cjs/clustering/runtime.js +134 -0
- package/dist-cjs/clustering/runtime.js.map +7 -0
- package/dist-cjs/clustering/schedule.js +170 -0
- package/dist-cjs/clustering/schedule.js.map +7 -0
- package/dist-cjs/clustering/types.js +17 -0
- package/dist-cjs/clustering/types.js.map +7 -0
- package/dist-cjs/index.d.ts +868 -0
- package/dist-cjs/index.js +126 -0
- package/dist-cjs/index.js.map +7 -0
- package/dist-cjs/ui/avatar.js +43 -0
- package/dist-cjs/ui/avatar.js.map +7 -0
- package/dist-cjs/ui/byline.js +35 -0
- package/dist-cjs/ui/byline.js.map +7 -0
- package/dist-cjs/ui/comment-card.js +37 -0
- package/dist-cjs/ui/comment-card.js.map +7 -0
- package/dist-cjs/ui/comment-composer.js +136 -0
- package/dist-cjs/ui/comment-composer.js.map +7 -0
- package/dist-cjs/ui/comment-extensions.js +37 -0
- package/dist-cjs/ui/comment-extensions.js.map +7 -0
- package/dist-cjs/ui/comment-mention.js +42 -0
- package/dist-cjs/ui/comment-mention.js.map +7 -0
- package/dist-cjs/ui/comment-pin.js +29 -0
- package/dist-cjs/ui/comment-pin.js.map +7 -0
- package/dist-cjs/ui/comment-text.js +29 -0
- package/dist-cjs/ui/comment-text.js.map +7 -0
- package/dist-cjs/ui/comment-thread.js +45 -0
- package/dist-cjs/ui/comment-thread.js.map +7 -0
- package/dist-cjs/ui/comments-list.js +106 -0
- package/dist-cjs/ui/comments-list.js.map +7 -0
- package/dist-cjs/ui/count-badge.js +28 -0
- package/dist-cjs/ui/count-badge.js.map +7 -0
- package/dist-cjs/ui/empty-state.js +31 -0
- package/dist-cjs/ui/empty-state.js.map +7 -0
- package/dist-cjs/ui/format-time.js +46 -0
- package/dist-cjs/ui/format-time.js.map +7 -0
- package/dist-cjs/ui/mention-list.js +70 -0
- package/dist-cjs/ui/mention-list.js.map +7 -0
- package/dist-cjs/ui/mention-suggestion.js +206 -0
- package/dist-cjs/ui/mention-suggestion.js.map +7 -0
- package/dist-cjs/ui/mention.js +31 -0
- package/dist-cjs/ui/mention.js.map +7 -0
- package/dist-cjs/ui/reaction.js +31 -0
- package/dist-cjs/ui/reaction.js.map +7 -0
- package/dist-cjs/ui/reactions.js +34 -0
- package/dist-cjs/ui/reactions.js.map +7 -0
- package/dist-cjs/ui/render-markdown.js +63 -0
- package/dist-cjs/ui/render-markdown.js.map +7 -0
- package/dist-cjs/ui/send-button.js +28 -0
- package/dist-cjs/ui/send-button.js.map +7 -0
- package/dist-esm/canvas/cluster-input.mjs +20 -0
- package/dist-esm/canvas/cluster-input.mjs.map +7 -0
- package/dist-esm/canvas/comment-body.mjs +11 -0
- package/dist-esm/canvas/comment-body.mjs.map +7 -0
- package/dist-esm/canvas/comment-render.mjs +52 -0
- package/dist-esm/canvas/comment-render.mjs.map +7 -0
- package/dist-esm/canvas/comment-store.mjs +28 -0
- package/dist-esm/canvas/comment-store.mjs.map +7 -0
- package/dist-esm/canvas/comment-tool.mjs +142 -0
- package/dist-esm/canvas/comment-tool.mjs.map +7 -0
- package/dist-esm/canvas/comments-filter-menu.mjs +99 -0
- package/dist-esm/canvas/comments-filter-menu.mjs.map +7 -0
- package/dist-esm/canvas/comments-menu-item.mjs +22 -0
- package/dist-esm/canvas/comments-menu-item.mjs.map +7 -0
- package/dist-esm/canvas/comments-overflow-menu.mjs +53 -0
- package/dist-esm/canvas/comments-overflow-menu.mjs.map +7 -0
- package/dist-esm/canvas/comments-overlay.mjs +1094 -0
- package/dist-esm/canvas/comments-overlay.mjs.map +7 -0
- package/dist-esm/canvas/comments-sidebar.mjs +120 -0
- package/dist-esm/canvas/comments-sidebar.mjs.map +7 -0
- package/dist-esm/canvas/hooks.mjs +25 -0
- package/dist-esm/canvas/hooks.mjs.map +7 -0
- package/dist-esm/canvas/license.mjs +8 -0
- package/dist-esm/canvas/license.mjs.map +7 -0
- package/dist-esm/canvas/options.mjs +27 -0
- package/dist-esm/canvas/options.mjs.map +7 -0
- package/dist-esm/canvas/region-options.mjs +29 -0
- package/dist-esm/canvas/region-options.mjs.map +7 -0
- package/dist-esm/canvas/rich-text.mjs +8 -0
- package/dist-esm/canvas/rich-text.mjs.map +7 -0
- package/dist-esm/canvas/sidebar-filters.mjs +10 -0
- package/dist-esm/canvas/sidebar-filters.mjs.map +7 -0
- package/dist-esm/canvas/state.mjs +60 -0
- package/dist-esm/canvas/state.mjs.map +7 -0
- package/dist-esm/canvas/thread-state.mjs +61 -0
- package/dist-esm/canvas/thread-state.mjs.map +7 -0
- package/dist-esm/clustering/computeClusterTable.mjs +63 -0
- package/dist-esm/clustering/computeClusterTable.mjs.map +7 -0
- package/dist-esm/clustering/mst.mjs +97 -0
- package/dist-esm/clustering/mst.mjs.map +7 -0
- package/dist-esm/clustering/replay.mjs +255 -0
- package/dist-esm/clustering/replay.mjs.map +7 -0
- package/dist-esm/clustering/runtime.mjs +114 -0
- package/dist-esm/clustering/runtime.mjs.map +7 -0
- package/dist-esm/clustering/schedule.mjs +150 -0
- package/dist-esm/clustering/schedule.mjs.map +7 -0
- package/dist-esm/clustering/types.mjs +1 -0
- package/dist-esm/clustering/types.mjs.map +7 -0
- package/dist-esm/index.d.mts +868 -0
- package/dist-esm/index.mjs +139 -0
- package/dist-esm/index.mjs.map +7 -0
- package/dist-esm/ui/avatar.mjs +23 -0
- package/dist-esm/ui/avatar.mjs.map +7 -0
- package/dist-esm/ui/byline.mjs +15 -0
- package/dist-esm/ui/byline.mjs.map +7 -0
- package/dist-esm/ui/comment-card.mjs +17 -0
- package/dist-esm/ui/comment-card.mjs.map +7 -0
- package/dist-esm/ui/comment-composer.mjs +116 -0
- package/dist-esm/ui/comment-composer.mjs.map +7 -0
- package/dist-esm/ui/comment-extensions.mjs +17 -0
- package/dist-esm/ui/comment-extensions.mjs.map +7 -0
- package/dist-esm/ui/comment-mention.mjs +22 -0
- package/dist-esm/ui/comment-mention.mjs.map +7 -0
- package/dist-esm/ui/comment-pin.mjs +9 -0
- package/dist-esm/ui/comment-pin.mjs.map +7 -0
- package/dist-esm/ui/comment-text.mjs +9 -0
- package/dist-esm/ui/comment-text.mjs.map +7 -0
- package/dist-esm/ui/comment-thread.mjs +25 -0
- package/dist-esm/ui/comment-thread.mjs.map +7 -0
- package/dist-esm/ui/comments-list.mjs +86 -0
- package/dist-esm/ui/comments-list.mjs.map +7 -0
- package/dist-esm/ui/count-badge.mjs +8 -0
- package/dist-esm/ui/count-badge.mjs.map +7 -0
- package/dist-esm/ui/empty-state.mjs +11 -0
- package/dist-esm/ui/empty-state.mjs.map +7 -0
- package/dist-esm/ui/format-time.mjs +26 -0
- package/dist-esm/ui/format-time.mjs.map +7 -0
- package/dist-esm/ui/mention-list.mjs +50 -0
- package/dist-esm/ui/mention-list.mjs.map +7 -0
- package/dist-esm/ui/mention-suggestion.mjs +186 -0
- package/dist-esm/ui/mention-suggestion.mjs.map +7 -0
- package/dist-esm/ui/mention.mjs +11 -0
- package/dist-esm/ui/mention.mjs.map +7 -0
- package/dist-esm/ui/reaction.mjs +11 -0
- package/dist-esm/ui/reaction.mjs.map +7 -0
- package/dist-esm/ui/reactions.mjs +14 -0
- package/dist-esm/ui/reactions.mjs.map +7 -0
- package/dist-esm/ui/render-markdown.mjs +45 -0
- package/dist-esm/ui/render-markdown.mjs.map +7 -0
- package/dist-esm/ui/send-button.mjs +8 -0
- package/dist-esm/ui/send-button.mjs.map +7 -0
- package/package.json +71 -0
- package/src/canvas/canvas.css +148 -0
- package/src/canvas/cluster-input.test.ts +223 -0
- package/src/canvas/cluster-input.ts +29 -0
- package/src/canvas/comment-body.tsx +21 -0
- package/src/canvas/comment-render.test.ts +90 -0
- package/src/canvas/comment-render.ts +92 -0
- package/src/canvas/comment-store.ts +55 -0
- package/src/canvas/comment-tool.tsx +200 -0
- package/src/canvas/comments-filter-menu.tsx +104 -0
- package/src/canvas/comments-menu-item.tsx +24 -0
- package/src/canvas/comments-overflow-menu.tsx +62 -0
- package/src/canvas/comments-overlay.tsx +1460 -0
- package/src/canvas/comments-sidebar.tsx +184 -0
- package/src/canvas/hooks.ts +28 -0
- package/src/canvas/license.ts +15 -0
- package/src/canvas/options.test.ts +127 -0
- package/src/canvas/options.ts +111 -0
- package/src/canvas/region-options.ts +57 -0
- package/src/canvas/rich-text.ts +15 -0
- package/src/canvas/sidebar-filters.ts +25 -0
- package/src/canvas/state.ts +102 -0
- package/src/canvas/thread-state.ts +91 -0
- package/src/clustering/computeClusterTable.test.ts +299 -0
- package/src/clustering/computeClusterTable.ts +82 -0
- package/src/clustering/locality.test.ts +189 -0
- package/src/clustering/mst.test.ts +383 -0
- package/src/clustering/mst.ts +134 -0
- package/src/clustering/replay.test.ts +547 -0
- package/src/clustering/replay.ts +326 -0
- package/src/clustering/runtime.test.ts +596 -0
- package/src/clustering/runtime.ts +153 -0
- package/src/clustering/schedule.test.ts +514 -0
- package/src/clustering/schedule.ts +184 -0
- package/src/clustering/types.ts +102 -0
- package/src/index.ts +96 -0
- package/src/ui/avatar.tsx +31 -0
- package/src/ui/byline.tsx +23 -0
- package/src/ui/comment-card.tsx +32 -0
- package/src/ui/comment-composer.tsx +179 -0
- package/src/ui/comment-extensions.ts +25 -0
- package/src/ui/comment-mention.ts +47 -0
- package/src/ui/comment-pin.tsx +21 -0
- package/src/ui/comment-text.tsx +12 -0
- package/src/ui/comment-thread.tsx +56 -0
- package/src/ui/comments-list.tsx +139 -0
- package/src/ui/comments.css +678 -0
- package/src/ui/count-badge.tsx +9 -0
- package/src/ui/empty-state.tsx +16 -0
- package/src/ui/format-time.ts +29 -0
- package/src/ui/mention-list.tsx +106 -0
- package/src/ui/mention-suggestion.test.ts +18 -0
- package/src/ui/mention-suggestion.tsx +285 -0
- package/src/ui/mention.tsx +9 -0
- package/src/ui/reaction.tsx +16 -0
- package/src/ui/reactions.tsx +15 -0
- package/src/ui/render-markdown.tsx +72 -0
- 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
|
+
}
|