diff --git a/.agents/rules/working-files.md b/.agents/rules/working-files.md index 5b71c41..6ca2993 100644 --- a/.agents/rules/working-files.md +++ b/.agents/rules/working-files.md @@ -66,7 +66,9 @@ pinned. ## Committing -Scratch patterns are in `.gitignore` and must never be committed. Note that -`.gitattributes` LFS-tracks `.glb/.fbx/.mp4/.task` but **not `.blend`** — an -80 MB master committed today goes into git proper, not LFS. Add `.blend` to -`.gitattributes` first if masters are ever to be tracked. +Scratch patterns are in `.gitignore` and must never be committed. `.blend`, +`.zprj`, `.obj`, `.npy`/`.npz` and the rest of the heavy formats are LFS-tracked +in `.gitattributes`, so a pinned master can be committed safely — but note that +`.png`/`.jpg` deliberately are **not** (≈250 are already tracked raw; adding them +would rewrite every one without shrinking history). A lane's QA renders are +scratch for that reason too, not just a disk one. diff --git a/.gitattributes b/.gitattributes index 501fb41..3f5b08a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -18,6 +18,11 @@ *.npy filter=lfs diff=lfs merge=lfs -text *.npz filter=lfs diff=lfs merge=lfs -text +# Generated .humans/ PDFs (tools/humans_to_pdf.py). Each regeneration is a whole +# new ~2 MB blob, so LFS from the start — unlike png/jpg above, none are tracked +# raw yet, so there is no history to rewrite. +*.pdf filter=lfs diff=lfs merge=lfs -text + # Reallusion CC / iClone asset containers *.iAvatar filter=lfs diff=lfs merge=lfs -text *.iavatar filter=lfs diff=lfs merge=lfs -text diff --git a/.humans/README.md b/.humans/README.md index af7c35e..61302a7 100644 --- a/.humans/README.md +++ b/.humans/README.md @@ -1,7 +1,7 @@ # .humans/ — pages written for people, not agents -Self-contained HTML pages explaining how parts of this repo work, for reading in -a browser. Same convention as `ariki-game/.humans/`. +Explainers about how parts of this repo work. Same convention as +`ariki-game/.humans/`: an HTML page per topic, read in a browser. The operating detail lives in `.claude/skills/` and `.agents/` and is written for whoever is driving the tools. These pages are the **explanation** — what we built, @@ -10,7 +10,21 @@ skill/wiki is the source of truth. | page | about | |---|---| -| `marvelous-designer.html` | How we author Ariki's garments as real sewn cloth in Marvelous Designer — the process, what's been made, the traps, what's still unsolved. Written for Özlem and Jeremy. | +| `marvelous-designer` | How we author Ariki's garments as real sewn cloth in Marvelous Designer — the process, what's been made, the traps, what's still unsolved. Written for Özlem and Jeremy. | -Images are referenced relatively (`../tools/tailor/screenshots/…`), so open these -from inside the repo rather than copying the file out on its own. +## HTML is the source; the PDF is what you send + +The HTML references screenshots relatively (`../tools/tailor/screenshots/…`), so it +only renders from inside a checkout — mail someone the `.html` on its own and they +get broken images. The **PDF has the images baked in** and travels as one file. + +Edit the HTML, then regenerate: + +``` +python tools/humans_to_pdf.py --all # or a single page +``` + +That drives headless Chrome, so what you get is exactly what the browser shows. +Each page carries an `@media print` block controlling the paper layout — without it +the wide tables and the pipeline diagram, which merely scroll on screen, get cut +off at the page edge. Commit the HTML and the PDF together so they don't drift. diff --git a/.humans/marvelous-designer.html b/.humans/marvelous-designer.html index 82bf690..ed18754 100644 --- a/.humans/marvelous-designer.html +++ b/.humans/marvelous-designer.html @@ -114,6 +114,36 @@ .callout p:last-child { margin-bottom: 0; } .meta { color: var(--muted); font-size: .85rem; border-top: 1px solid var(--line); margin-top: 3.5rem; padding-top: 1.2rem; } hr { border: 0; border-top: 1px solid var(--line); margin: 3rem 0 0; } + + /* PDF export — `python tools/humans_to_pdf.py` drives headless Chrome. + Screen rules scroll wide content; on paper it would simply be cut off. */ + @media print { + @page { size: A4; margin: 12mm; } + html, body { + background: #0b1113 !important; + -webkit-print-color-adjust: exact; + print-color-adjust: exact; + } + body { padding: 0; font-size: 10pt; line-height: 1.5; } + h1 { font-size: 26pt; margin-bottom: .6rem; } + h2 { font-size: 13pt; margin: 20pt 0 6pt; } + h3 { font-size: 10.5pt; margin: 12pt 0 4pt; } + p, li { max-width: none; } + a { text-decoration: none; } + /* Wide content is scrollable on screen; on paper it must reflow. */ + .wrap { overflow: visible; } + table { min-width: 0; font-size: 8.5pt; } + th, td { padding: .5rem .7rem; } + .flow { padding: .8rem 1rem; overflow: visible; } + .flow pre { font-size: 7pt; line-height: 1.5; } + /* Never split a figure, table row, callout or diagram across a page. */ + figure, .callout, .flow, .wrap, tr { break-inside: avoid; page-break-inside: avoid; } + h1, h2, h3 { break-after: avoid; page-break-after: avoid; } + figure img { max-height: 16cm; width: auto; margin: 0 auto; } + figcaption { font-size: 8.5pt; } + .shots { display: block; } + .meta { margin-top: 20pt; font-size: 8pt; } + } diff --git a/.humans/marvelous-designer.pdf b/.humans/marvelous-designer.pdf new file mode 100644 index 0000000..bb7b8ff --- /dev/null +++ b/.humans/marvelous-designer.pdf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0a25790e9203bef3e27fdbc2bd9d1b32a42b61a262b1ad8fa4077b382bba5a68 +size 1848450 diff --git a/tools/humans_to_pdf.py b/tools/humans_to_pdf.py new file mode 100644 index 0000000..7bc5aa1 --- /dev/null +++ b/tools/humans_to_pdf.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +"""Render a .humans/ HTML page to a self-contained PDF via headless Chrome. + +The pages in `.humans/` reference screenshots relatively +(`../tools/tailor/screenshots/...`), so the HTML only works from inside a repo +checkout. The PDF embeds those images, which makes it the thing you actually +send to someone. + +The HTML stays the source of truth: edit the page, re-run this, commit both. + + python tools/humans_to_pdf.py .humans/marvelous-designer.html + python tools/humans_to_pdf.py --all +""" +from __future__ import annotations + +import argparse +import subprocess +import sys +import tempfile +from pathlib import Path + +CHROME_CANDIDATES = [ + r"C:\Program Files\Google\Chrome\Application\chrome.exe", + r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe", + r"C:\Program Files\Microsoft\Edge\Application\msedge.exe", + r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe", + "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", + "/usr/bin/google-chrome", + "/usr/bin/chromium", +] + + +def find_chrome() -> Path: + for c in CHROME_CANDIDATES: + p = Path(c) + if p.exists(): + return p + raise SystemExit("error: no Chrome or Edge found; tried:\n " + + "\n ".join(CHROME_CANDIDATES)) + + +def to_pdf(html: Path, chrome: Path) -> Path: + out = html.with_suffix(".pdf") + # Chrome refuses to reuse a running profile, so give it a throwaway one. + with tempfile.TemporaryDirectory() as profile: + cmd = [ + str(chrome), + "--headless=new", + "--disable-gpu", + f"--user-data-dir={profile}", + "--no-pdf-header-footer", # drop the URL/date furniture + "--virtual-time-budget=15000", # let local images decode first + f"--print-to-pdf={out}", + html.resolve().as_uri(), + ] + r = subprocess.run(cmd, capture_output=True, text=True, timeout=180) + if not out.exists(): + sys.stderr.write(r.stderr or "") + raise SystemExit(f"error: Chrome produced no PDF for {html}") + return out + + +def main() -> int: + ap = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("page", type=Path, nargs="?", help="HTML page under .humans/") + ap.add_argument("--all", action="store_true", help="render every .humans/*.html") + args = ap.parse_args() + + repo = Path(__file__).resolve().parent.parent + if args.all: + pages = sorted((repo / ".humans").glob("*.html")) + elif args.page: + pages = [args.page] + else: + ap.error("give a page, or --all") + + if not pages: + print("nothing to render") + return 0 + + chrome = find_chrome() + print(f"using {chrome.name}") + for html in pages: + if not html.is_file(): + print(f" skip (missing): {html}") + continue + out = to_pdf(html, chrome) + print(f" {out.stat().st_size / 1048576:.2f} MB {out}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())