Packet print at reading size: the prayer points flow over two sheets at ~9pt, and Full detail's tally heads take two lines over the worksheet's 160px columns

Points: text 22.5px (~9.2pt on letter), headings 25px, the Mark 5 passage 20px, the themes 21px; the themes and
points 1–6 on the first sheet, 7–12 and the credit on the second, each titled with its range. Measured in all 11
languages: the fullest first sheet keeps ~330px. Full detail: tally columns 160px (the worksheet's), heads 15px
in two lines in every language, the first-page block 720px (the tallest, ru, uses 692), 11 rows on the first page.
Gates: the render gate measures BOTH points sheets (pointsOver), counts the points by .pt-h across the document,
expects 2 points sheets; the structure gate reads consecutive points sheets as one entry; profile-table probes the
15px tier between the two budgets wherever they fall.
This commit is contained in:
Doxa Maps 2026-10-02 22:50:16 +00:00
parent 2e0422f6fa
commit 4d4fa4f6d0
4 changed files with 34 additions and 21 deletions

View file

@ -78,7 +78,7 @@ function sequence(html) {
: /word-title/.test(seg) ? 'word' // point 4's passage in full + the Father's-heart list (2026-09-30)
: /points-title|points_title|Prayer Points/.test(seg) ? 'points'
: `?${cls.trim()}`
if ((kind === 'profiles' || kind === 'worksheet') && out[out.length - 1] === kind) return // one entry per run
if ((kind === 'profiles' || kind === 'worksheet' || kind === 'points') && out[out.length - 1] === kind) return // one entry per run
out.push(kind)
})
return out.join(' · ')

View file

@ -279,8 +279,11 @@ const r = await page.evaluate(() => {
return Math.round(Math.max(0, ...[...b.children].map((k) => k.getBoundingClientRect().bottom + parseFloat(getComputedStyle(k).marginBottom))) - top - b.clientHeight) })(),
// The points page is ONE sheet by design and fits only because its sizes were tuned to the longest locale;
// measure its content against its box (studio's ask), as fdTopOver does for the list's first page.
pointsOver: (() => { const b = document.querySelector('.points-body'); if (!b) return null; const top = b.getBoundingClientRect().top
return Math.round(Math.max(b.scrollHeight - b.clientHeight, ...[...b.children].map((k) => k.getBoundingClientRect().bottom - top - b.clientHeight))) })(),
// The points flow over TWO sheets (2026-10-02); each sheet's body is measured, the worst one reported.
pointsOver: (() => { const bs = [...document.querySelectorAll('.points-body')]; if (!bs.length) return null
return Math.max(...bs.map((b) => { const top = b.getBoundingClientRect().top
return Math.round(Math.max(b.scrollHeight - b.clientHeight, ...[...b.children].map((k) => k.getBoundingClientRect().bottom - top - b.clientHeight))) })) })(),
pointsSheets: document.querySelectorAll('.sheet-points').length,
funnelTip: document.querySelectorAll('.funnel-tip').length,
lifeShaded: (() => { const g = document.querySelector('.sheet-christian .lc-svg'); return g ? window.__lcBoxes(g).shaded.length : null })(),
// ★ FILLED FROM THE TOP, in reading order (the packet's design), measured on the DOM order of the cells:
@ -456,7 +459,7 @@ const r = await page.evaluate(() => {
if (!host) return -1
// Each point's heading carries class="pt-h": a numbered LINE is not a point. German cites "1. Petrus 5:6–11"
// (its own name for 1 Peter), and that reference line was counted as a thirteenth point.
const heads = host.querySelectorAll('.pt-h')
const heads = document.querySelectorAll('.pt-h')
if (heads.length) return heads.length
return (host.innerText || '').split('\n').filter((l) => /^\s*\d{1,2}\.\s/.test(l)).length
})(),
@ -783,7 +786,9 @@ if (r.fdTopOver > 0) fails.push(`the Full detail's explanation block outgrows it
if (r.wsRows && r.wsSheets !== wsPages(r.wsRows)) fails.push(`${r.wsRows} worksheet rows should take ${wsPages(r.wsRows)} page(s) but the packet has ${r.wsSheets}`)
// + 2 again (2026-09-30, the second revision): point 4's Mark 5:18-20 and the Father's-heart list are back ON the
// points page, so the back matter is the wheel and the points.
const expectedSheets = 1 + lifeSheets + funnelSheets + listSheets + setsSheets + 2
// + 3 (2026-10-02): the wheel, and the points on TWO sheets (themes + points 1–6, then 7–12), read at print size.
if (r.pointsSheets !== 2) fails.push(`the prayer points should flow over 2 sheets, found ${r.pointsSheets}`)
const expectedSheets = 1 + lifeSheets + funnelSheets + listSheets + setsSheets + 3
// ── THE LIFE-CALENDAR PAGES, asserted rather than merely counted ──────────────────────────────────
// A page that EXISTS is not a page that is RIGHT: an empty grid, a grid of the wrong size, or a
// christian page whose shading silently fell to zero would all still be one sheet. 5,200 is the

View file

@ -121,7 +121,9 @@ const probe = (txt) => {
const words = (n) => 'word '.repeat(n).trim()
ok(probe(words(10)).px === 17, 'a short description did not render at the full 17px')
ok(probe(words(Math.floor(budget(17) / 5))).px === 17, 'a description just inside the 17px budget stepped down anyway')
ok(probe(words(Math.floor(budget(17) / 5) + 6)).px === 15,
// Between the 17px and the 15px budgets, wherever they fall for this row width: '+6 words' was 30 characters, and
// when the tally cells widened the gap between the two budgets shrank to 24, so the probe skipped 15 for 14.
ok(probe(words(Math.floor((budget(17) + budget(15)) / 2 / 5))).px === 15,
'a description over the 17px budget did not step to 15px')
const long = probe(words(Math.floor(budget(14) / 5) + 40))
ok(long.px === 14, `a very long description did not clamp at the smallest size (got ${long.px}px)`)

View file

@ -131,13 +131,13 @@ export function createPacketRenderer({ strings, lang = 'en', fontData = null, de
// The tally cells no longer carry their question under each box: the three questions head the table ONCE
// per page (.pray-head), so a cell is all tally room. 84 -> 116 gives every box more of it and the heads a
// readable 15px (three short lines; at 96px they were three tiny ones).
const PRAY_CELL = 116;
const PRAY_CELL = 160;
// The question heads above the first row of every page, and the first page's explanation (the intro,
// the blessing, the band) in a reserved block, so the first page holds FD_FIRST rows and the rest 20.
// FD_TOP_H is MEASURED: with a 100-group list the tallest locales' block uses 612px (es fr pt de it ro ru hi;
// en 591, ar 507, zh 513), so 640 keeps 28px; with the 62px head row the first page holds 11 rows. check:packet-render fails a block
// whose content outgrows it (the block clips rather than overlap the rows, so only a measurement sees it).
const PRAY_HEAD_H = 62, FD_TOP_H = 640;
const PRAY_HEAD_H = 62, FD_TOP_H = 720;
const FD_ROWS_TOP = M + HEAD + FD_TOP_H + PRAY_HEAD_H;
const SET_PANEL = 660; // one square sets panel: two + a 40px gap fit 1380px
const SET_SOLO = 960; // one chart per page: a large square, centred
@ -374,7 +374,7 @@ export function createPacketRenderer({ strings, lang = 'en', fontData = null, de
/* The three questions, ONCE per page, over the tally cells' own columns. */
.pray-head{position:absolute;inset-inline-end:${M}px;display:grid;grid-template-columns:repeat(${PRAY_COLS.length},${PRAY_CELL}px);
height:${PRAY_HEAD_H - 6}px;align-items:end;font:700 15px/1.2 Georgia,serif;color:#6f6a5c;text-align:center}
.pray-head span{text-wrap:balance;padding:0 3px 5px;border-bottom:2px solid #3b463d}
.pray-head span{text-wrap:balance;padding:0 0 5px;border-bottom:2px solid #3b463d}
.fd-top{position:absolute;left:${M}px;top:${M + HEAD}px;width:${CW.toFixed(0)}px;height:${FD_TOP_H}px;overflow:hidden}
.hd-r{position:absolute;right:${M}px;top:54px;font:400 19px Georgia,serif;color:#8c8170}
/* ⚠ box-sizing:border-box IS LOAD-BEARING HERE, NOT HOUSEKEEPING. Without it the row's height
@ -1352,11 +1352,13 @@ export function createPacketRenderer({ strings, lang = 'en', fontData = null, de
// Mark 5:18-20 and the Father's-heart list share this ONE sheet, in every locale: the sizes below are the
// largest that fit the longest locale. class="points-body" is a TEST HOOK: the page must fit ONE sheet in
// every locale (its scrollHeight within its clientHeight).
// ★ 2026-10-02: the prayer themes now head the page at a size that reads on paper (21px, ~8.6pt on letter) and
// the credit line sits 0.28in above the sheet's edge, so the points give back a little: 20 → 18px text, 10 → 5px
// between points, the Mark 5 passage 18 → 16px, and the body starts 14px higher (the heading stays 22). Measured
// in all 11 locales: the tightest (fr, hi) keep 46px under the last point; check:packet-render fails overflow.
const PT = { head: 22, sub: 19, text: 18, refs: 16, gap: 5, passage: 16 };
// ★ READABLE ON PAPER, ON TWO SHEETS (2026-10-02): the point text at 22.5px (~9.2pt on letter), the themes at
// 21px (~8.6pt), the Mark 5 passage at 20px; that no longer fits one sheet, so the points FLOW onto a second: the
// themes and points 1–6 (point 4 carries its passage and the Father's-heart list) on the first, 7–12 on the
// second, which carries the credit line. Measured in all 11 locales: the fullest first sheet (fr, de) keeps
// ~350px; check:packet-render fails either sheet's overflow (pointsOver).
const PT = { head: 25, sub: 21, text: 22.5, refs: 18.5, gap: 18, passage: 20 };
const POINTS_FIRST = 6;
// ⚠ ISOLATE EACH REFERENCE FOR BIDI. Scripture references stay in English
// (a reader looks them up in their own Bible), so on an RTL page they are
// LTR runs inside an RTL paragraph. Without isolation the Unicode bidi
@ -1405,14 +1407,18 @@ export function createPacketRenderer({ strings, lang = 'en', fontData = null, de
<div style="font:600 ${PT.refs}px Georgia,serif;color:#7a3b2e;margin-top:2px">${refs}</div>
${p.print_passage ? passageBlock() + heartBlock() : ''}
</div>`;
}).join('');
return sheet(`
});
// Each sheet names its range (1–5, 6–12) beside the title, in the language's digits.
const head = (from, to, intro) => `
<div style="position:absolute;left:${M}px;top:44px;right:${M}px">
<div class="points-title" style="font:700 40px Georgia,serif;color:#1b1a17">${esc(t('points_title'))}</div>
<div style="font:italic 18px Georgia,serif;color:#6d766e;margin-top:6px">${esc(t('points_intro'))}</div>
</div>
<div class="points-body" style="position:absolute;left:${M}px;top:${M + 82}px;right:${M}px;bottom:80px">${themesBlock()}${rows}</div>
<div class="fine">${esc(t('points_attrib'))}</div>`);
<div class="points-title" style="font:700 40px Georgia,serif;color:#1b1a17">${esc(t('points_title'))}
<span class="pt-range" style="font:400 24px Georgia,serif;color:#8c8170">${num(from)}–${num(to)}</span></div>
${intro ? `<div style="font:italic 18px Georgia,serif;color:#6d766e;margin-top:6px">${esc(t('points_intro'))}</div>` : ''}
</div>`;
const body = (html, top) => `<div class="points-body" style="position:absolute;left:${M}px;top:${top}px;right:${M}px;bottom:80px">${html}</div>`;
return sheet(`${head(1, POINTS_FIRST, true)}${body(themesBlock() + rows.slice(0, POINTS_FIRST).join(''), M + 96)}`, 'sheet-points')
+ '\n' + sheet(`${head(POINTS_FIRST + 1, POINTS.length, false)}${body(rows.slice(POINTS_FIRST).join(''), M + 70)}
<div class="fine">${esc(t('points_attrib'))}</div>`, 'sheet-points');
}
// ★ THE HEAVENLY FATHER'S HEART (2026-09-30): a plain statement of what they need to know (point4_heart_lede,