feat(book): J4.c.2 — threaded text exports to PDF on designed pages

renderBookDesignedPageToCanvas now walks page layers after the
master + page layers are drawn, finds text frames whose thread head
holds a placed chapter, paginates via _paginateChapterIntoThread,
and rasterises each frame's slice onto the export canvas at the
frame's coordinates.

New helper _drawFrameBlocksToCanvas — stripped-down counterpart to
the block-painting loop in renderBookReflowPhysicalPageToCanvas.
Handles paragraphs, headings, lists, blockquotes, code blocks, hr,
and plain text — same things the editor's _paintFrameBlocks paints
to DOM. Tables in threaded frames + real-PDF textOps emission (for
selectable text in the PDF) are deferred to a J4.b2 cleanup.

PDF output should now contain the threaded chapter text where the
user authored their frames, not just the dashed text-frame outline.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.7 committed 2026-06-04 20:34:46 -04:00
1 parent 9d8be7e0fe
commit 10d9e51155
1 file changed
+67
+67
View File
@@ -27990,8 +27990,75 @@
.slice().sort((a, b) => (a.z || 0) - (b.z || 0));
const subPage = applyBookTokensToLayers(pageLayers, ctxToken);
for (const l of subPage) await drawLayerToCanvas(ctx, l, dpi, offX, offY);
// J4.c.2 — threaded chapter text: for any text-frame layer on
// this page that participates in a chain whose head has a
// sourceChapter, paint its slice of the chapter's pagination
// on top of everything else (above the frame outline, below
// any layer the user explicitly placed over it).
for (const layer of pageLayers) {
if (layer.type !== 'shape' || layer.shape?.kind !== 'text-frame') continue;
const chain = _bookCollectThreadChain(layer);
const head = chain[0];
if (!head?.sourceChapter) continue;
const myIndex = chain.indexOf(layer);
let paginated;
try { paginated = _paginateChapterIntoThread(head, book); }
catch (err) { console.warn('PDF thread paginate failed', err); continue; }
const page = paginated.pages[myIndex];
if (!page || !page.blocks?.length) continue;
_drawFrameBlocksToCanvas(ctx, page, layer, dpi, bleed);
}
return canvas;
}
// J4.c.2 — paint a paginated frame's blocks onto a PDF-export
// canvas at the frame's coordinates. Stripped-down counterpart
// to the block-painting loop in renderBookReflowPhysicalPageToCanvas
// (no tables, no real-PDF textOps emission yet — both can land
// in J4.b2 / J4.c follow-ups). For now: paragraphs, headings,
// lists, blockquotes, code blocks, hr, plain text — same things
// _paintFrameBlocks paints in the editor.
function _drawFrameBlocksToCanvas(ctx, page, frameLayer, dpi, bleed) {
const padding = Number(frameLayer.shape?.padding) || 0;
// Frame x/y are sheet-relative (bleed-inclusive). Subtract bleed
// to get trim-relative inches, matching the canvas's coordinate
// system (canvas covers trim only).
const innerX = (Number(frameLayer.x) || 0) + padding - bleed;
const innerY = (Number(frameLayer.y) || 0) + padding - bleed;
const innerW = Math.max(0.1, (Number(frameLayer.w) || 0) - 2 * padding);
for (const block of (page.blocks || [])) {
const style = block.style || {};
const fontSizePt = Number(style.fontSize) || 11;
const fontPx = fontSizePt * (dpi / 72);
const family = style.fontFamily || 'Georgia, serif';
const weight = Number(style.fontWeight) || 400;
const styleAttr = style.fontStyle || 'normal';
const marginLeftIn = Number(style.marginLeft) || 0;
const marginRightIn = Number(style.marginRight) || 0;
const innerW2 = Math.max(0.1, innerW - marginLeftIn - marginRightIn);
const xStartIn = innerX + marginLeftIn;
const lineHIn = fontSizePt * (Number(style.lineHeight) || 1.5) / 72;
const align = style.alignment || 'left';
const color = style.color || '#111';
// Skip table blocks for now — they render to raster via the
// existing engine's paintReflowPageOverlay; threaded-PDF
// table support follows in a J4.b2 cleanup.
if (block.tokenType === 'table') continue;
ctx.font = `${styleAttr} ${weight} ${fontPx}px ${family}`;
ctx.fillStyle = color;
ctx.textBaseline = 'top';
ctx.textAlign = align === 'center' ? 'center' : align === 'right' ? 'right' : 'left';
const blockYPx = (innerY * dpi) + (Number(block.y) || 0) * dpi;
const lineH = fontPx * (Number(style.lineHeight) || 1.5);
for (let i = 0; i < (block.lines || []).length; i++) {
let xPx;
if (align === 'center') xPx = (xStartIn + innerW2 / 2) * dpi;
else if (align === 'right') xPx = (xStartIn + innerW2) * dpi;
else xPx = xStartIn * dpi;
ctx.fillText(block.lines[i], xPx, blockYPx + i * lineH);
}
}
}
// Render one physical page of a reflow segment: master underlay
// (with the text-frame outline stripped) + paginated text blocks
// drawn via fillText at the frame's coordinates.