08 — Print System
A vector PDF atlas engine for A4, A3 and Letter. Multiple print modes (GM, Player, Battle, Poster, Atlas). Embedded fonts, bleed, crop marks. Optional print-shop marketplace integration for nearby print shops with QR pickup codes.
0. Where we actually are (refreshed 2026-09-06)
The sections below describe the intended print design. This block records what is actually on
main, verified againstpackages/print*,apps/api/src/routes/print*.ts,apps/web/src/app/print-shopsandgit log(PRs #73–#113). Ground truth for milestones issuperpowers/STATUS.md.
| Design element | Status | Where |
|---|---|---|
| Layout engine + vector output (§2, §3) | ✅ SHIPPED (tags spec-4-print-complete, spec-4-print-slice-2-complete, spec-4-themes-borders-and-print-complete) | packages/print-engine/src/{layout,renderer,svg-emitter,png}.ts (v0.2.0); packages/print/src/index.ts (card/section page breaking, breakMode per-card or per-section); POST /print/compile in apps/api/src/routes/print.ts. |
| Five named print modes (§1, §4 — GM / Player / Battle / Poster / Atlas) | ❌ Not built as named modes | No PrintMode enum or per-mode content filtering exists in packages/print* or the compile route. Atlas-style multi-map output is covered by the Atlas feature (tag spec-6-complete, /atlas/[worldId]), not by a print mode. |
| PNG / image export (§5) | ✅ SHIPPED | packages/print-engine/src/png.ts; editor <ExportMenu>. |
| Print-shop directory (§6) | ⚠️ Stub with real distance math | GET /print-shops?near=lat,lng&radius_km= (PR #73 stub, 2026-08-23) now filters by haversine distance on a hard-coded in-memory shop list (PR #111, 2026-08-25). No real shop registry / onboarding. |
| Print orders + QR pickup (§6) | ✅ MVP | POST /print-orders, GET /print-orders, GET /print-orders/:id, GET /print-orders/:id/qr (apps/api/src/routes/print-shops.ts); DB-persisted in print_orders (PR #76, migration 0024); audit row on create (PR #79). |
/print-shops UI | ✅ MVP | apps/web/src/app/print-shops/page.tsx (PR #74) with "Your recent orders" (PR #77) and browser geolocation feeding the distance filter via hooks/use-geolocation.ts (PR #112). E2E: e2e/print-shop-order.spec.ts (PR #92) + geo-status branches (PR #113). Audit flow print-shop-order enrolled in the testid gate (PR #110). |
| Payments, shop notifications, pickup email/push (§6) | ❌ Not started | No Stripe / payment provider code in the print path; no notification transport. |
| Print preview (§7), print profiles (§8) | ❌ Not built | — |
1. Print modes
Every world can be exported as a PDF atlas in five modes:
| Mode | Use case | Features |
|---|---|---|
| GM | Game master reference | full grid, hidden notes, secret doors, NPC names |
| Player | Handout to players | clean version, no grid, no secret info |
| Battle | Tactical combat | grid visible, scale ruler, no labels |
| Poster | Wall art | no grid, no labels, decorative border |
| Atlas | Multi-page book | index page, legend, coordinates, scale |
Each mode produces a different PDF with different visible content.
2. Layout engine
The layout engine slices a world into printable pages.
2.1 Paper sizes
| Size | Width × Height (pt) | Width × Height (mm) |
|---|---|---|
| A4 | 595 × 842 | 210 × 297 |
| A3 | 842 × 1191 | 297 × 420 |
| Letter | 612 × 792 | 216 × 279 |
2.2 Inputs
interface PrintLayoutOptions {
paperSize: 'A4' | 'A3' | 'Letter';
orientation: 'portrait' | 'landscape';
scale: number; // e.g. 1 hex = 1 inch
marginsMm: number; // default 5
bleedMm: number; // default 3
overlapMm: number; // overlap between adjacent pages, default 10
includeGrid: boolean;
includeCoords: boolean;
includeLegend: boolean;
includeCover: boolean;
includeIndex: boolean;
mode: 'gm' | 'player' | 'battle' | 'poster' | 'atlas';
theme?: string; // optional theme override
title?: string;
author?: string;
}
2.3 Output
The engine produces a multi-page PDF:
Page 1: Cover (title + hero shot)
Page 2: Index (list of regions + page numbers)
Page 3..N: Atlas pages
Last page: Legend + scale + credits
Each atlas page:
- Contains the tile window for its grid bounds.
- Has crop marks at the corners.
- Has a 10 mm overlap band on the right + bottom edges with assembly instructions.
- Has a small footer with page number, world title, and a "Page X of N" indicator.
- Includes a 3 mm bleed on all edges.
2.4 Calculation
function layoutPages(world: World, opts: PrintLayoutOptions): Page[] {
const paper = PAPER_SIZES[opts.paperSize];
const printableMm = {
width: paper.mm.width - 2 * (opts.marginsMm + opts.bleedMm),
height: paper.mm.height - 2 * (opts.marginsMm + opts.bleedMm),
};
// world extents in mm at the given scale
const worldMm = {
width: world.grid.size.cols * world.grid.hexSize * 2 * opts.scale,
height: world.grid.size.rows * world.grid.hexSize * Math.sqrt(3) * opts.scale,
};
const colsPerPage = Math.ceil(worldMm.width / printableMm.width);
const rowsPerPage = Math.ceil(worldMm.height / printableMm.height);
// ...build a Page[] with overlap and crop marks
}
The user sees a print preview with red lines showing where the cuts and overlaps fall, and can adjust scale / paper size until satisfied.
3. Vector output
For maximum print quality, every element is vector:
- Tile SVG → vector path.
- Object SVG → vector path.
- Text → embedded font glyphs (subset).
- Grid lines → stroked vector lines.
- Borders → vector rectangles.
The PDF contains no rasters unless the user enables "Include rasters" (e.g. for a photo-realistic theme pack that ships PNG textures).
3.1 Embedded fonts
Theme packs ship font files; the print engine embeds the relevant subset. No font substitution at print time.
3.2 Crop marks
┌─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─┐
Page content
└─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─┘
Crop marks are 5 mm long, 0.25 pt wide, offset 3 mm from the page edge. They sit on the bleed layer and never appear in print.
3.3 Bleed
A 3 mm bleed extends artwork past the trim line. The print engine knows the bleed for each theme pack and renders correctly.
4. The five modes in detail
4.1 GM mode
- Full grid visible (subtle).
- All labels visible.
- All NPC names + roles shown.
- Hidden regions: a "GM only" overlay with notes pinned by the GM.
- A small "GM secret" stamp at the corner.
4.2 Player mode
- Grid: hidden by default (toggleable).
- Labels: visible if marked public.
- Hidden regions: never rendered.
- A "Players' version" stamp at the corner.
4.3 Battle mode
- Grid: bold, high contrast.
- Labels: hidden.
- Scale ruler at the corner.
- "Battle map" stamp.
- A faint hex / square coordinate grid in the margin.
4.4 Poster mode
- Grid: hidden.
- Labels: hidden.
- Decorative border from the theme pack.
- "Poster" stamp hidden (this is wall art).
4.5 Atlas mode
- Multiple pages.
- Index page with hyperlinked page numbers.
- Legend page.
- Per-page header with region name.
- Coordinates on every page.
- Scale ruler on the cover.
5. PNG / image export
In addition to PDF, the engine supports PNG export:
- Single PNG: the full world at a chosen DPI (default 300).
- Tiled PNGs: like the PDF atlas but as separate PNG files for digital use.
- WebP: smaller, supports transparency.
The PNG export runs the same layout engine; each page renders at the chosen DPI.
6. Print-shop marketplace
The killer feature: find a print shop, upload, pick up.
6.1 User flow
- User clicks Print this map in the editor.
- A modal asks:
- Paper size (A3 default).
- Mode (Atlas default).
- Quantity (1 default).
- Optional: matte / gloss / lamination.
- The engine:
- Renders the PDF in-browser.
- Calls a print-shop API to find nearby shops.
- Lists shops with price + ETA + rating.
- User selects a shop.
- Engine uploads the PDF (signed URL).
- Shop confirms the order.
- User gets:
- Order ID.
- QR code (PNG) for pickup.
- Email receipt.
6.2 Print-shop API
A simple REST API:
GET /v1/shops?lat=..&lon=..&radius=..&paper=A3
→ [{ id, name, address, distanceKm, priceCents, etaHours, rating }]
POST /v1/orders
{ shopId, pdfUrl, options }
→ { orderId, qrUrl, statusUrl }
The integration is best-effort: if no API exists, the user gets an email template with the PDF attached for manual sending.
6.3 Caching
Shop listings are cached locally per geohash for 24 hours.
7. Print preview
A live preview lets the user see exactly what will print:
- Page-by-page navigation.
- Toggle grid, labels, legend, crop marks.
- Zoom + pan.
- "What you see is what prints" — no hidden surprises.
The preview is rendered in the browser using the same vector pipeline; no server round-trip needed.
8. Print profiles
Save a print profile for reuse:
interface PrintProfile {
id: string;
name: string; // "GM A3 Atlas"
options: PrintLayoutOptions;
}
Profiles are GAS assets (gas://print-profile/...) and can be shared.
9. Multi-map atlases
A user can bundle multiple worlds into one PDF atlas (e.g. "Northern Kingdom + Southern Wastes + Underdark"). Each map gets its own section with cover, index, legend, and atlas pages.
10. Performance
- PDF generation runs in a Web Worker (the browser stays responsive).
- For very large worlds (> 1000 pages), the engine streams pages to the user.
- Tile vectorisation is cached per pack.
11. Determinism
Same World.json + same PrintLayoutOptions → byte-identical PDF. Required for
marketplace previews and reproducible tests.