Replace js by ts

This commit is contained in:
2026-04-01 01:27:51 +02:00
parent 4613b8e5dd
commit 6306c73f26
291 changed files with 501210 additions and 525 deletions

View File

@@ -0,0 +1,122 @@
/**
* AnnotationLayer — per-page SVG overlays for text highlight annotations.
*
* Each page wrapper gets one <svg class="annot-layer"> absolutely positioned
* over the canvas. Highlight quads (in PDF coordinate space, origin
* bottom-left) are converted to CSS pixel coordinates using the simple
* transform: cx = x * scale, cy = (vpHeight - y) * scale
* which is correct for upright (non-rotated) pages.
*
* Annotations from the model arrive as snake_case JSON matching the Rust
* field names (annotation_type.type = "textmarkup", etc.).
*/
export class AnnotationLayer {
/**
* @param {HTMLElement[]} wrappers - PageManager.pageWrappers (0-indexed array)
* @param {Array<{width:number,height:number}>} viewports - scale=1 page dims
* @param {Function} onAnnotationClick - (annotationId: string) => void
*/
constructor(wrappers, viewports, onAnnotationClick) {
this._wrappers = wrappers;
this._viewports = viewports;
this._onAnnotationClick = onAnnotationClick;
this._annotations = [];
this._scale = 1;
this._selectedId = null;
}
/**
* Replace the full annotation list and re-render every page overlay.
*
* @param {object[]} annotations - Annotation objects from the backend
* @param {number} scale - Current CSS display scale
*/
setAnnotations(annotations, scale) {
this._annotations = annotations || [];
this._scale = scale;
for (let i = 0; i < this._wrappers.length; i++) {
this._renderPage(i + 1);
}
}
/** Called by viewer when zoom debounce fires; re-renders all overlays. */
onScaleChange(scale) {
this._scale = scale;
for (let i = 0; i < this._wrappers.length; i++) {
this._renderPage(i + 1);
}
}
/**
* Mark one annotation as visually selected (adds .selected CSS class).
* Pass null to clear the selection.
*
* @param {string|null} id
*/
selectAnnotation(id) {
if (this._selectedId) {
document.querySelectorAll(`.annot-layer [data-id="${CSS.escape(this._selectedId)}"]`)
.forEach(el => el.classList.remove("selected"));
}
this._selectedId = id;
if (id) {
document.querySelectorAll(`.annot-layer [data-id="${CSS.escape(id)}"]`)
.forEach(el => el.classList.add("selected"));
}
}
// ── Private ────────────────────────────────────────────────────────────────
_renderPage(pageNum) {
const i = pageNum - 1;
const wrap = this._wrappers[i];
if (!wrap) return;
// Remove any existing overlay for this page
wrap.querySelector(".annot-layer")?.remove();
const vp0 = this._viewports[i];
const scale = this._scale;
const cssW = vp0.width * scale;
const cssH = vp0.height * scale;
// Collect highlights for this page (page field is 0-indexed in the model)
const highlights = this._annotations.filter(a =>
a.page === i &&
a.annotation_type?.type === "textmarkup" &&
a.annotation_type?.markup_type === "highlight"
);
if (highlights.length === 0) return;
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.classList.add("annot-layer");
svg.setAttribute("viewBox", `0 0 ${cssW} ${cssH}`);
svg.style.width = cssW + "px";
svg.style.height = cssH + "px";
for (const ann of highlights) {
for (const quad of ann.annotation_type.quads ?? []) {
const pts = (quad.points ?? []).map(pt => {
const cx = pt.x * scale;
const cy = (vp0.height - pt.y) * scale;
return `${cx},${cy}`;
}).join(" ");
const polygon = document.createElementNS("http://www.w3.org/2000/svg", "polygon");
polygon.setAttribute("points", pts);
polygon.classList.add("highlight");
polygon.dataset.id = ann.id;
if (this._selectedId === ann.id) polygon.classList.add("selected");
polygon.addEventListener("click", ev => {
ev.stopPropagation();
this._onAnnotationClick?.(ann.id);
});
svg.appendChild(polygon);
}
}
wrap.appendChild(svg);
}
}