Replace js by ts
This commit is contained in:
361
src-tauri/dist/page-manager-enhanced.js
vendored
Normal file
361
src-tauri/dist/page-manager-enhanced.js
vendored
Normal file
@@ -0,0 +1,361 @@
|
||||
/**
|
||||
* page-manager-enhanced.ts — Enhanced PageManager with performance optimizations
|
||||
*
|
||||
* Extends the original PageManager with:
|
||||
* - Better integration with render system
|
||||
* - Adaptive quality support
|
||||
* - Memory-efficient canvas management
|
||||
* - Smooth transitions during quality changes
|
||||
*/
|
||||
const MAX_CANVAS_PIXELS = 16777216; // 4096 × 4096
|
||||
/**
|
||||
* Clamp render scale so the canvas pixel count stays within the budget.
|
||||
* CSS dimensions stay correct — pages appear at the right size, just at
|
||||
* reduced effective DPI when zoomed very high.
|
||||
*/
|
||||
function clampedRenderScale(vpWidth, vpHeight, desiredScale, dpr, quality = 1.0) {
|
||||
const effectiveScale = desiredScale * quality;
|
||||
const w = vpWidth * effectiveScale * dpr;
|
||||
const h = vpHeight * effectiveScale * dpr;
|
||||
const pixels = w * h;
|
||||
if (pixels <= MAX_CANVAS_PIXELS)
|
||||
return effectiveScale * dpr;
|
||||
return effectiveScale * dpr * Math.sqrt(MAX_CANVAS_PIXELS / pixels);
|
||||
}
|
||||
const State = Object.freeze({
|
||||
PLACEHOLDER: 0,
|
||||
RENDERING: 1,
|
||||
RENDERED: 2,
|
||||
QUALITY_ADJUSTING: 3 // New state for quality transitions
|
||||
});
|
||||
export class EnhancedPageManager {
|
||||
/**
|
||||
* @param wrapper - #pages-wrapper container (already in DOM)
|
||||
* @param viewports - scale=1 viewports (0-indexed)
|
||||
* @param initialScale - initial display scale
|
||||
* @param dpr - devicePixelRatio
|
||||
* @param dispatchRender - (pageNum, scale, vpWidth, vpHeight, gen, quality) => void
|
||||
*/
|
||||
constructor(wrapper, viewports, initialScale, dpr, dispatchRender) {
|
||||
this._wrapper = wrapper;
|
||||
this._viewports = viewports;
|
||||
this._scale = initialScale;
|
||||
this._dpr = dpr;
|
||||
this._dispatchRender = dispatchRender;
|
||||
this._renderGen = 0;
|
||||
this._inFlight = 0;
|
||||
this._zooming = false;
|
||||
this._states = new Array(viewports.length).fill(State.PLACEHOLDER);
|
||||
this._canvases = new Array(viewports.length).fill(null);
|
||||
this._wrappers = [];
|
||||
this._currentQualities = new Array(viewports.length).fill(1.0); // Track quality per page
|
||||
this._targetQualities = new Array(viewports.length).fill(1.0); // Target quality for transitions
|
||||
this._needsQualityUpdate = false;
|
||||
this._buildPlaceholders();
|
||||
}
|
||||
get pageWrappers() { return this._wrappers; }
|
||||
get numPages() { return this._viewports.length; }
|
||||
get renderGen() { return this._renderGen; }
|
||||
/** Called by ZoomController to suppress canvas teardown during CSS zoom. */
|
||||
setZooming(z) { this._zooming = z; }
|
||||
setScale(scale) {
|
||||
this._scale = scale;
|
||||
}
|
||||
setQualityForPage(pageNum, quality) {
|
||||
const i = pageNum - 1;
|
||||
if (i >= 0 && i < this._viewports.length) {
|
||||
this._targetQualities[i] = quality;
|
||||
// If page is rendered and quality changed significantly, trigger re-render
|
||||
if (this._states[i] === State.RENDERED &&
|
||||
Math.abs(quality - this._currentQualities[i]) > 0.1) {
|
||||
this._startQualityAdjustment(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
setGlobalQuality(quality) {
|
||||
for (let i = 0; i < this._targetQualities.length; i++) {
|
||||
this._targetQualities[i] = quality;
|
||||
}
|
||||
// Re-render visible pages with new quality
|
||||
this._needsQualityUpdate = true;
|
||||
}
|
||||
_buildPlaceholders() {
|
||||
for (let i = 0; i < this._viewports.length; i++) {
|
||||
const vp = this._viewports[i];
|
||||
const wrap = document.createElement("div");
|
||||
wrap.className = "page-wrapper";
|
||||
wrap.dataset.page = String(i + 1);
|
||||
wrap.style.width = vp.width * this._scale + "px";
|
||||
wrap.style.height = vp.height * this._scale + "px";
|
||||
this._wrapper.appendChild(wrap);
|
||||
this._wrappers.push(wrap);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Called on each IntersectionObserver tick.
|
||||
* Renders pages entering the buffer; cleans up pages leaving it.
|
||||
* Visible pages are prioritized in the render queue.
|
||||
*
|
||||
* @param bufferSet - 1-based page numbers in the buffer zone
|
||||
* @param visibleSet - 1-based page numbers currently on screen
|
||||
*/
|
||||
reconcile(bufferSet, visibleSet) {
|
||||
const gen = this._renderGen;
|
||||
// Prioritize visible pages, then the rest of the buffer
|
||||
const toRender = [
|
||||
...[...visibleSet].sort((a, b) => a - b),
|
||||
...[...bufferSet].filter(p => !visibleSet.has(p)).sort((a, b) => a - b),
|
||||
];
|
||||
for (const pageNum of toRender) {
|
||||
const i = pageNum - 1;
|
||||
if (i < 0 || i >= this._viewports.length)
|
||||
continue;
|
||||
if (this._states[i] === State.PLACEHOLDER) {
|
||||
this._startRender(i, gen);
|
||||
}
|
||||
else if (this._states[i] === State.QUALITY_ADJUSTING) {
|
||||
// Already handling quality transition
|
||||
}
|
||||
else if (this._needsQualityUpdate && this._states[i] === State.RENDERED) {
|
||||
// Apply quality updates if needed
|
||||
if (Math.abs(this._targetQualities[i] - this._currentQualities[i]) > 0.05) {
|
||||
this._startQualityAdjustment(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Clean up pages no longer in the buffer
|
||||
for (let i = 0; i < this._viewports.length; i++) {
|
||||
const pageNum = i + 1;
|
||||
if (!this._zooming && !bufferSet.has(pageNum) && this._states[i] === State.RENDERED) {
|
||||
this._cleanup(i);
|
||||
}
|
||||
// Cancel stale RENDERING pages outside the buffer
|
||||
if (!bufferSet.has(pageNum) && (this._states[i] === State.RENDERING || this._states[i] === State.QUALITY_ADJUSTING)) {
|
||||
if (this._states[i] === State.RENDERING)
|
||||
this._inFlight--;
|
||||
this._states[i] = State.PLACEHOLDER;
|
||||
}
|
||||
}
|
||||
this._needsQualityUpdate = false;
|
||||
}
|
||||
get allRendered() { return this._inFlight === 0; }
|
||||
getPageElement(pageNum) {
|
||||
const i = pageNum - 1;
|
||||
if (i >= 0 && i < this._wrappers.length) {
|
||||
return this._wrappers[i];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
_startRender(i, gen) {
|
||||
const vp = this._viewports[i];
|
||||
const quality = this._targetQualities[i];
|
||||
const scale = clampedRenderScale(vp.width, vp.height, this._scale, this._dpr, quality);
|
||||
this._states[i] = State.RENDERING;
|
||||
this._inFlight++;
|
||||
this._dispatchRender(i + 1, this._scale, vp.width, vp.height, gen, quality);
|
||||
}
|
||||
_startQualityAdjustment(i) {
|
||||
// Transition to quality adjustment state
|
||||
this._states[i] = State.QUALITY_ADJUSTING;
|
||||
const pageNum = i + 1;
|
||||
const gen = ++this._renderGen; // Increment gen to cancel any pending renders
|
||||
const vp = this._viewports[i];
|
||||
const quality = this._targetQualities[i];
|
||||
const scale = clampedRenderScale(vp.width, vp.height, this._scale, this._dpr, quality);
|
||||
this._inFlight++;
|
||||
this._dispatchRender(pageNum, this._scale, vp.width, vp.height, gen, quality);
|
||||
}
|
||||
_cleanup(i) {
|
||||
if (this._states[i] === State.RENDERING || this._states[i] === State.QUALITY_ADJUSTING) {
|
||||
this._inFlight--;
|
||||
}
|
||||
this._states[i] = State.PLACEHOLDER;
|
||||
const canvas = this._canvases[i];
|
||||
if (canvas) {
|
||||
canvas.remove();
|
||||
this._canvases[i] = null;
|
||||
}
|
||||
// Also remove the text and annotation layers
|
||||
const wrap = this._wrappers[i];
|
||||
wrap.querySelector(".text-layer")?.remove();
|
||||
wrap.querySelector(".annot-layer")?.remove();
|
||||
}
|
||||
/**
|
||||
* Called when the worker returns a rendered bitmap.
|
||||
*
|
||||
* @param pageNum
|
||||
* @param gen - render generation the bitmap was rendered for
|
||||
* @param bitmap
|
||||
* @param quality - quality at which this bitmap was rendered
|
||||
*/
|
||||
onRendered(pageNum, gen, bitmap, quality) {
|
||||
if (gen !== this._renderGen) {
|
||||
bitmap.close();
|
||||
return;
|
||||
}
|
||||
const i = pageNum - 1;
|
||||
if (i < 0 || i >= this._viewports.length ||
|
||||
(this._states[i] !== State.RENDERING && this._states[i] !== State.QUALITY_ADJUSTING)) {
|
||||
bitmap.close();
|
||||
return;
|
||||
}
|
||||
this._inFlight--;
|
||||
this._currentQualities[i] = quality;
|
||||
const vp = this._viewports[i];
|
||||
const wrap = this._wrappers[i];
|
||||
const cssW = vp.width * this._scale;
|
||||
const cssH = vp.height * this._scale;
|
||||
// For quality adjustments, we want smooth transitions
|
||||
const wasQualityAdjustment = this._states[i] === State.QUALITY_ADJUSTING;
|
||||
this._states[i] = State.RENDERED;
|
||||
// Remove the old canvas
|
||||
const old = wrap.querySelector("canvas");
|
||||
if (old) {
|
||||
if (wasQualityAdjustment) {
|
||||
// For quality adjustments, fade out the old canvas for smooth transition
|
||||
old.style.transition = "opacity 0.15s ease-out";
|
||||
old.style.opacity = "0";
|
||||
setTimeout(() => old.remove(), 150);
|
||||
}
|
||||
else {
|
||||
old.remove();
|
||||
}
|
||||
}
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = bitmap.width;
|
||||
canvas.height = bitmap.height;
|
||||
canvas.style.width = cssW + "px";
|
||||
canvas.style.height = cssH + "px";
|
||||
// Zero-copy display — fall back to drawImage if bitmaprenderer unavailable
|
||||
const ctx = canvas.getContext("bitmaprenderer");
|
||||
if (ctx) {
|
||||
ctx.transferFromImageBitmap(bitmap);
|
||||
}
|
||||
else {
|
||||
const ctx2d = canvas.getContext("2d");
|
||||
if (ctx2d) {
|
||||
ctx2d.drawImage(bitmap, 0, 0);
|
||||
bitmap.close();
|
||||
}
|
||||
}
|
||||
// For quality adjustments, fade in the new canvas
|
||||
if (wasQualityAdjustment) {
|
||||
canvas.style.opacity = "0";
|
||||
canvas.style.transition = "opacity 0.15s ease-in";
|
||||
wrap.appendChild(canvas);
|
||||
requestAnimationFrame(() => {
|
||||
canvas.style.opacity = "1";
|
||||
});
|
||||
}
|
||||
else {
|
||||
wrap.appendChild(canvas);
|
||||
}
|
||||
this._canvases[i] = canvas;
|
||||
}
|
||||
/**
|
||||
* Get the canvas element for a specific page
|
||||
*/
|
||||
getPageCanvas(pageNum) {
|
||||
const i = pageNum - 1;
|
||||
if (i >= 0 && i < this._canvases.length) {
|
||||
return this._canvases[i];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* Called after the zoom debounce fires.
|
||||
* Increments renderGen (cancels stale work), resizes all placeholders,
|
||||
* then re-renders the buffer pages at the new resolution.
|
||||
*
|
||||
* @param newScale
|
||||
* @param bufferSet
|
||||
* @param visibleSet
|
||||
*/
|
||||
onScaleChange(newScale, bufferSet, visibleSet) {
|
||||
this._scale = newScale;
|
||||
this._renderGen++;
|
||||
for (let i = 0; i < this._viewports.length; i++) {
|
||||
const pageNum = i + 1;
|
||||
const vp = this._viewports[i];
|
||||
const wrap = this._wrappers[i];
|
||||
const cssW = vp.width * newScale;
|
||||
const cssH = vp.height * newScale;
|
||||
wrap.style.width = cssW + "px";
|
||||
wrap.style.height = cssH + "px";
|
||||
wrap.style.zoom = "1";
|
||||
if (bufferSet.has(pageNum)) {
|
||||
// Keep the old canvas visible (stretched to the new CSS size) so there
|
||||
// is no blank flash while the new render is in flight.
|
||||
const canvas = this._canvases[i];
|
||||
if (canvas) {
|
||||
canvas.style.width = cssW + "px";
|
||||
canvas.style.height = cssH + "px";
|
||||
}
|
||||
if (this._states[i] === State.RENDERING || this._states[i] === State.QUALITY_ADJUSTING) {
|
||||
this._inFlight--;
|
||||
}
|
||||
this._states[i] = State.PLACEHOLDER;
|
||||
}
|
||||
else {
|
||||
// Off-screen: clean up immediately — not visible, so no flash.
|
||||
const canvas = this._canvases[i];
|
||||
if (canvas) {
|
||||
canvas.remove();
|
||||
this._canvases[i] = null;
|
||||
}
|
||||
this._states[i] = State.PLACEHOLDER;
|
||||
}
|
||||
}
|
||||
this.reconcile(bufferSet, visibleSet);
|
||||
}
|
||||
/**
|
||||
* Returns the 1-based number of the topmost visible page.
|
||||
*
|
||||
* @param visibleSet
|
||||
*/
|
||||
getCurrentPage(visibleSet) {
|
||||
// Defensive programming: handle null/undefined visibleSet
|
||||
if (!visibleSet || visibleSet.size === 0) {
|
||||
// Fall back to scroll position
|
||||
const top = this._wrapper.parentElement?.getBoundingClientRect().top ?? 0;
|
||||
for (const wrap of this._wrappers) {
|
||||
if (wrap.getBoundingClientRect().bottom > top + 4) {
|
||||
return parseInt(wrap.dataset.page || '1', 10);
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
return Math.min(...visibleSet);
|
||||
}
|
||||
/**
|
||||
* Get the current quality for a specific page
|
||||
*/
|
||||
getPageQuality(pageNum) {
|
||||
const i = pageNum - 1;
|
||||
if (i >= 0 && i < this._currentQualities.length) {
|
||||
return this._currentQualities[i];
|
||||
}
|
||||
return 1.0;
|
||||
}
|
||||
/**
|
||||
* Get the current render state for a page
|
||||
*/
|
||||
getPageState(pageNum) {
|
||||
const i = pageNum - 1;
|
||||
if (i >= 0 && i < this._states.length) {
|
||||
return this._states[i];
|
||||
}
|
||||
return State.PLACEHOLDER;
|
||||
}
|
||||
/**
|
||||
* Force cleanup of all resources
|
||||
*/
|
||||
cleanup() {
|
||||
for (let i = 0; i < this._viewports.length; i++) {
|
||||
this._cleanup(i);
|
||||
}
|
||||
this._inFlight = 0;
|
||||
this._renderGen = 0;
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=page-manager-enhanced.js.map
|
||||
1
src-tauri/dist/page-manager-enhanced.js.map
vendored
Normal file
1
src-tauri/dist/page-manager-enhanced.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
115
src-tauri/dist/performance-manager.js
vendored
Normal file
115
src-tauri/dist/performance-manager.js
vendored
Normal file
@@ -0,0 +1,115 @@
|
||||
// performance-manager.ts — Performance monitoring and optimization system
|
||||
// TypeScript rewrite with proper type safety
|
||||
class PerformanceManager {
|
||||
constructor() {
|
||||
this.frameTimes = [];
|
||||
this.maxFrameHistory = 60;
|
||||
this.lastFrameTime = 0;
|
||||
this.fps = 60;
|
||||
this.memoryWarnings = 0;
|
||||
this.renderQuality = 1.0;
|
||||
this.adaptiveMode = false;
|
||||
this.setupMonitoring();
|
||||
}
|
||||
setupMonitoring() {
|
||||
let lastTime = performance.now();
|
||||
let frameCount = 0;
|
||||
const monitorLoop = (currentTime) => {
|
||||
frameCount++;
|
||||
if (frameCount % 10 === 0) {
|
||||
const delta = currentTime - lastTime;
|
||||
const avgFrameTime = delta / 10;
|
||||
this.fps = Math.round(1000 / avgFrameTime);
|
||||
this.frameTimes.push(avgFrameTime);
|
||||
if (this.frameTimes.length > this.maxFrameHistory) {
|
||||
this.frameTimes.shift();
|
||||
}
|
||||
this.analyzePerformance();
|
||||
lastTime = currentTime;
|
||||
}
|
||||
requestAnimationFrame(monitorLoop);
|
||||
};
|
||||
requestAnimationFrame(monitorLoop);
|
||||
if (window.performance && performance.memory) {
|
||||
setInterval(() => this.checkMemory(), 2000);
|
||||
}
|
||||
}
|
||||
analyzePerformance() {
|
||||
if (this.frameTimes.length < 10)
|
||||
return;
|
||||
const sorted = [...this.frameTimes].sort((a, b) => a - b);
|
||||
const avgFrameTime = sorted.reduce((a, b) => a + b, 0) / sorted.length;
|
||||
const p90FrameTime = sorted[Math.floor(sorted.length * 0.9)];
|
||||
const frameBudget = 16; // 16ms for 60fps
|
||||
if (p90FrameTime > frameBudget * 1.5) {
|
||||
this.setRenderQuality(Math.max(0.5, this.renderQuality - 0.2));
|
||||
this.adaptiveMode = true;
|
||||
}
|
||||
else if (p90FrameTime > frameBudget * 1.2) {
|
||||
this.setRenderQuality(Math.max(0.7, this.renderQuality - 0.1));
|
||||
}
|
||||
else if (avgFrameTime < frameBudget * 0.8 && this.adaptiveMode) {
|
||||
this.setRenderQuality(Math.min(1.0, this.renderQuality + 0.05));
|
||||
if (this.renderQuality >= 0.95) {
|
||||
this.adaptiveMode = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
checkMemory() {
|
||||
try {
|
||||
// Use type assertion for non-standard memory API
|
||||
const perf = performance;
|
||||
const memory = perf.memory;
|
||||
if (!memory) {
|
||||
throw new Error('Memory API not available');
|
||||
}
|
||||
const usedHeap = memory.usedJSHeapSize;
|
||||
const heapLimit = memory.jsHeapSizeLimit;
|
||||
const usageRatio = usedHeap / heapLimit;
|
||||
if (usageRatio > 0.8) {
|
||||
this.memoryWarnings++;
|
||||
if (this.memoryWarnings > 3) {
|
||||
this.triggerMemoryCleanup();
|
||||
this.memoryWarnings = 0;
|
||||
}
|
||||
this.setRenderQuality(Math.max(0.6, this.renderQuality - 0.1));
|
||||
}
|
||||
else {
|
||||
this.memoryWarnings = Math.max(0, this.memoryWarnings - 0.5);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.warn('Memory monitoring not available:', e);
|
||||
}
|
||||
}
|
||||
triggerMemoryCleanup() {
|
||||
if (window.gc) {
|
||||
window.gc();
|
||||
}
|
||||
}
|
||||
setRenderQuality(quality) {
|
||||
this.renderQuality = Math.max(0.3, Math.min(1.0, quality));
|
||||
console.debug(`[Perf] Render quality adjusted to: ${(this.renderQuality * 100).toFixed(0)}%`);
|
||||
}
|
||||
getCurrentQuality() {
|
||||
return this.renderQuality;
|
||||
}
|
||||
isPerformanceCritical() {
|
||||
return this.adaptiveMode || this.fps < 45;
|
||||
}
|
||||
getPerformanceStats() {
|
||||
return {
|
||||
fps: this.fps,
|
||||
avgFrameTime: this.frameTimes.length > 0
|
||||
? this.frameTimes.reduce((a, b) => a + b, 0) / this.frameTimes.length
|
||||
: 0,
|
||||
renderQuality: this.renderQuality,
|
||||
adaptiveMode: this.adaptiveMode
|
||||
};
|
||||
}
|
||||
cleanup() {
|
||||
this.frameTimes = [];
|
||||
}
|
||||
}
|
||||
export { PerformanceManager };
|
||||
//# sourceMappingURL=performance-manager.js.map
|
||||
1
src-tauri/dist/performance-manager.js.map
vendored
Normal file
1
src-tauri/dist/performance-manager.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
339
src-tauri/dist/render-system.js
vendored
Normal file
339
src-tauri/dist/render-system.js
vendored
Normal file
@@ -0,0 +1,339 @@
|
||||
/**
|
||||
* render-system.ts — Advanced rendering system with performance optimization
|
||||
*
|
||||
* Manages the complete rendering pipeline with:
|
||||
* - Double buffering for flicker-free rendering
|
||||
* - Adaptive quality based on performance
|
||||
* - Intelligent page caching
|
||||
* - Smooth zoom and scroll handling
|
||||
*/
|
||||
import { PerformanceManager } from './performance-manager.js';
|
||||
class RenderSystem {
|
||||
constructor(pdfDoc, container, pageManager) {
|
||||
this.pdfDoc = pdfDoc;
|
||||
this.container = container;
|
||||
this.pageManager = pageManager;
|
||||
try {
|
||||
this.performanceManager = new PerformanceManager();
|
||||
}
|
||||
catch (error) {
|
||||
console.error("Failed to create PerformanceManager:", error);
|
||||
// Fallback: create a minimal performance manager
|
||||
this.performanceManager = {
|
||||
getPerformanceStats: () => ({ fps: 60, renderQuality: 1.0, adaptiveMode: false }),
|
||||
isPerformanceCritical: () => false,
|
||||
cleanup: () => { }
|
||||
};
|
||||
}
|
||||
// Render state
|
||||
this.currentScale = 1.0;
|
||||
this.targetScale = 1.0;
|
||||
this.renderQuality = 1.0;
|
||||
this.visiblePages = new Set();
|
||||
this.bufferPages = new Set();
|
||||
this.renderQueue = [];
|
||||
this.activeRenders = new Map();
|
||||
this.pageCache = new Map(); // { "page_scale": {bitmap, timestamp, quality} }
|
||||
this.maxCacheSize = 20; // Max pages in cache
|
||||
this.cacheSize = 0;
|
||||
// Performance metrics
|
||||
this.lastRenderTime = 0;
|
||||
this.renderCount = 0;
|
||||
// Setup event listeners
|
||||
this.setupEventListeners();
|
||||
}
|
||||
setupEventListeners() {
|
||||
// Performance-based quality adjustments
|
||||
setInterval(() => {
|
||||
this.adjustQualityBasedOnPerformance();
|
||||
}, 1000);
|
||||
}
|
||||
setVisibility(visiblePages, bufferPages) {
|
||||
this.visiblePages = new Set(visiblePages);
|
||||
this.bufferPages = new Set(bufferPages);
|
||||
this.scheduleRenders();
|
||||
}
|
||||
setScale(scale) {
|
||||
this.targetScale = scale;
|
||||
// Smooth transition for zoom
|
||||
if (Math.abs(this.targetScale - this.currentScale) > 0.01) {
|
||||
this.startZoomTransition();
|
||||
}
|
||||
else {
|
||||
this.currentScale = this.targetScale;
|
||||
this.scheduleRenders();
|
||||
}
|
||||
}
|
||||
startZoomTransition() {
|
||||
const startScale = this.currentScale;
|
||||
const endScale = this.targetScale;
|
||||
const startTime = performance.now();
|
||||
const duration = 150; // 150ms transition
|
||||
const animate = (currentTime) => {
|
||||
const elapsed = currentTime - startTime;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
// Ease-in-out animation
|
||||
const easedProgress = this.easeInOutCubic(progress);
|
||||
this.currentScale = startScale + (endScale - startScale) * easedProgress;
|
||||
// Use lower quality during transition
|
||||
const transitionQuality = this.renderQuality * 0.8;
|
||||
this.scheduleRenders(transitionQuality);
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
else {
|
||||
this.currentScale = endScale;
|
||||
// Final high-quality render after transition
|
||||
this.scheduleRenders(this.renderQuality);
|
||||
}
|
||||
};
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
easeInOutCubic(t) {
|
||||
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
||||
}
|
||||
scheduleRenders(overrideQuality = null) {
|
||||
const quality = overrideQuality !== null ? overrideQuality : this.renderQuality;
|
||||
const scale = this.currentScale;
|
||||
// Cancel renders for pages that are no longer needed
|
||||
this.cancelStaleRenders();
|
||||
// Prioritize visible pages, then buffer pages
|
||||
const allPages = [...this.visiblePages, ...this.bufferPages];
|
||||
allPages.forEach(pageNum => {
|
||||
const cacheKey = this.getCacheKey(pageNum, scale);
|
||||
// Check if we need to render this page
|
||||
if (this.shouldRenderPage(pageNum, scale, quality)) {
|
||||
this.queueRender(pageNum, scale, quality);
|
||||
}
|
||||
});
|
||||
// Process the render queue
|
||||
this.processRenderQueue();
|
||||
}
|
||||
shouldRenderPage(pageNum, scale, quality) {
|
||||
const cacheKey = this.getCacheKey(pageNum, scale);
|
||||
// Check if already rendering
|
||||
if (this.activeRenders.has(pageNum)) {
|
||||
return false;
|
||||
}
|
||||
// Check cache with acceptable quality
|
||||
const cached = this.pageCache.get(cacheKey);
|
||||
if (cached) {
|
||||
// If cached quality is acceptable, no need to re-render
|
||||
if (cached.quality >= quality * 0.9) {
|
||||
this.useCachedPage(pageNum, scale, cached.bitmap);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
queueRender(pageNum, scale, quality) {
|
||||
// Avoid duplicate queue entries
|
||||
if (this.renderQueue.some(item => item.pageNum === pageNum && Math.abs(item.scale - scale) < 0.01)) {
|
||||
return;
|
||||
}
|
||||
// Prioritize visible pages
|
||||
const priority = this.visiblePages.has(pageNum) ? 1 : 2;
|
||||
this.renderQueue.push({ pageNum, scale, quality, priority });
|
||||
// Sort by priority (visible pages first)
|
||||
this.renderQueue.sort((a, b) => a.priority - b.priority);
|
||||
}
|
||||
processRenderQueue() {
|
||||
if (this.renderQueue.length === 0)
|
||||
return;
|
||||
// Limit concurrent renders based on performance
|
||||
const maxConcurrent = this.performanceManager.isPerformanceCritical() ? 2 : 4;
|
||||
while (this.activeRenders.size < maxConcurrent && this.renderQueue.length > 0) {
|
||||
const renderTask = this.renderQueue.shift();
|
||||
if (renderTask) {
|
||||
this.startRender(renderTask.pageNum, renderTask.scale, renderTask.quality);
|
||||
}
|
||||
}
|
||||
}
|
||||
async startRender(pageNum, scale, quality) {
|
||||
this.activeRenders.set(pageNum, true);
|
||||
let page = null;
|
||||
try {
|
||||
page = await this.pdfDoc.getPage(pageNum);
|
||||
// Apply quality-based scaling
|
||||
const effectiveScale = scale * quality;
|
||||
const viewport = page.getViewport({ scale: effectiveScale });
|
||||
// Validate canvas dimensions
|
||||
const width = Math.round(viewport.width);
|
||||
const height = Math.round(viewport.height);
|
||||
if (isNaN(width) || isNaN(height) || width <= 0 || height <= 0) {
|
||||
throw new Error(`Invalid canvas dimensions: ${width}x${height}`);
|
||||
}
|
||||
// Create offscreen canvas
|
||||
const offscreen = new OffscreenCanvas(width, height);
|
||||
const context = offscreen.getContext('2d');
|
||||
if (!context) {
|
||||
throw new Error('Failed to get 2D context for offscreen canvas');
|
||||
}
|
||||
// Render with quality settings
|
||||
await page.render({
|
||||
canvasContext: context,
|
||||
viewport: viewport,
|
||||
// Use lower quality rendering when performance is critical
|
||||
renderQuality: this.performanceManager.isPerformanceCritical() ? 'low' : 'high'
|
||||
}).promise;
|
||||
// Transfer bitmap
|
||||
const bitmap = offscreen.transferToImageBitmap();
|
||||
// Cache the result
|
||||
const cacheKey = this.getCacheKey(pageNum, scale);
|
||||
this.cachePage(cacheKey, bitmap, quality);
|
||||
// Update display
|
||||
this.updatePageDisplay(pageNum, bitmap);
|
||||
}
|
||||
catch (error) {
|
||||
if (error && typeof error === 'object' && 'name' in error && error.name !== "RenderingCancelledException") {
|
||||
console.warn(`[RenderSystem] Render failed for page ${pageNum}:`, error);
|
||||
}
|
||||
}
|
||||
finally {
|
||||
this.activeRenders.delete(pageNum);
|
||||
page?.cleanup();
|
||||
// Process next in queue
|
||||
this.processRenderQueue();
|
||||
}
|
||||
}
|
||||
cachePage(cacheKey, bitmap, quality) {
|
||||
// Evict if cache is full
|
||||
if (this.cacheSize >= this.maxCacheSize) {
|
||||
this.evictOldestCacheEntry();
|
||||
}
|
||||
this.pageCache.set(cacheKey, {
|
||||
bitmap,
|
||||
quality,
|
||||
timestamp: Date.now(),
|
||||
accessTime: Date.now()
|
||||
});
|
||||
this.cacheSize++;
|
||||
// Update access time for LRU
|
||||
this.updateCacheAccess(cacheKey);
|
||||
}
|
||||
evictOldestCacheEntry() {
|
||||
if (this.pageCache.size === 0)
|
||||
return;
|
||||
// Find least recently used
|
||||
let oldestKey = null;
|
||||
let oldestTime = Infinity;
|
||||
this.pageCache.forEach((entry, key) => {
|
||||
if (entry.accessTime < oldestTime) {
|
||||
oldestTime = entry.accessTime;
|
||||
oldestKey = key;
|
||||
}
|
||||
});
|
||||
if (oldestKey) {
|
||||
const entry = this.pageCache.get(oldestKey);
|
||||
if (entry) {
|
||||
entry.bitmap.close(); // Free memory
|
||||
}
|
||||
this.pageCache.delete(oldestKey);
|
||||
this.cacheSize--;
|
||||
}
|
||||
}
|
||||
updateCacheAccess(cacheKey) {
|
||||
const entry = this.pageCache.get(cacheKey);
|
||||
if (entry) {
|
||||
entry.accessTime = Date.now();
|
||||
}
|
||||
}
|
||||
useCachedPage(pageNum, scale, bitmap) {
|
||||
// Create a copy of the bitmap for display
|
||||
const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
|
||||
const context = canvas.getContext('2d');
|
||||
if (context) {
|
||||
context.drawImage(bitmap, 0, 0);
|
||||
const displayBitmap = canvas.transferToImageBitmap();
|
||||
this.updatePageDisplay(pageNum, displayBitmap);
|
||||
}
|
||||
}
|
||||
updatePageDisplay(pageNum, bitmap) {
|
||||
// Use the EnhancedPageManager's onRendered method to handle display
|
||||
// This ensures proper integration with the page manager's state and canvas management
|
||||
const gen = this.pageManager.renderGen;
|
||||
const quality = this.renderQuality; // Use current render quality
|
||||
this.pageManager.onRendered(pageNum, gen, bitmap, quality);
|
||||
}
|
||||
cancelStaleRenders() {
|
||||
// Cancel renders for pages that are no longer visible or buffered
|
||||
const neededPages = new Set([...this.visiblePages, ...this.bufferPages]);
|
||||
this.activeRenders.forEach((_, pageNum) => {
|
||||
if (!neededPages.has(pageNum)) {
|
||||
// This render is no longer needed
|
||||
this.activeRenders.delete(pageNum);
|
||||
}
|
||||
});
|
||||
// Also clean up render queue
|
||||
this.renderQueue = this.renderQueue.filter(task => neededPages.has(task.pageNum));
|
||||
}
|
||||
adjustQualityBasedOnPerformance() {
|
||||
const stats = this.performanceManager.getPerformanceStats();
|
||||
// If performance is critical, we may have already adjusted quality
|
||||
// Here we can make additional adjustments based on render load
|
||||
if (this.activeRenders.size > 4 && stats.fps < 50) {
|
||||
// Too many active renders and low FPS - reduce quality more
|
||||
const newQuality = Math.max(0.5, this.renderQuality - 0.1);
|
||||
if (newQuality < this.renderQuality) {
|
||||
this.setRenderQuality(newQuality);
|
||||
}
|
||||
}
|
||||
}
|
||||
setRenderQuality(quality) {
|
||||
this.renderQuality = Math.max(0.3, Math.min(1.0, quality));
|
||||
// Re-render visible pages with new quality
|
||||
if (this.visiblePages.size > 0) {
|
||||
this.scheduleRenders(this.renderQuality);
|
||||
}
|
||||
}
|
||||
cleanupMemory() {
|
||||
// Aggressive cleanup - keep only visible pages
|
||||
const visibleCacheKeys = new Set();
|
||||
this.visiblePages.forEach(pageNum => {
|
||||
const cacheKey = this.getCacheKey(pageNum, this.currentScale);
|
||||
visibleCacheKeys.add(cacheKey);
|
||||
});
|
||||
// Remove all non-visible pages from cache
|
||||
this.pageCache.forEach((entry, cacheKey) => {
|
||||
if (!visibleCacheKeys.has(cacheKey)) {
|
||||
entry.bitmap.close();
|
||||
this.pageCache.delete(cacheKey);
|
||||
this.cacheSize--;
|
||||
}
|
||||
});
|
||||
// Force garbage collection if available
|
||||
if (window.gc) {
|
||||
window.gc();
|
||||
}
|
||||
}
|
||||
getCacheKey(pageNum, scale) {
|
||||
// Round scale to 2 decimal places for caching
|
||||
const roundedScale = Math.round(scale * 100) / 100;
|
||||
return `${pageNum}_${roundedScale}`;
|
||||
}
|
||||
async handleRenderRequest(pageNum, scale, vpWidth, vpHeight, quality) {
|
||||
// Handle render request from PageManager
|
||||
// This integrates with the PageManager's rendering pipeline
|
||||
await this.startRender(pageNum, scale, quality);
|
||||
}
|
||||
cleanup() {
|
||||
// Clean up all resources
|
||||
this.pageCache.forEach(entry => {
|
||||
entry.bitmap.close();
|
||||
});
|
||||
this.activeRenders.clear();
|
||||
this.renderQueue = [];
|
||||
this.performanceManager.cleanup();
|
||||
}
|
||||
getPerformanceStats() {
|
||||
return {
|
||||
...this.performanceManager.getPerformanceStats(),
|
||||
activeRenders: this.activeRenders.size,
|
||||
cacheSize: this.cacheSize,
|
||||
renderQueueLength: this.renderQueue.length
|
||||
};
|
||||
}
|
||||
}
|
||||
// Export for ES6 modules
|
||||
export { RenderSystem };
|
||||
//# sourceMappingURL=render-system.js.map
|
||||
1
src-tauri/dist/render-system.js.map
vendored
Normal file
1
src-tauri/dist/render-system.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
4
src-tauri/dist/types.js
vendored
Normal file
4
src-tauri/dist/types.js
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
// Centralized type definitions for the PDF viewer
|
||||
// This file contains all type definitions in one place for easier management
|
||||
export {};
|
||||
//# sourceMappingURL=types.js.map
|
||||
1
src-tauri/dist/types.js.map
vendored
Normal file
1
src-tauri/dist/types.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"types.js","sourceRoot":"","sources":["../assets/viewer/types.ts"],"names":[],"mappings":"AAAA,kDAAkD;AAClD,6EAA6E","sourcesContent":["// Centralized type definitions for the PDF viewer\n// This file contains all type definitions in one place for easier management\n\n/**\n * PDF.js Library Types\n */\ninterface PDFDocumentProxy {\n getPage(pageNumber: number): Promise<PDFPageProxy>;\n numPages: number;\n destroy(): void;\n}\n\ninterface PDFPageProxy {\n getViewport(options: { scale: number }): { width: number; height: number };\n render(options: {\n canvasContext: CanvasRenderingContext2D;\n viewport: any;\n renderQuality?: string;\n }): { promise: Promise<void> };\n cleanup(): void;\n}\n\ninterface PDFDocumentLoadingTask {\n promise: Promise<PDFDocumentProxy>;\n}\n\n/**\n * DOM API Extensions\n */\ninterface OffscreenCanvas {\n transferToImageBitmap(): ImageBitmap;\n}\n\n// Extend Performance interface for non-standard memory API\ninterface Performance {\n memory?: {\n usedJSHeapSize: number;\n jsHeapSizeLimit: number;\n };\n}\n\n/**\n * Window Extensions\n */\ninterface Window {\n pdfjsLib: any;\n __TAURI__?: {\n invoke(cmd: string, args?: any): Promise<any>;\n };\n}\n\n/**\n * Common Utility Types\n */\ntype DispatchRenderFunction = (\n pageNum: number,\n scale: number,\n vpWidth: number,\n vpHeight: number,\n gen: number,\n quality: number\n) => void;\n\ntype ZoomCallback = (\n newScale: number,\n bufferSet: Set<number>,\n visibleSet: Set<number>\n) => void;\n\ntype GetVisibilityState = () => {\n bufferSet: Set<number>;\n visibleSet: Set<number>;\n};\n\n/**\n * Annotation Types\n */\ninterface Quad {\n points: { x: number; y: number }[];\n}\n\ninterface Annotation {\n id: string;\n page: number;\n quads: Quad[];\n content?: string;\n color?: string;\n type: string;\n}\n\n/**\n * Performance Monitoring Types\n */\ninterface PerformanceStats {\n fps: number;\n avgFrameTime: number;\n renderQuality: number;\n adaptiveMode: boolean;\n}\n\n/**\n * Viewport and Geometry Types\n */\ninterface Viewport {\n width: number;\n height: number;\n}\n\ninterface BoundingBox {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\n// PDF.js library declaration\ndeclare const pdfjsLib: {\n getDocument(options: { url: string }): PDFDocumentLoadingTask;\n GlobalWorkerOptions: {\n workerSrc: string;\n };\n};\n\nexport { \n PDFDocumentProxy, \n PDFPageProxy, \n PDFDocumentLoadingTask,\n OffscreenCanvas,\n PerformanceStats,\n Viewport,\n BoundingBox,\n Quad,\n Annotation,\n DispatchRenderFunction,\n ZoomCallback,\n GetVisibilityState\n};"]}
|
||||
353
src-tauri/dist/viewer.js
vendored
Normal file
353
src-tauri/dist/viewer.js
vendored
Normal file
@@ -0,0 +1,353 @@
|
||||
/**
|
||||
* viewer.ts — Performance-focused PDF viewer with enhanced rendering system
|
||||
*
|
||||
* Features:
|
||||
* - Double-buffered rendering for flicker-free operation
|
||||
* - Adaptive quality based on performance monitoring
|
||||
* - Intelligent scroll prediction and pre-loading
|
||||
* - Smooth zoom animations with quality transitions
|
||||
* - Memory-efficient caching with LRU eviction
|
||||
* - Comprehensive performance monitoring
|
||||
*/
|
||||
// @ts-ignore - Importing JavaScript modules
|
||||
import { ViewportTracker } from "./viewport-tracker.js";
|
||||
// @ts-ignore - Importing JavaScript modules
|
||||
import { EnhancedPageManager } from "./page-manager-enhanced.js";
|
||||
// @ts-ignore - Importing JavaScript modules
|
||||
import { RenderSystem } from "./render-system.js";
|
||||
// @ts-ignore - Importing JavaScript modules
|
||||
import { MessageBridge } from "./message-bridge.js";
|
||||
// @ts-ignore - Importing JavaScript modules
|
||||
import { AnnotationLayer } from "./annotation-layer.js";
|
||||
// ── DOM refs ─────────────────────────────────────────────────────────────────
|
||||
const container = document.getElementById("canvas-container");
|
||||
const pagesWrapper = document.getElementById("pages-wrapper");
|
||||
const statusEl = document.getElementById("status");
|
||||
const zoomLabel = document.getElementById("zoom-label");
|
||||
const pageIndicator = document.getElementById("page-indicator");
|
||||
// ── Global state ─────────────────────────────────────────────────────────────
|
||||
const params = new URLSearchParams(location.search);
|
||||
const refId = params.get("ref_id") || "";
|
||||
const savedZoom = parseFloat(params.get("zoom") || ""); // NaN if absent
|
||||
const savedScrollTop = parseFloat(params.get("scroll_top") || ""); // NaN if absent
|
||||
const DPR = window.devicePixelRatio || 1;
|
||||
let pdfDoc = null;
|
||||
let pageManager = null;
|
||||
let viewportTracker = null;
|
||||
let zoomController = null; // Replace with proper type when available
|
||||
let bridge = null;
|
||||
let annotationLayer = null;
|
||||
let renderSystem = null;
|
||||
let viewports = []; // scale=1 dims, used by text layer and annotation layer
|
||||
// ── Utilities ────────────────────────────────────────────────────────────────
|
||||
function setStatus(msg) {
|
||||
if (statusEl)
|
||||
statusEl.textContent = msg;
|
||||
}
|
||||
function showError(msg) {
|
||||
const b = document.getElementById("error-banner");
|
||||
if (b) {
|
||||
b.textContent = msg;
|
||||
b.style.display = "block";
|
||||
}
|
||||
setStatus("Error");
|
||||
}
|
||||
function refreshPageIndicator() {
|
||||
if (!pageManager)
|
||||
return;
|
||||
try {
|
||||
const cur = pageManager.getCurrentPage(getCurrentVisibleSet());
|
||||
if (pageIndicator) {
|
||||
pageIndicator.textContent = cur + " / " + pageManager.numPages;
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.warn("Failed to refresh page indicator:", error);
|
||||
if (pageIndicator) {
|
||||
pageIndicator.textContent = "1 / " + (pageManager.numPages || "1");
|
||||
}
|
||||
}
|
||||
}
|
||||
function getCurrentVisibleSet() {
|
||||
if (viewportTracker?.visibleSet) {
|
||||
return viewportTracker.visibleSet;
|
||||
}
|
||||
return new Set();
|
||||
}
|
||||
async function fitToPage() {
|
||||
if (!pdfDoc)
|
||||
return 1.0;
|
||||
const page = await pdfDoc.getPage(1);
|
||||
const vp = page.getViewport({ scale: 1.0 });
|
||||
const scaleW = (container?.clientWidth || 0 - 40) / vp.width;
|
||||
const scaleH = (container?.clientHeight || 0 - 40) / vp.height;
|
||||
return Math.max(0.1, Math.min(5.0, Math.min(scaleW, scaleH)));
|
||||
}
|
||||
function scrollToPage(pageNum) {
|
||||
if (!pageManager)
|
||||
return;
|
||||
const wrap = pageManager.pageWrappers[pageNum - 1];
|
||||
if (!wrap)
|
||||
return;
|
||||
if (container) {
|
||||
container.scrollTop +=
|
||||
wrap.getBoundingClientRect().top - container.getBoundingClientRect().top;
|
||||
}
|
||||
}
|
||||
// ── Render request handler ────────────────────────────────────────────────────
|
||||
async function handleRenderRequest(pageNum, scale, vpWidth, vpHeight, quality) {
|
||||
if (!renderSystem)
|
||||
return;
|
||||
try {
|
||||
await renderSystem.handleRenderRequest(pageNum, scale, vpWidth, vpHeight, quality);
|
||||
}
|
||||
catch (error) {
|
||||
console.warn(`Failed to render page ${pageNum}:`, error);
|
||||
}
|
||||
}
|
||||
// ── Text layer rendering ──────────────────────────────────────────────────────
|
||||
function renderTextLayer(pageNum, cssVp, textContent) {
|
||||
const wrap = pageManager?.pageWrappers[pageNum - 1];
|
||||
if (!wrap)
|
||||
return;
|
||||
// Remove stale text layer (e.g. after zoom)
|
||||
wrap.querySelector(".text-layer")?.remove();
|
||||
const div = document.createElement("div");
|
||||
div.className = "text-layer";
|
||||
div.style.width = Math.round(cssVp.width) + "px";
|
||||
div.style.height = Math.round(cssVp.height) + "px";
|
||||
wrap.appendChild(div);
|
||||
const pdfjsLib = window.pdfjsLib;
|
||||
if (!pdfjsLib)
|
||||
return;
|
||||
if (typeof pdfjsLib.TextLayer === "function") {
|
||||
// PDF.js 3.x+ API
|
||||
try {
|
||||
const tl = new pdfjsLib.TextLayer({
|
||||
textContentSource: textContent,
|
||||
container: div,
|
||||
viewport: cssVp,
|
||||
});
|
||||
tl.render().catch(() => { });
|
||||
}
|
||||
catch (_) { }
|
||||
}
|
||||
else if (typeof pdfjsLib.renderTextLayer === "function") {
|
||||
// Legacy API (2.x)
|
||||
try {
|
||||
pdfjsLib.renderTextLayer({ textContent, container: div, viewport: cssVp });
|
||||
}
|
||||
catch (_) { }
|
||||
}
|
||||
}
|
||||
// ── Text selection → annotation quads ────────────────────────────────────────
|
||||
function handleTextSelection() {
|
||||
const sel = window.getSelection();
|
||||
if (!sel || sel.isCollapsed || sel.rangeCount === 0)
|
||||
return;
|
||||
const selectedText = sel.toString().trim();
|
||||
if (!selectedText || !bridge)
|
||||
return;
|
||||
const range = sel.getRangeAt(0);
|
||||
// Walk up from the anchor node to find the page wrapper
|
||||
let node = range.startContainer;
|
||||
while (node && !(node instanceof HTMLElement && node.classList?.contains("page-wrapper"))) {
|
||||
node = node.parentElement;
|
||||
}
|
||||
if (!node)
|
||||
return;
|
||||
const pageNum = parseInt(node.dataset.page || '1', 10);
|
||||
if (!pageNum || pageNum < 1 || pageNum > viewports.length)
|
||||
return;
|
||||
const vp0 = viewports[pageNum - 1];
|
||||
const scale = zoomController?.getCurrentScale() || 1;
|
||||
const rects = Array.from(range.getClientRects());
|
||||
const wrapR = node.getBoundingClientRect();
|
||||
const quads = rects
|
||||
.filter(r => r.width > 1 && r.height > 1)
|
||||
.map(r => {
|
||||
const x0 = (r.left - wrapR.left) / scale;
|
||||
const x1 = (r.right - wrapR.left) / scale;
|
||||
const y0 = vp0.height - (r.bottom - wrapR.top) / scale;
|
||||
const y1 = vp0.height - (r.top - wrapR.top) / scale;
|
||||
return {
|
||||
points: [
|
||||
{ x: x0, y: y0 },
|
||||
{ x: x1, y: y0 },
|
||||
{ x: x1, y: y1 },
|
||||
{ x: x0, y: y1 },
|
||||
],
|
||||
};
|
||||
});
|
||||
if (quads.length === 0)
|
||||
return;
|
||||
// Clear browser selection so the new highlight takes over visually
|
||||
sel.removeAllRanges();
|
||||
// 0-indexed page number for the backend model
|
||||
bridge.postAnnotationCreate(refId, pageNum - 1, quads, selectedText);
|
||||
}
|
||||
// ── Main init ────────────────────────────────────────────────────────────────
|
||||
async function load() {
|
||||
if (!refId) {
|
||||
showError("No ref_id in URL.");
|
||||
return;
|
||||
}
|
||||
setStatus("Loading…");
|
||||
const pdfjsLib = window.pdfjsLib;
|
||||
if (!pdfjsLib) {
|
||||
showError("PDF.js failed to load.");
|
||||
return;
|
||||
}
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = "brittle://app/pdfjs/build/pdf.worker.min.js";
|
||||
const pdfUrl = "brittle://app/pdf?ref_id=" + encodeURIComponent(refId);
|
||||
try {
|
||||
// 1. Load PDF
|
||||
const loadingTask = pdfjsLib.getDocument({ url: pdfUrl });
|
||||
pdfDoc = await loadingTask.promise;
|
||||
// 2. Fetch all page viewports at scale=1
|
||||
setStatus("Reading…");
|
||||
viewports = [];
|
||||
if (pdfDoc) {
|
||||
for (let i = 1; i <= pdfDoc.numPages; i++) {
|
||||
const page = await pdfDoc.getPage(i);
|
||||
const vp = page.getViewport({ scale: 1.0 });
|
||||
viewports.push({ width: vp.width, height: vp.height });
|
||||
}
|
||||
}
|
||||
// 3. Compute initial scale: use saved zoom if available, else fit full page
|
||||
const fittedScale = Math.max(0.1, Math.min(5.0, Math.min(((container?.clientWidth || 0) - 40) / viewports[0].width, ((container?.clientHeight || 0) - 40) / viewports[0].height)));
|
||||
const initialScale = (savedZoom > 0) ? Math.max(0.1, Math.min(5.0, savedZoom)) : fittedScale;
|
||||
// 4. EnhancedPageManager — creates placeholder divs with quality support
|
||||
if (!pagesWrapper)
|
||||
throw new Error("Pages wrapper not found");
|
||||
pageManager = new EnhancedPageManager(pagesWrapper, viewports, initialScale, DPR, (pageNum, scale, vpWidth, vpHeight, gen, quality) => {
|
||||
// Handle render request directly
|
||||
handleRenderRequest(pageNum, scale, vpWidth, vpHeight, quality);
|
||||
});
|
||||
// 4.5. RenderSystem — handles actual page rendering
|
||||
if (!container)
|
||||
throw new Error("Container not found");
|
||||
if (!pdfDoc)
|
||||
throw new Error("PDF document not loaded");
|
||||
renderSystem = new RenderSystem(pdfDoc, container, pageManager);
|
||||
// 5. ViewportTracker — handles visibility detection
|
||||
if (!pageManager)
|
||||
throw new Error("Page manager not initialized");
|
||||
viewportTracker = new ViewportTracker(container, pageManager.pageWrappers, (bufferSet, visibleSet) => {
|
||||
pageManager?.reconcile(bufferSet, visibleSet);
|
||||
if (renderSystem)
|
||||
renderSystem.setVisibility(visibleSet, bufferSet);
|
||||
refreshPageIndicator();
|
||||
});
|
||||
// 6. EnhancedZoomController — smooth zoom with quality management
|
||||
// @ts-ignore - Importing JavaScript module
|
||||
const EnhancedZoomController = (await import("./zoom-controller-enhanced.js")).EnhancedZoomController;
|
||||
zoomController = new EnhancedZoomController(container, pageManager, (newScale, bufferSet, visibleSet) => {
|
||||
if (pageManager)
|
||||
pageManager.onScaleChange(newScale, bufferSet, visibleSet);
|
||||
if (renderSystem)
|
||||
renderSystem.setScale(newScale);
|
||||
annotationLayer?.onScaleChange(newScale);
|
||||
sendViewerState();
|
||||
}, () => ({ bufferSet: new Set(), visibleSet: new Set() }), zoomLabel);
|
||||
// Set initial scale
|
||||
zoomController?.applyScale(initialScale, { animate: false });
|
||||
// 7. MessageBridge
|
||||
bridge = new MessageBridge(() => scrollToPage(Math.min((pageManager?.getCurrentPage(viewportTracker?.visibleSet || new Set()) || 1), pdfDoc?.numPages || 1)), () => scrollToPage(Math.max((pageManager?.getCurrentPage(viewportTracker?.visibleSet || new Set()) || 1) - 1, 1)), {
|
||||
onAnnotationsSet: (annotations) => {
|
||||
annotationLayer?.setAnnotations(annotations, zoomController?.scale || 1);
|
||||
},
|
||||
});
|
||||
// 8. AnnotationLayer
|
||||
annotationLayer = new AnnotationLayer(pageManager?.pageWrappers || [], viewports, (annotationId) => bridge?.postAnnotationClick(refId, annotationId));
|
||||
// Request annotations from parent
|
||||
bridge?.requestAnnotations(refId);
|
||||
// 9. Toolbar buttons
|
||||
document.getElementById("btn-zoom-out")?.addEventListener("click", () => zoomController?.zoomOut());
|
||||
document.getElementById("btn-zoom-in")?.addEventListener("click", () => zoomController?.zoomIn());
|
||||
document.getElementById("btn-zoom-fit")?.addEventListener("click", async () => {
|
||||
const fitScale = await fitToPage();
|
||||
zoomController?.applyScale(fitScale, { animate: false });
|
||||
});
|
||||
// Keyboard shortcuts + keydown forwarding to parent
|
||||
document.addEventListener("keydown", ev => {
|
||||
if (ev.target.tagName === "INPUT")
|
||||
return;
|
||||
if (ev.key === "+" || ev.key === "=") {
|
||||
ev.preventDefault();
|
||||
zoomController?.zoomIn();
|
||||
}
|
||||
if (ev.key === "-") {
|
||||
ev.preventDefault();
|
||||
zoomController?.zoomOut();
|
||||
}
|
||||
if (ev.key === "0") {
|
||||
ev.preventDefault();
|
||||
fitToPage().then(s => zoomController?.applyScale(s, { animate: false }));
|
||||
}
|
||||
bridge?.forwardKeydown(ev);
|
||||
});
|
||||
// mouseup on the document → check for text selection to create highlights
|
||||
document.addEventListener("mouseup", ev => {
|
||||
setTimeout(() => handleTextSelection(), 0);
|
||||
});
|
||||
// Scroll → update page indicator + debounced state save
|
||||
let _scrollSaveTimer = null;
|
||||
container?.addEventListener("scroll", () => {
|
||||
refreshPageIndicator();
|
||||
if (_scrollSaveTimer)
|
||||
clearTimeout(_scrollSaveTimer);
|
||||
_scrollSaveTimer = window.setTimeout(sendViewerState, 500);
|
||||
}, { passive: true });
|
||||
// Restore saved scroll position (rAF ensures layout is ready)
|
||||
if (savedScrollTop > 0 && container) {
|
||||
requestAnimationFrame(() => { container.scrollTop = savedScrollTop; });
|
||||
}
|
||||
// Lifecycle: tab hidden → free caches; tab visible → re-reconcile
|
||||
document.addEventListener("visibilitychange", () => {
|
||||
if (document.hidden) {
|
||||
pdfDoc?.destroy();
|
||||
}
|
||||
else {
|
||||
// Trigger a visibility update to refresh pages
|
||||
const bufferSet = new Set();
|
||||
const visibleSet = new Set();
|
||||
pageManager?.reconcile(bufferSet, visibleSet);
|
||||
}
|
||||
});
|
||||
// Cleanup on unload
|
||||
window.addEventListener("beforeunload", () => {
|
||||
viewportTracker?.disconnect();
|
||||
bridge?.disconnect();
|
||||
renderSystem?.cleanup();
|
||||
pdfDoc?.destroy();
|
||||
if (zoomController)
|
||||
zoomController.cleanup();
|
||||
});
|
||||
// DPR change (e.g., moving window to a monitor with different DPI)
|
||||
if (window.matchMedia) {
|
||||
window.matchMedia(`(resolution: ${DPR}dppx)`).addEventListener("change", () => {
|
||||
if (pageManager && zoomController) {
|
||||
const bufferSet = new Set();
|
||||
const visibleSet = new Set();
|
||||
pageManager?.onScaleChange(zoomController.getCurrentScale(), bufferSet, visibleSet);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (pageIndicator && pdfDoc) {
|
||||
pageIndicator.textContent = "1 / " + pdfDoc.numPages;
|
||||
}
|
||||
setStatus("Ready");
|
||||
}
|
||||
catch (e) {
|
||||
showError("Could not load PDF: " + (e instanceof Error ? e.message : String(e)));
|
||||
}
|
||||
}
|
||||
function sendViewerState() {
|
||||
if (!bridge || !zoomController || !container)
|
||||
return;
|
||||
bridge.postViewerState(refId, zoomController.getCurrentScale(), container.scrollTop);
|
||||
}
|
||||
// Start the viewer
|
||||
load();
|
||||
//# sourceMappingURL=viewer.js.map
|
||||
1
src-tauri/dist/viewer.js.map
vendored
Normal file
1
src-tauri/dist/viewer.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
168
src-tauri/dist/zoom-controller-enhanced.js
vendored
Normal file
168
src-tauri/dist/zoom-controller-enhanced.js
vendored
Normal file
@@ -0,0 +1,168 @@
|
||||
/**
|
||||
* zoom-controller-enhanced.ts — Advanced zoom controller with smooth transitions
|
||||
*
|
||||
* Provides smooth zoom animations, adaptive quality during zoom,
|
||||
* and intelligent zoom level management.
|
||||
*/
|
||||
class EnhancedZoomController {
|
||||
constructor(container, pageManager, onScaleChange, getVisibilityState, zoomLabel) {
|
||||
this.container = container;
|
||||
this.pageManager = pageManager;
|
||||
this.onScaleChange = onScaleChange;
|
||||
this.getVisibilityState = getVisibilityState;
|
||||
this.zoomLabel = zoomLabel;
|
||||
// Zoom state
|
||||
this.scale = 1.0;
|
||||
this.targetScale = 1.0;
|
||||
this.minScale = 0.1;
|
||||
this.maxScale = 5.0;
|
||||
this.zoomAnimation = null;
|
||||
this.isZooming = false;
|
||||
this.zoomQuality = 1.0;
|
||||
// Setup event listeners
|
||||
this.setupEventListeners();
|
||||
// Update zoom label
|
||||
this.updateZoomLabel();
|
||||
}
|
||||
setupEventListeners() {
|
||||
// Smooth zoom with animation
|
||||
this.container.addEventListener('wheel', (event) => {
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
this.handleZoomWheel(event);
|
||||
event.preventDefault();
|
||||
}
|
||||
}, { passive: false });
|
||||
// Double-click for zoom to fit
|
||||
this.container.addEventListener('dblclick', (event) => {
|
||||
this.zoomToFit();
|
||||
});
|
||||
}
|
||||
handleZoomWheel(event) {
|
||||
// Calculate zoom factor based on wheel delta
|
||||
const zoomFactor = event.deltaY > 0 ? 0.9 : 1.1;
|
||||
const newScale = this.scale * zoomFactor;
|
||||
// Apply constraints
|
||||
const constrainedScale = Math.max(this.minScale, Math.min(this.maxScale, newScale));
|
||||
if (Math.abs(constrainedScale - this.scale) > 0.01) {
|
||||
this.applyScale(constrainedScale);
|
||||
}
|
||||
}
|
||||
applyScale(newScale, options = {}) {
|
||||
const { animate = true, qualityOverride = null } = options;
|
||||
// Constrain scale
|
||||
this.targetScale = Math.max(this.minScale, Math.min(this.maxScale, newScale));
|
||||
if (Math.abs(this.targetScale - this.scale) < 0.01)
|
||||
return;
|
||||
if (animate) {
|
||||
this.startZoomAnimation(qualityOverride);
|
||||
}
|
||||
else {
|
||||
this.scale = this.targetScale;
|
||||
const { visibleSet, bufferSet } = this.getVisibilityState();
|
||||
this.onScaleChange(this.scale, visibleSet, bufferSet);
|
||||
this.updateZoomLabel();
|
||||
}
|
||||
}
|
||||
startZoomAnimation(qualityOverride = null) {
|
||||
if (this.zoomAnimation) {
|
||||
cancelAnimationFrame(this.zoomAnimation);
|
||||
}
|
||||
this.isZooming = true;
|
||||
const startScale = this.scale;
|
||||
const endScale = this.targetScale;
|
||||
const startTime = performance.now();
|
||||
const duration = 150; // 150ms for smooth zoom
|
||||
// Set lower quality during zoom for performance
|
||||
this.zoomQuality = qualityOverride !== null ? qualityOverride : 0.8;
|
||||
const animate = (currentTime) => {
|
||||
const elapsed = currentTime - startTime;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
// Ease-in-out animation for natural feel
|
||||
const easedProgress = this.easeInOutCubic(progress);
|
||||
this.scale = startScale + (endScale - startScale) * easedProgress;
|
||||
// Apply the scale
|
||||
this.applyScaleToContainer();
|
||||
// Notify about scale change with current quality
|
||||
const { visibleSet, bufferSet } = this.getVisibilityState();
|
||||
this.onScaleChange(this.scale, visibleSet, bufferSet);
|
||||
if (progress < 1) {
|
||||
this.zoomAnimation = requestAnimationFrame(animate);
|
||||
}
|
||||
else {
|
||||
this.isZooming = false;
|
||||
this.scale = endScale;
|
||||
this.zoomQuality = 1.0; // Restore full quality
|
||||
// Final high-quality render
|
||||
const { visibleSet, bufferSet } = this.getVisibilityState();
|
||||
this.onScaleChange(this.scale, visibleSet, bufferSet);
|
||||
this.updateZoomLabel();
|
||||
this.zoomAnimation = null;
|
||||
}
|
||||
};
|
||||
this.zoomAnimation = requestAnimationFrame(animate);
|
||||
}
|
||||
applyScaleToContainer() {
|
||||
// Apply CSS transform for smooth zooming
|
||||
this.container.style.transform = `scale(${this.scale})`;
|
||||
// Also update the page manager
|
||||
if (this.pageManager.setScale) {
|
||||
this.pageManager.setScale(this.scale);
|
||||
}
|
||||
}
|
||||
easeInOutCubic(t) {
|
||||
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
||||
}
|
||||
zoomToFit() {
|
||||
if (!this.pageManager || !this.pageManager.viewports)
|
||||
return;
|
||||
const firstViewport = this.pageManager.viewports[0];
|
||||
if (!firstViewport)
|
||||
return;
|
||||
const containerWidth = this.container.clientWidth;
|
||||
const containerHeight = this.container.clientHeight;
|
||||
// Calculate fit scale
|
||||
const scaleW = (containerWidth - 40) / firstViewport.width;
|
||||
const scaleH = (containerHeight - 40) / firstViewport.height;
|
||||
const fitScale = Math.min(scaleW, scaleH);
|
||||
this.applyScale(Math.max(this.minScale, Math.min(this.maxScale, fitScale)));
|
||||
}
|
||||
zoomIn() {
|
||||
this.applyScale(this.scale * 1.25);
|
||||
}
|
||||
zoomOut() {
|
||||
this.applyScale(this.scale * 0.8);
|
||||
}
|
||||
zoomToActualSize() {
|
||||
this.applyScale(1.0);
|
||||
}
|
||||
getCurrentScale() {
|
||||
return this.scale;
|
||||
}
|
||||
getCurrentZoomQuality() {
|
||||
return this.zoomQuality;
|
||||
}
|
||||
isZoomingInProgress() {
|
||||
return this.isZooming;
|
||||
}
|
||||
updateZoomLabel() {
|
||||
if (this.zoomLabel) {
|
||||
this.zoomLabel.textContent = `${Math.round(this.scale * 100)}%`;
|
||||
}
|
||||
}
|
||||
cleanup() {
|
||||
if (this.zoomAnimation) {
|
||||
cancelAnimationFrame(this.zoomAnimation);
|
||||
}
|
||||
}
|
||||
getZoomStats() {
|
||||
return {
|
||||
currentScale: this.scale,
|
||||
targetScale: this.targetScale,
|
||||
isZooming: this.isZooming,
|
||||
zoomQuality: this.zoomQuality
|
||||
};
|
||||
}
|
||||
}
|
||||
// Export for ES6 modules
|
||||
export { EnhancedZoomController };
|
||||
//# sourceMappingURL=zoom-controller-enhanced.js.map
|
||||
1
src-tauri/dist/zoom-controller-enhanced.js.map
vendored
Normal file
1
src-tauri/dist/zoom-controller-enhanced.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user