perf: the header/footer layer draws the pages being read
A long document placed a zone box on every page, and the browser laid all of them out again on every pagination pass. Only a window around the current page carries its zones now; a capture or a printout opens it to the whole document first, so the PDF export and Cmd+P are unchanged. Measured on a 460-page file: settled 6.17 s → 5.27 s (interleaved A/B, median of seven). Spacer, index and text-box geometry byte-identical on five files.
This commit is contained in:
1 parent
2f51292587
commit
e399a3a62c
4 files changed
+35
-2
No files matched your search
@@ -132,7 +132,7 @@ Both anchors differ: a wheel zoom holds the **point under the cursor** fixed (`p
|
||||
|
||||
One header and one footer (`HfDoc` = a single-paragraph TipTap doc per zone, persisted to `edentext-header`/`-footer`), repeated on every page. Double-clicking a page margin — or the Layout-panel "Edit header/footer" buttons — opens the zone for editing; clicking back into the body (`onFocus`) or Escape ends it. The edge→zone distance (header from top, footer from bottom) is user-configurable in cm via the Layout panel (`HfDistances`, persisted to `edentext-hf-distances`, default `HF_DISTANCE_CM` 1.27cm, clamped 0–10 and below the body margin). A **section** can have its own (`HfSet.distances`, `distancesFirst` where its page style hands over): ODF's page-layout margin *is* that distance, Word's is `w:pgMar w:header/w:footer`, and an index whose header starts 4cm down is not the body's 1.25 — `zoneBox` and `sectionReach` both read the section's.
|
||||
- **Fields:** the bar beside the active zone inserts the `pageField.ts` atoms — page number, page count, chapter (the running head, level 1). Each is patched per page, so the same zone shows a different value on every page.
|
||||
- **Rendering:** the zone's own font is the document's **default paragraph style** (`styleCss` gives `.paper .hf-layer .hf-zone` its text half): both formats base the Header/Footer style on it, so a file whose body is Arial 10pt has an Arial footer, not the editor's serif. `HeaderFooterLayer.svelte` mounts inside the scaled `.paper` (like `.band-layer`), positioning per-page zone boxes in unscaled doc px. A zone's out-of-flow frame (`wrap` ≠ inline) is hidden in the box and painted per page by `.hf-bg-layer` at `z-index: -1` — hence `.paper`, not `.tiptap`, paints the page surface, so the image lands between the sheet and the body text. Inactive zones render as static HTML (`generateHTML` + `hfExtensions`); the active zone hosts the single live TipTap editor. `App.svelte`'s `activeEditor`/`activeTick` route the top toolbars to `hfEditor` while a zone is active, so all body formatting works on the header/footer with no toolbar changes.
|
||||
- **Rendering:** the zone's own font is the document's **default paragraph style** (`styleCss` gives `.paper .hf-layer .hf-zone` its text half): both formats base the Header/Footer style on it, so a file whose body is Arial 10pt has an Arial footer, not the editor's serif. `HeaderFooterLayer.svelte` mounts inside the scaled `.paper` (like `.band-layer`), positioning per-page zone boxes in unscaled doc px. A zone's out-of-flow frame (`wrap` ≠ inline) is hidden in the box and painted per page by `.hf-bg-layer` at `z-index: -1` — hence `.paper`, not `.tiptap`, paints the page surface, so the image lands between the sheet and the body text. Inactive zones render as static HTML (`generateHTML` + `hfExtensions`); the active zone hosts the single live TipTap editor. `App.svelte`'s `activeEditor`/`activeTick` route the top toolbars to `hfEditor` while a zone is active, so all body formatting works on the header/footer with no toolbar changes. Only a window of pages around `currentPage` carries its zones (`WINDOW_PAGES`/`WINDOW_STEP`): a 460-page document is a thousand absolutely-placed boxes otherwise, which the browser lays out again on every pagination pass — worth ~0.4 s of the load. The window shifts a step at a time, so scrolling does not rebuild it on every tick, and `pageWindow.svelte.ts`'s `allPagesDrawn` opens it to the whole document for anything that captures the live DOM: `renderPaperToCanvas` (`export/pdf.ts`) before it clones, and `beforeprint`/`afterprint` for a browser printout.
|
||||
|
||||
## Comments (`CommentsPane.svelte`)
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
import { formatOrdinal } from '../utils/orderedListTypes';
|
||||
import { chapterOn, type ChapterStart } from '../utils/chapterField';
|
||||
import { t } from '../i18n/i18n.svelte';
|
||||
import { allPagesDrawn } from './pageWindow.svelte';
|
||||
|
||||
let {
|
||||
headerDoc = $bindable(),
|
||||
@@ -110,7 +111,18 @@
|
||||
: { header: headerDistPx, footer: footerDistPx };
|
||||
}
|
||||
|
||||
let pages = $derived(Array.from({ length: Math.max(1, numPages) }, (_, i) => i + 1));
|
||||
// Only the pages around the one being read carry their zones: a long document's layer
|
||||
// is a thousand absolutely-placed boxes otherwise, laid out again on every pass. The
|
||||
// window shifts a step at a time, so a scroll does not rebuild it on every tick.
|
||||
const WINDOW_STEP = 25;
|
||||
const WINDOW_PAGES = 75;
|
||||
let pages = $derived.by(() => {
|
||||
const total = Math.max(1, numPages);
|
||||
if (allPagesDrawn.on || total <= WINDOW_PAGES) return Array.from({ length: total }, (_, i) => i + 1);
|
||||
const step = Math.floor((currentPage - 1) / WINDOW_STEP) * WINDOW_STEP - WINDOW_STEP + 1;
|
||||
const start = Math.max(1, Math.min(total - WINDOW_PAGES + 1, step));
|
||||
return Array.from({ length: WINDOW_PAGES }, (_, i) => start + i);
|
||||
});
|
||||
|
||||
function zoneBox(zone: HfZone, page: number) {
|
||||
// Mirrored margins: an even page is the left-hand one, so the pair is swapped.
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { flushSync } from 'svelte';
|
||||
|
||||
// The header/footer layer draws only the pages around the one being read. A capture or a
|
||||
// printout takes the live DOM, so both open the window to the whole document first.
|
||||
export const allPagesDrawn = $state({ on: false });
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('beforeprint', () => {
|
||||
allPagesDrawn.on = true;
|
||||
flushSync();
|
||||
});
|
||||
window.addEventListener('afterprint', () => {
|
||||
allPagesDrawn.on = false;
|
||||
});
|
||||
}
|
||||
@@ -2,7 +2,9 @@
|
||||
// print-time re-render drifts text (vector ≠ hinted metrics) and broke the band masks,
|
||||
// so we raster the on-screen render and overlay an invisible, positioned text layer.
|
||||
|
||||
import { tick } from 'svelte';
|
||||
import { generateHTML } from '@tiptap/core';
|
||||
import { allPagesDrawn } from '../components/pageWindow.svelte';
|
||||
import { type Orientation } from '../storage/pageOrientation';
|
||||
import { DEFAULT_MARGINS, PX_PER_CM, type PageMargins } from '../storage/pageMargins';
|
||||
import { pageDimsCm, type PageFormat } from '../storage/pageFormat';
|
||||
@@ -228,7 +230,11 @@ export async function renderPaperToCanvas(opts: PdfOptions, scale = 2): Promise<
|
||||
const { pageW, pageH } = pageDims(opts.pageFormat ?? 'A4', orientation);
|
||||
const cycle = pageH + PAGE_GAP;
|
||||
|
||||
// The header/footer layer draws a window of pages; the capture takes them all.
|
||||
allPagesDrawn.on = true;
|
||||
await tick();
|
||||
const { holder, clone, style } = buildClone(paper, pageW, opts.printMarkup !== false);
|
||||
allPagesDrawn.on = false;
|
||||
document.head.appendChild(style);
|
||||
document.body.appendChild(holder);
|
||||
const cleanup = () => { holder.remove(); style.remove(); };
|
||||
|
||||
Reference in new issue
Block a user