--- summary: 'Architecture: five-dimension model, single-source SVG render chain, platform seam, domain-file conventions' read_when: - first time in this repo - adding themes/components/layouts - touching the export pipeline --- # Architecture A PPT deck is, at bottom, five orthogonal concerns: a **content model**, a **2D layout**, a **visual style**, **time-based interaction** (transitions/animation), and a **narrative** that sequences slides. pptfast gives each one exactly one owning layer, so a change in one dimension (say, a new style) never leaks into another (layout code stays style-agnostic). | Dimension | Owning layer | Location | |---|---|---| | Content model | IR (zod schema, semantic components) | `src/ir/` | | 2D layout | layout registry (standard layouts + image takeovers) + components + capacity tables + seeded variety | `src/svg/` | | Visual style | style tokens + theme definitions (curated layout sets + motif + optional per-page-type layout tendencies, 17 built-in themes) | `src/themes/` | | Time-based interaction | `meta.animation` in the IR → slide transition / element entrance patches | `src/pptx/` | | Narrative | narrative axes (strategy × pacing × audience, named presets) resolving editorial discipline, plus a first-class spec artifact (`deck.spec.json` — locked page order/type/heading, strategy-aware hard gates via `spec validate`) that `assembleDeck`/`disassembleDeck` materialize to and from IR, driving a six-phase spec→fill skill methodology for slide sequencing | `src/spec/`, `src/narrative/`, `skills/` | The core insight, carried over from the production system pptfast was extracted from: **visual variety comes from tokens × layout library × seed — not freeform drawing.** Swapping only color tokens (the shadcn-style reskin) still converges on sameness. The layout library is what raises the ceiling — and, as of the theme-structure wave, the theme axis is no longer a constant term in that product: a theme's optional `layoutTendencies` (`docs/concepts.md`'s theme section) softly steers *which* layout the seeded pick favors, per page type, so swapping only the theme (same IR, same seed) can now itself change the realized layout sequence, not just the palette — see `docs/selection-and-seed.md` for the mechanics. ## Render chain Every slide renders through exactly one path, so preview and export can never drift apart: ``` IR (validated) → FullSlideSvg (React → one flat 1280×720 SVG) → svg2pptx (per-element DrawingML ops) → pptxgenjs + JSZip patches (animations, gradients, ea font slots, media dedupe) → .pptx bytes ``` `renderSlideSvg` and `generatePptx` both start from the same `FullSlideSvg` component — the SDK has no second, cheaper rendering path to fall out of sync. ### Fidelity ledger `svg2pptx`'s dispatch (`svg2pptx/dispatch.ts`'s `leafToOp` → `svg2pptx/render.ts`'s `renderOp`) is a closed table — every SVG leaf tag it recognizes lands on exactly one native DrawingML shape, and any tag it doesn't recognize is simply skipped (not drawn), never rasterized as a fallback: | SVG leaf | Op kind | pptxgenjs call | PPTX result | |---|---|---|---| | `` | `shape` | `addShape("rect"\|"roundRect")` | native shape (editable) | | ``/`` | `shape` | `addShape("ellipse")` | native shape (editable) | | `` | `line` | `addShape("line")` | native connector (editable) | | ``/``/`` | `path` | `addShape("custGeom")` | native custom geometry (editable) | | `` | `text` | `addText` | native text box/run (editable) | | `` | `image` | `addImage` | `` — the only picture path | Icons (`src/svg/icons.tsx`'s lucide path/circle/ellipse/rect/line/polyline/polygon primitives) flow through this same table like any other vector markup and land as custGeom or native shapes — never a picture. **Invariant: the only rasterization exit in the export chain is `` backed by a real, resolved asset.** Every ``-emitting call site — the `image`/`image_grid`/`image_compare` components, `Background`'s asset background, `BrandChrome`'s logo, and the 4 `image-*` takeover layouts (`image-pages.tsx`) — resolves a real asset first. When one is missing it degrades to a placeholder (a `` for a content image slot, or the logo simply omitting itself) and never degrades to an ``. Separately, `rasterizeSvg` (next section) — the one function in this codebase that turns SVG into pixels — is reachable only from the optional `--pixels` audit path (`src/svg/audit/pixel-audit.ts`) — `generatePptxBlob`/`svg2pptx` never call it. Rasterization and export are two disjoint subsystems by construction. Regression-guarded by `src/pptx/generate-fidelity-export.test.ts`: a deck covering every registered component type with zero real assets exports with `ppt/media/` empty and zero `` anywhere. Adding one real image asset moves the count by exactly +1, landing on exactly that slide. ## Platform seam `src/index.ts` and everything it imports must stay usable in a browser: no `commander`, no `linkedom`, no `sharp`. Three seams in `src/platform/registry.ts` (`domParser`, `recodeImageToPng`, `rasterizeSvg`) are `undefined` until something calls `installPlatform()`. `src/platform/node.ts` supplies the Node implementation (linkedom for DOM parsing, sharp for image re-encoding and SVG rasterization) via `installNodePlatform()` — the CLI calls it on startup. SDK consumers running in Node must call it themselves before rendering. `rasterizeSvg` (audit-v2 phase B, `docs/contrast-system.md`'s own pixel-layer section) is the one seam with a real browser default too: `src/platform/browser.ts`'s `rasterizeSvgInBrowser` (native `Image` + `OffscreenCanvas`/``) is applied as a plain `?? fallback` at its one call site (`src/svg/audit/pixel-audit.ts`), the same pattern `domParser`'s own `?? globalThis.DOMParser` fallback already uses — not through `installPlatform()`, since nothing calls that automatically in a browser. ### Distribution: the seam being real vs. the package being loadable "`src/index.ts`'s closure is browser-safe" and "the published package loads in a browser" are two different claims — the P2 browser-distribution wave closed the gap between them. The seam above has always been true (verified against a real Chrome tab, not just jsdom), but the default build (`tsup.config.ts`'s `index`/`node`/`cli` entries) externalizes every `dependencies` entry (`react`, `react-dom`, `zod`, `jszip`, `dagre`, `pptxgenjs`) as bare ESM specifiers — correct for a bundler consumer (Vite/webpack resolve them from `node_modules`, deduped against the rest of the app), fatal for a bare `