1
31import type { Presence } from "./types";
32
33export interface CursorPoint {
34 x: number;
35 y: number;
36}
37
38
39export interface SelectionRect {
40 x1: number;
41 y1: number;
42 x2: number;
43 y2: number;
44}
45
46
47export type BlockOffset = number | "end";
48
49
55export type CursorLookup = (blockId: string, offset: BlockOffset) => CursorPoint | null;
56
57export interface ResolvedCursor {
58 peer: Presence;
59 caret: CursorPoint;
60
61 rects: SelectionRect[];
62}
63
64export interface CursorResolution {
65
66 cursors: ResolvedCursor[];
67
68 dropped: number;
69}
70
71
72export function selectionRanges(
73 blockOrder: readonly string[],
74 sel: { fromBlockId: string; fromOffset: number; toBlockId: string; toOffset: number },
75): { blockId: string; from: BlockOffset; to: BlockOffset }[] {
76 const fi = blockOrder.indexOf(sel.fromBlockId);
77 const ti = blockOrder.indexOf(sel.toBlockId);
78 if (fi === -1 || ti === -1) return [];
79 if (fi === ti) {
80 return [
81 {
82 blockId: sel.fromBlockId,
83 from: Math.min(sel.fromOffset, sel.toOffset),
84 to: Math.max(sel.fromOffset, sel.toOffset),
85 },
86 ];
87 }
88 const [lo, hi] = fi < ti ? [fi, ti] : [ti, fi];
89 const out: { blockId: string; from: BlockOffset; to: BlockOffset }[] = [];
90 for (let i = lo; i <= hi; i += 1) {
91 const blockId = blockOrder[i]!;
92 if (i === lo)
93 out.push({
94 blockId,
95 from: fi < ti ? sel.fromOffset : sel.toOffset,
96 to: i === hi ? (fi < ti ? sel.toOffset : sel.fromOffset) : "end",
97 });
98 else if (i === hi) out.push({ blockId, from: 0, to: fi < ti ? sel.toOffset : sel.fromOffset });
99 else out.push({ blockId, from: 0, to: "end" });
100 }
101 return out;
102}
103
104function bbox(a: CursorPoint, b: CursorPoint): SelectionRect {
105 return {
106 x1: Math.min(a.x, b.x),
107 y1: Math.min(a.y, b.y),
108 x2: Math.max(a.x, b.x),
109 y2: Math.max(a.y, b.y),
110 };
111}
112
113export function resolvePresenceCursors(
114 peers: readonly Presence[],
115 blockOrder: readonly string[],
116 lookup: CursorLookup,
117): CursorResolution {
118 const cursors: ResolvedCursor[] = [];
119 let dropped = 0;
120
121 for (const peer of peers) {
122 const cursor = peer.cursor;
123 if (cursor === null || cursor === undefined) continue;
124 if (peer.viewing === false) continue;
125
126 const caret = lookup(cursor.blockId, cursor.offset);
127 if (caret === null) {
128 dropped += 1;
129 continue;
130 }
131
132 const rects: SelectionRect[] = [];
133 if (cursor.selection) {
134 for (const range of selectionRanges(blockOrder, cursor.selection)) {
135 const p1 = lookup(range.blockId, range.from);
136 const p2 = lookup(range.blockId, range.to);
137 if (p1 !== null && p2 !== null) rects.push(bbox(p1, p2));
138
139 }
140 }
141
142 cursors.push({ peer, caret, rects });
143 }
144
145 return { cursors, dropped };
146}
147