Replace js by ts
This commit is contained in:
122
src-tauri/assets/viewer/annotation-layer.js
Normal file
122
src-tauri/assets/viewer/annotation-layer.js
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user