From e50fc858ba6ac94f86d4806040d7eed78594475d Mon Sep 17 00:00:00 2001 From: doxa-maps-88655c70 <88627970+doxa-maps-88655c70@users.noreply.github.com> Date: Fri, 25 Sep 2026 13:36:32 +0000 Subject: [PATCH] WIP: poster capture no longer dies on a late idle; gate waits 120s --- library/composables/poster/useMapPoster.js | 24 ++++++++++++------- .../scripts/check-export-one-click.mjs | 6 ++--- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/library/composables/poster/useMapPoster.js b/library/composables/poster/useMapPoster.js index 1633f0614..b923c4cfb 100644 --- a/library/composables/poster/useMapPoster.js +++ b/library/composables/poster/useMapPoster.js @@ -410,24 +410,30 @@ async function _spawnHiddenMap(sourceMap, w, h, opts = {}) { return { hiddenMap, restoreDpr, container }; } +// The hidden map's 'idle' can arrive late or never on a heavy render (measured 2026-09-25: an +// Arabic Letter poster, RTL shaping + a 2x canvas, missed a 15s idle and the export gave NO file). +// So: at IDLE_TIMEOUT_MS, capture if every tile is in (something else is only repainting); keep +// waiting to IDLE_HARD_MS otherwise, then capture what is drawn with a warning. A poster whose last +// tiles are missing is still a file; a rejected promise is a dead Export button. +const IDLE_HARD_MS = 60000; function _waitForIdle(map) { - return new Promise((resolve, reject) => { + return new Promise((resolve) => { let settled = false; - const t = setTimeout(() => { + const timers = []; + const finish = (why) => { if (settled) return; settled = true; - reject(new Error('useMapPoster: hidden map idle timeout')); - }, IDLE_TIMEOUT_MS); - const finish = () => { - if (settled) return; - settled = true; - clearTimeout(t); + timers.forEach(clearTimeout); + if (why) console.warn('[useMapPoster] ' + why); requestAnimationFrame(() => resolve()); }; + const tilesIn = () => { try { return typeof map.areTilesLoaded === 'function' && map.areTilesLoaded(); } catch (_) { return false; } }; + timers.push(setTimeout(() => { if (tilesIn()) finish(); }, IDLE_TIMEOUT_MS)); + timers.push(setTimeout(() => finish('hidden map not idle after ' + IDLE_HARD_MS / 1000 + 's; capturing what is drawn'), IDLE_HARD_MS)); if (map.loaded() && !map.isMoving()) { finish(); } else { - map.once('idle', finish); + map.once('idle', () => finish()); } }); } diff --git a/plugin-designs/doxa-research-map/scripts/check-export-one-click.mjs b/plugin-designs/doxa-research-map/scripts/check-export-one-click.mjs index f1f2356ed..762de8fa0 100644 --- a/plugin-designs/doxa-research-map/scripts/check-export-one-click.mjs +++ b/plugin-designs/doxa-research-map/scripts/check-export-one-click.mjs @@ -14,7 +14,7 @@ * The drawer's "Export poster…" does not count: the drawer is CLOSED by default (translated off * screen), so it costs an extra click to reach — measured 2026-09-25, and in Arabic the button * could not be clicked at all. The toolbar button must be on screen with no preparation; - * 2. a download arrives within 90s, WITHOUT any further click; + * 2. a download arrives within 120s, WITHOUT any further click; * 3. the file is a real PNG (signature) of the print size (US Letter at 200 DPI = 1700x2200); * 4. 0 page errors. * @@ -71,7 +71,7 @@ try { const label = ((await btn.getAttribute('title', { timeout: 20000 }).catch(() => '')) || '').trim() let clicks = 0 try { await btn.click({ timeout: 20000 }); clicks = 1 } catch (e) { errs.push('could not click: ' + String(e.message || e).split('\n')[0]) } - for (let i = 0; i < 90 && !downloads.length; i++) await p.waitForTimeout(1000) + for (let i = 0; i < 120 && !downloads.length; i++) await p.waitForTimeout(1000) let file = null, info = '' if (downloads.length) { file = downloads[0].suggestedFilename() @@ -83,7 +83,7 @@ try { if (!png || w < 1600 || h < 2100) errs.push(`file is not the expected print PNG (${info})`) } const ok = clicks === 1 && !!file && !errs.length - console.log(`${ok ? 'PASS' : 'FAIL'} [${where} ${lang}] "${label}" · 1 click → ${file ? `${file} (${info})` : 'NO FILE within 90s'}${errs.length ? ' · ' + [...new Set(errs)].slice(0, 3).join(' | ') : ' · 0 errors'}`) + console.log(`${ok ? 'PASS' : 'FAIL'} [${where} ${lang}] "${label}" · 1 click → ${file ? `${file} (${info})` : 'NO FILE within 120s'}${errs.length ? ' · ' + [...new Set(errs)].slice(0, 3).join(' | ') : ' · 0 errors'}`) if (!ok) fails.push(`${where} ${lang}`) await ctx.close() }