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