New architecture
This commit is contained in:
88
src-tauri/assets/viewer-src/src/viewport-tracker.ts
Normal file
88
src-tauri/assets/viewer-src/src/viewport-tracker.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* ViewportTracker — dual IntersectionObserver for page visibility detection.
|
||||
*
|
||||
* - visibleSet: pages currently on screen (rootMargin "0px")
|
||||
* - bufferSet: pages within ~2 viewport heights above/below (rootMargin "200% 0px")
|
||||
*
|
||||
* Both observers schedule a single rAF-deferred notification to prevent stale
|
||||
* data when the two observers fire in separate microtasks for the same layout
|
||||
* change (e.g. buffer removes a page before visible has also removed it).
|
||||
*
|
||||
* Hard invariant enforced before every callback: every visible page is also
|
||||
* in the buffer set.
|
||||
*/
|
||||
|
||||
type OnVisibilityChange = (
|
||||
bufferSet: ReadonlySet<number>,
|
||||
visibleSet: ReadonlySet<number>,
|
||||
) => void;
|
||||
|
||||
export class ViewportTracker {
|
||||
private readonly _onChange: OnVisibilityChange;
|
||||
private readonly _visibleSet: Set<number> = new Set();
|
||||
private readonly _bufferSet: Set<number> = new Set();
|
||||
private _visibleObserver: IntersectionObserver | null = null;
|
||||
private _bufferObserver: IntersectionObserver | null = null;
|
||||
private _rafPending: number | null = null;
|
||||
|
||||
constructor(
|
||||
root: HTMLElement,
|
||||
pageWrappers: HTMLElement[],
|
||||
onChange: OnVisibilityChange,
|
||||
) {
|
||||
this._onChange = onChange;
|
||||
this._observe(root, pageWrappers);
|
||||
}
|
||||
|
||||
private _observe(root: HTMLElement, pageWrappers: HTMLElement[]): void {
|
||||
const scheduleNotify = (): void => {
|
||||
if (this._rafPending !== null) return;
|
||||
this._rafPending = requestAnimationFrame(() => {
|
||||
this._rafPending = null;
|
||||
// Hard invariant: every visible page must also be in the buffer.
|
||||
for (const p of this._visibleSet) this._bufferSet.add(p);
|
||||
this._onChange(new Set(this._bufferSet), new Set(this._visibleSet));
|
||||
});
|
||||
};
|
||||
|
||||
this._visibleObserver = new IntersectionObserver(entries => {
|
||||
for (const e of entries) {
|
||||
const page = parseInt((e.target as HTMLElement).dataset["page"]!, 10);
|
||||
if (e.isIntersecting) this._visibleSet.add(page);
|
||||
else this._visibleSet.delete(page);
|
||||
}
|
||||
scheduleNotify();
|
||||
}, { root, rootMargin: "0px", threshold: 0 });
|
||||
|
||||
this._bufferObserver = new IntersectionObserver(entries => {
|
||||
for (const e of entries) {
|
||||
const page = parseInt((e.target as HTMLElement).dataset["page"]!, 10);
|
||||
if (e.isIntersecting) this._bufferSet.add(page);
|
||||
else this._bufferSet.delete(page);
|
||||
}
|
||||
scheduleNotify();
|
||||
}, { root, rootMargin: "200% 0px", threshold: 0 });
|
||||
|
||||
for (const wrap of pageWrappers) {
|
||||
this._visibleObserver.observe(wrap);
|
||||
this._bufferObserver.observe(wrap);
|
||||
}
|
||||
}
|
||||
|
||||
/** Observe a newly-added page wrapper (unused currently, kept for extensibility). */
|
||||
observe(wrap: HTMLElement): void {
|
||||
this._visibleObserver?.observe(wrap);
|
||||
this._bufferObserver?.observe(wrap);
|
||||
}
|
||||
|
||||
disconnect(): void {
|
||||
this._visibleObserver?.disconnect();
|
||||
this._bufferObserver?.disconnect();
|
||||
this._visibleSet.clear();
|
||||
this._bufferSet.clear();
|
||||
if (this._rafPending !== null) {
|
||||
cancelAnimationFrame(this._rafPending);
|
||||
this._rafPending = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user