From 626b1df0f34b4a910be79137d695a682dc294d92 Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Sat, 3 Oct 2026 14:22:10 +0000 Subject: [PATCH] A print's progress moves as the work does: the poster reports each real step (people groups, map outlines, engine, then every drawing stage), yields a frame between them so the page repaints, and shows step n of N with the elapsed seconds instead of fixed percentages. The sheet's loads start together, and the leader-crossing count skips disjoint segment pairs and is no longer counted twice. Every sheet stays byte-identical (286/286). --- library/components/poster/PosterWall.vue | 41 +++++++++++++++++-- library/i18n/locales/en/wall.json | 18 +++++++- library/packet/countrySheet.js | 10 ++++- library/packet/poster-render.integrity.js | 4 +- library/packet/poster-render.js | 27 +++++++++++- library/packet/posterSheets.js | 18 ++++++-- .../country-posters/country-poster-wall.vue | 4 +- .../country-posters/src/indiaStateSheet.js | 7 +++- 8 files changed, 111 insertions(+), 18 deletions(-) diff --git a/library/components/poster/PosterWall.vue b/library/components/poster/PosterWall.vue index de0946649..5bbf1fda2 100644 --- a/library/components/poster/PosterWall.vue +++ b/library/components/poster/PosterWall.vue @@ -316,6 +316,31 @@ const countOf = (key, n) => tCount(key, { n: Number(n || 0).toLocaleString() }, // The HUD's kinds and progress messages, and the poster image's alt text (what a screen reader announces // for a card), in the page's language; English only where a key is absent. The same translator goes to // usePosterPrints (opts.t) so the messages IT sets are localized too. +// ★ A PRINT'S REAL STEPS (the Driver: a bar that moves as the work moves, never frozen, no invented percentages). +// The sheet reports each step by name (countrySheet / posterSheets / renderPoster's onStep), the bar is the share of +// the KNOWN steps done, and the elapsed seconds tick on their own whenever the page can paint. +const PRINT_STEPS = ['records', 'boundaries', 'generator', 'map-frame', 'neighbours', 'seas', 'pins', 'side-lanes', 'cities', 'names', 'header', 'print'] +const STEP_EN = { + records: 'loading the people groups', boundaries: 'loading the map outlines', generator: 'loading the poster engine', + 'map-frame': 'fitting the map', neighbours: 'drawing the neighbouring provinces', seas: 'naming the seas', + pins: 'placing the pins', 'side-lanes': 'laying out the side lanes', cities: 'adding the cities', + names: 'placing the names', header: 'writing the header', print: 'opening the print dialog', +} +const stepKey = (name) => name.replace(/-([a-z])/g, (m, c) => c.toUpperCase()) +const nowMs = ref(Date.now()) +let elapsedTick = null +const elapsedS = computed(() => (exportState.value && exportState.value.startedAt ? Math.max(0, Math.floor((nowMs.value - exportState.value.startedAt) / 1000)) : 0)) +function printStepper() { + let done = 0 + return (name) => { + const i = PRINT_STEPS.indexOf(name) + if (i < 0 || !exportState.value) return + done = Math.max(done, i + 1) + exportState.value = { ...exportState.value, phase: 'render', message: hud(`wall.hud.step.${stepKey(name)}`, STEP_EN[name]), + stepN: done, stepM: PRINT_STEPS.length, pct: Math.round((100 * done) / PRINT_STEPS.length) } + } +} + const hud = (key, en, params = null, n = undefined) => { const v = tCount(key, params || {}, n) return v && v !== key ? v : String(en).replace(/\{(\w+)\}/g, (m, k) => (params && params[k] != null ? String(params[k]) : m)) @@ -556,6 +581,12 @@ function showOnMap(item) { // than by keeping two renderers in sync, and it works on Android where the Node // generator cannot run at all. const exportState = ref(null) +// the elapsed seconds tick for the life of a print's HUD (declared here: the watch reads exportState at setup) +watch(() => exportState.value && exportState.value.startedAt, (st) => { + clearInterval(elapsedTick); elapsedTick = null + if (st) { nowMs.value = Date.now(); elapsedTick = setInterval(() => { nowMs.value = Date.now() }, 250) } +}) +onBeforeUnmount(() => { clearInterval(elapsedTick) }) const { scrimRef, scrimStyle } = useViewportScrim(exportState) async function exportPoster(item) { @@ -566,7 +597,8 @@ async function exportPoster(item) { return } exportState.value = { label: item.label, kind: hud('wall.hud.kindPoster', 'Poster'), phase: 'fetch', - message: hud('wall.hud.fetchingPoster', 'fetching the poster'), pct: 20 } + message: hud('wall.hud.fetchingPoster', 'fetching the poster'), pct: 0, startedAt: Date.now() } + const step = printStepper() try { // ★ GENERATED FIRST (the print standard): a host that can draw the sheet now (the `sheet` prop, // packet/posterSheets.js) prints or downloads THAT — no stored file involved, so a country, which has @@ -574,15 +606,16 @@ async function exportPoster(item) { // for a home printer), the item's own poster size for a download. A generation that fails falls through // to the stored image and HTML below rather than to an error. if (props.sheet) { - exportState.value = { ...exportState.value, phase: 'render', message: hud('wall.hud.generatingPoster', 'generating the poster'), pct: 35 } + exportState.value = { ...exportState.value, phase: 'render', message: hud('wall.hud.generatingPoster', 'generating the poster') } const format = props.posterAction === 'print' ? 'letter' : (item.sheet === '3x3' ? '3x3' : '3x4') - const html = await Promise.resolve().then(() => props.sheet(item, format)).catch(() => '') + const html = await Promise.resolve().then(() => props.sheet(item, format, { onStep: step })).catch(() => '') if (html) { const { rasterPoster, looksRendered, hasExternalImages } = await import('@map/packet/poster-raster.js') // ★ A SHEET WITH PHOTOS PRINTS AS ITS OWN DOCUMENT (the large side-lane sheets): a raster cannot carry a // photo from its own host (poster-raster.js says why), and the sheet's own @page is its true size // (60in 96in), where an image of it would be shrunk onto letter. A download still rasterises, without them. if (props.posterAction === 'print' && hasExternalImages(html)) { await printSheetDocument(html); return } + step('print') const blob = await rasterPoster(html, { scale: 2 }) if (await looksRendered(blob)) { await deliverPoster(blob, item); return } } @@ -1377,7 +1410,7 @@ provide(POSTER_WALL_CTX, {