#!/usr/bin/env python3 """Extract the canonical in-game voxel render rig into a standalone JS file. The app's render rig lives in `src/buddy_fusion/voxel_embed.py` as the `VOXEL_SETUP_JS` string (Three.js scene, studio lighting, real shadows, ACES tone mapping, turntable, the `window.voxel*` builders, and `setupVoxel` which boots any `.buddy-voxel-root` from its `.buddyVoxelConfig` JSON). The Tools report pages (`tools/parts-catalog/`, `tools/compendium/`) render live 3D creatures with that exact rig instead of static thumbnails. Rather than copy-paste (and drift), this script imports `voxel_embed`, grabs `VOXEL_SETUP_JS` verbatim, and writes `tools/shared/voxel-rig.js` with a generated-file header plus a standalone boot (observer + initial scan + a cheap retry poll). The app render path in voxel_embed.py is NOT touched. Run: python tools/shared/sync_voxel_rig.py """ from __future__ import annotations import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT / "src")) from buddy_fusion import voxel_embed # noqa: E402 OUT = Path(__file__).resolve().parent / "voxel-rig.js" HEADER = """\ // GENERATED FILE — do not edit by hand. // Source of truth: src/buddy_fusion/voxel_embed.py (VOXEL_SETUP_JS). // Regenerate with: python tools/shared/sync_voxel_rig.py // // This is the production in-game voxel render rig (Three.js scene, studio // lighting, real shape-based shadows, ACES tone mapping, turntable, OrbitControls, // and the window.voxel* builders). Booting it scans the document for // .buddy-voxel-root nodes and renders each from its .buddyVoxelConfig JSON. """ # Standalone boot: the app appends its own observer/poll wired to canvas/tree # helpers; the reports only need the voxel scan. setupVoxel is idempotent and # bails (without marking inited) on a 0x0 stage, so the cheap retry poll picks up # any card whose layout settled a frame late. BOOT = """ // --- standalone boot (reports only; app uses its own VOXEL_JS_ON_LOAD) --- // A page with many cards sets window.__VOXEL_NO_AUTOBOOT before loading this rig // and drives setupVoxel itself through an IntersectionObserver context-cap // manager (one live WebGL context per visible card would blow the browser's // ~16-context limit). Pages with a handful of cards just let it auto-boot. if (!window.__VOXEL_NO_AUTOBOOT) { installVoxelObserver(); scanAndSetupVoxels(); setInterval(scanAndSetupVoxels, 400); } """ def main() -> None: js = voxel_embed.VOXEL_SETUP_JS for name in ("setupVoxel", "voxelsFromBoxes", "voxelBuildMesh", "installVoxelObserver", "scanAndSetupVoxels"): if name not in js: raise SystemExit(f"VOXEL_SETUP_JS is missing expected symbol: {name}") OUT.write_text(HEADER + js + BOOT, encoding="utf-8") print(f"wrote {OUT} ({len(OUT.read_text(encoding='utf-8'))} bytes)") if __name__ == "__main__": main()