Poster Design: Styles draws the Bless wall panels from the shared panel geometry

wall.bless-panels drew nothing. It now draws the ladder's 5 x 8 ft size
(60x96: three 60 x 36 in sheets, 6 in joins) through panelSheets'
planPanels and screenLayout, the same plan the Bless print uses: the
sheets pulled apart, each join a red glue band with a dashed edge, a
banner on the first sheet, a footer on the last, and rows of portrait
cards inside each sheet's content box, never crossing a join.

The cards are a layout (src/panelsSvg.js): the Bless wall draws its real
cards in its own plugin, which the gallery cannot import. The card is not
offered for printing here.
This commit is contained in:
Doxa Maps 2026-10-01 00:45:15 +00:00
parent 02ba16a7de
commit fd43836920
3 changed files with 86 additions and 6 deletions

View file

@ -25,6 +25,8 @@ import countryData from '@map/data/country-posters.json'
import { makeCountrySheet } from '@map/packet/countrySheet.js'
import { loadPacketStrings } from '@map/packet/packetStrings.js'
import { firstPageImage } from './src/pageThumb.js'
import { panelsSvgUrl } from './src/panelsSvg.js'
import { resolvePosterSize } from '@map/constants/posterSizes.js'
import { listConfigurations, FAMILY_KEYS, SAMPLE_COUNTRY_ID } from './src/templates.js'
import FullDetailsDialog from './src/FullDetailsDialog.vue'
@ -96,7 +98,12 @@ async function packetDocument(variant) {
const doc = R.buildPacket(sample, rows, new Map(), mapSvg, null, null, variant, null)
return { html: doc, page: '.sheet', width: R.W, height: R.H, firstPageOnly: true }
}
const ctx = { countrySheet, cardDeck, packetDocument }
// A multi-sheet poster as its panels, from the shared ladder's sheets for that size.
function panels(sizeKey) {
const size = resolvePosterSize(sizeKey)
return size ? { image: panelsSvgUrl(size.sheets) } : null
}
const ctx = { countrySheet, cardDeck, packetDocument, panels }
// The printed document's size, measured when it is drawn: what "file size" means in the details.
// render() answers a poster's HTML (a map sheet) or a document { html, page, width, height } (a deck, a packet).
@ -105,6 +112,7 @@ async function renderDocument(name) {
const c = byName.get(name)
if (!c || !c.render) return null
const out = await c.render(ctx)
if (out && out.image) return out
const html = typeof out === 'string' ? out : out && out.html
if (!html) return null
if (!(out && out.firstPageOnly)) fileBytes.value = { ...fileBytes.value, [name]: new Blob([html]).size }
@ -117,6 +125,7 @@ async function renderDocument(name) {
async function previewSrc(item) {
const out = await renderDocument(item.id)
if (!out) return null
if (out.image) return out.image
if (typeof out === 'object') return firstPageImage(out.html, out)
const { rasterPoster, looksRendered } = await import('@map/packet/poster-raster.js')
const blob = await rasterPoster(out, { scale: 0.25 })
@ -130,7 +139,7 @@ async function previewSrc(item) {
// packet missing its later pages would misrepresent the template.
async function sheet(item) {
const out = await renderDocument(item.id)
if (!out || (typeof out === 'object' && out.firstPageOnly)) return ''
if (!out || out.image || (typeof out === 'object' && out.firstPageOnly)) return ''
return typeof out === 'string' ? out : out.html
}

View file

@ -0,0 +1,69 @@
/**
* panelsSvg.js: a poster printed on several sheets, drawn as the sheets pulled apart, each join a red glue
* band, as one SVG image for a poster card's thumbnail.
*
* The geometry is the shared panel plan (library/components/poster/panelSheets.js planPanels + screenLayout):
* the same sheets, joins and content boxes the print uses. Inside each sheet's content box sit rows of cards,
* the way the Bless wall fills its 5 × 8 ft panels: a banner on the first sheet, a footer on the last, and
* card rows that never cross a join. The cards are a LAYOUT (blocks, no words): the Bless wall draws its real
* cards in its own plugin, which a library-only gallery cannot import.
*/
import { planPanels, screenLayout } from '@map/components/poster/panelSheets.js'
const PAPER = '#f7f4ec'
const INK = '#3b463d'
const ACCENT = '#6B4A78' // the Bless wall's purple
const PHOTO = '#e9e3d4'
const LINE = '#c9c3b3'
/**
* @param {{ sheetW, sheetH, count, direction, joinIn }} sheets a size's `sheets` from the shared ladder
* @param {{ borderIn?: number, gapIn?: number, cols?: number }} o
* @returns {string} an image URL (a data: URL of an SVG), drawn in inches
*/
export function panelsSvgUrl(sheets, { borderIn = sheets.joinIn || 2, gapIn = 0.75, cols = 14 } = {}) {
const plan = planPanels({ ...sheets, borderIn, joinFill: 'glue' })
const L = screenLayout(plan, { gapIn, pxPerIn: 1 })
const W = L.worldW, H = L.worldH
const s1 = Math.min(plan.sheetW, plan.sheetH)
const stroke = s1 / 200
let out = ''
for (const sh of plan.sheets) {
const at = L.sheets[sh.s]
const ox = at.x, oy = at.y
out += `<rect x="${ox}" y="${oy}" width="${plan.sheetW}" height="${plan.sheetH}" fill="${PAPER}" stroke="${LINE}" stroke-width="${stroke}"/>`
const c = sh.content
let top = oy + c.y0, bottom = oy + c.y1
const cw0 = c.x1 - c.x0
if (sh.first) { // the banner: a title band and the prayer-themes table
const bh = cw0 * 0.075, th = cw0 * 0.17
out += `<rect x="${ox + c.x0}" y="${top}" width="${cw0}" height="${bh}" fill="${INK}"/>`
out += `<rect x="${ox + c.x0}" y="${top + bh + cw0 * 0.01}" width="${cw0}" height="${th}" fill="none" stroke="${ACCENT}" stroke-width="${stroke * 2}"/>`
top += bh + th + cw0 * 0.025
}
if (sh.last) { // the footer: the blessing and the closing line
const fh = cw0 * 0.09
out += `<rect x="${ox + c.x0}" y="${bottom - fh}" width="${cw0}" height="${fh}" fill="${ACCENT}" opacity=".85"/>`
bottom -= fh + cw0 * 0.015
}
// rows of portrait cards (0.8 aspect), centred in the band, never crossing a join
const cw = cw0 / cols, ch = cw / 0.8
const rows = Math.max(0, Math.floor((bottom - top) / ch))
const y0 = top + Math.max(0, (bottom - top) - rows * ch) / 2
for (let r = 0; r < rows; r++) {
for (let k = 0; k < cols; k++) {
const x = ox + c.x0 + k * cw, y = y0 + r * ch
out += `<rect x="${x + cw * 0.08}" y="${y + ch * 0.06}" width="${cw * 0.84}" height="${cw * 0.84}" fill="${PHOTO}"/>`
out += `<rect x="${x + cw * 0.15}" y="${y + ch * 0.06 + cw * 0.9}" width="${cw * 0.7}" height="${ch * 0.06}" fill="${INK}" opacity=".7"/>`
}
}
if (sh.band) { // the glue band: red, dashed at its edge, as PanelSheets draws it
const b = sh.band
const ex = plan.direction === 'vertical' ? `x1="${ox}" y1="${oy + b.h}" x2="${ox + b.w}" y2="${oy + b.h}"` : `x1="${ox + b.w}" y1="${oy}" x2="${ox + b.w}" y2="${oy + b.h}"`
out += `<rect x="${ox + b.x}" y="${oy + b.y}" width="${b.w}" height="${b.h}" fill="rgba(176,0,0,.14)"/>`
out += `<line ${ex} stroke="#b00" stroke-width="${stroke * 1.5}" stroke-dasharray="${stroke * 6} ${stroke * 4}"/>`
}
}
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${H}" width="${(W * 4).toFixed(0)}" height="${(H * 4).toFixed(0)}">${out}</svg>`
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`
}

View file

@ -10,7 +10,8 @@
* render(ctx) draws the template from the LIBRARY with the sample data, so a library change shows up in
* the gallery the next time it opens. It returns either a poster's HTML (a map sheet: an <svg> the poster
* rasteriser lifts) or a printable DOCUMENT as { html, page, width, height } (a deck, a packet: HTML pages,
* whose first page becomes the thumbnail, src/pageThumb.js). Entries the gallery cannot draw yet
* whose first page becomes the thumbnail, src/pageThumb.js) or a ready image as { image } (panels drawn from
* the shared panel geometry, src/panelsSvg.js). Entries the gallery cannot draw yet
* say so (render: null) and the card shows the wall's "preview unavailable" placeholder; the details
* dialog states which part is missing, rather than a picture of something else.
*
@ -96,10 +97,11 @@ export function listConfigurations() {
family: 'wall',
labelKey: 'posterDesign.tpl.blessPanels',
size: '5 × 8 ft, 3 panels',
renderer: 'plugin-designs/wall-of-people-groups/src/blessPoster.js (not in library/ yet)',
settings: {},
renderer: 'library/components/poster/panelSheets.js planPanels (the sheets, joins and content boxes); the cards are drawn by plugin-designs/wall-of-people-groups/src/blessPoster.js',
settings: { size: '60x96', joinFill: 'glue' },
sample: null,
render: null,
// The panel geometry of the ladder's 5 × 8 ft size, with the card rows as a layout (src/panelsSvg.js).
render: (ctx) => ctx.panels('60x96'),
missing: 'posterDesign.missing.bless',
},
{