docs(humans): ship the MD explainer as a PDF with the images baked in
The HTML references screenshots relatively, so it only renders inside a checkout -- mailing the file on its own gives broken images. The PDF is one self-contained file, which is what actually gets sent to Ozlem. - tools/humans_to_pdf.py: renders .humans/*.html via headless Chrome (or Edge), so the output is what the browser shows. HTML stays the source. - @media print block on the page: the wide tables and the pipeline diagram scroll on screen but would be cut off at the paper edge; this reflows them and keeps figures, callouts and table rows off page breaks. Verified at A4 content width -- 6 pages, both screenshots embedded. - *.pdf -> LFS. Each regeneration is a whole new ~2 MB blob, and unlike png/jpg there are no PDFs tracked raw yet, so there is no history to rewrite by adding it now. Also corrects working-files.md: it claimed .gitattributes does not LFS-track .blend, which was true when written and is no longer -- .blend/.zprj/.obj/ .npy/.npz have since been added. The note now covers what is actually still excluded (png/jpg, deliberately) and why that makes QA renders scratch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
+19
-5
@@ -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.
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
Before
After
|
Binary file not shown.
@@ -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())
|
||||
Reference in New Issue
Block a user