import type { VecLike } from 'tldraw' /** * One comment thread's pin, already resolved to a page-space anchor point. * @internal */ export interface LeafInput { /** Unique. Thread id. Uniqueness is a precondition — throw on duplicates. */ id: string /** Page-space coordinates. Must be finite — throw on NaN/Infinity. */ point: VecLike } /** * Constant screen-px render offsets for markers that don't draw exactly on their anchor — * imprecise shape pins, which tuck ~20px into their shape. Keyed by leaf id; leaves absent from * the map render on their anchor. A marker's visual position at zoom z is `z · point + offset`. * Passed as the optional third argument to `computeClusterTable`; when omitted or empty the * algorithm's behavior (and output) is identical to an offset-unaware run. * @internal */ export type LeafScreenOffsets = ReadonlyMap /** An edge of the Euclidean MST over the leaf anchor points. */ export interface MstEdge { /** Index into the input leaves array. Normalized: leaves[a].id < leaves[b].id (lexicographic). */ a: number /** Index into the input leaves array. */ b: number /** Exact Euclidean page-space distance between the two anchors. May be 0 (coincident). */ d: number } /** * A cluster in the merge tree: a leaf (one thread) or a merged group. * @internal */ export interface ClusterNode { /** Leaves: the thread id verbatim. Merged nodes: `cluster:${count}:${minMemberId}`. */ id: string /** Page space; count-weighted mean of all member leaf anchors. */ centroid: VecLike /** Number of member leaves. Leaves = 1. */ count: number /** All member thread ids, sorted lexicographically ascending. */ members: string[] } /** One merge produced by the capped replay, before contraction. */ export interface RawMergeEvent { /** Effective merge threshold zEff = min(Tc/d, Dmax/unionBboxDiag). +Infinity for coincident anchors. */ z: number /** The two clusters consumed, ordered by ascending min-member id. */ children: [ClusterNode, ClusterNode] /** The cluster produced. */ result: ClusterNode } /** A merge event after contraction: possibly multi-way, before hysteresis. */ export interface ContractedEvent { /** Fires (merges) when zoom <= zMerge. May be +Infinity. */ zMerge: number /** Clusters consumed — 2 or more, ordered by ascending min-member id. */ children: ClusterNode[] /** Cluster produced. */ result: ClusterNode } /** * A finalized merge event, ready for the runtime. * @internal */ export interface MergeEvent { zMerge: number /** Reverses (splits) when zoom \>= zSplit. Always \> zMerge. May be +Infinity. */ zSplit: number children: ClusterNode[] result: ClusterNode } /** * The precomputed clustering schedule for one page's comments. * @internal */ export interface ClusterTable { /** Sorted non-increasing by zMerge; satisfies the invariants of CLUSTERING.md §7.6. */ events: readonly MergeEvent[] /** One node per input leaf, in input order. */ leaves: readonly ClusterNode[] } /** @internal */ export interface ClusterOptions { /** Cluster (merge) distance, screen px. Default 22. */ Tc?: number /** Uncluster (split) distance, screen px. Must be \> Tc. Default 1.2 · Tc. */ Tu?: number /** Contraction window ratio. Default 0.7. */ eps?: number /** Max cluster screen extent at birth, screen px. Must be \>= Tc. Default 3.75 · Tc. */ Dmax?: number /** Camera zoom bounds — pass the editor's camera constraints. Required. */ minZoom: number maxZoom: number /** * Zoom by which every cluster has split, no matter how close its members are — including * coincident anchors, which otherwise never split. Merge thresholds are capped at * `maxSplitZoom / (Tu/Tc)` so the hysteresis band keeps its shape below the cap. * Default 6 (600%). */ maxSplitZoom?: number }