#!/usr/bin/env python3 """Export the L59 open-shaft lattice and render four reviewed viewpoints.""" from __future__ import annotations import argparse import hashlib import importlib.util import json import math import os from pathlib import Path import subprocess import sys ROOT = Path(__file__).resolve().parents[4] CAPTURE54 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux54/CaptureNativeCavesL54.py" spec = importlib.util.spec_from_file_location("capture_native_caves_l54", CAPTURE54) capture54 = importlib.util.module_from_spec(spec) assert spec and spec.loader spec.loader.exec_module(capture54) run_step = capture54.run_step camera_angles = capture54.camera_angles filter_mesh_y_leq = capture54.filter_mesh_y_leq fmt = capture54.fmt def sha256(path: Path) -> str: digest = hashlib.sha256() with path.open("rb") as stream: for block in iter(lambda: stream.read(1024 * 1024), b""): digest.update(block) return digest.hexdigest() def midpoint(points: list[list[float]]) -> tuple[float, float, float]: return tuple(sum(point[axis] for point in points) / len(points) for axis in range(3)) def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--out", type=Path, required=True) parser.add_argument("--fixture", type=Path, required=True) parser.add_argument("--layout", type=Path, required=True) parser.add_argument("--headless", type=Path, required=True) parser.add_argument("--renderer", type=Path, required=True) parser.add_argument("--artifact-dir", type=Path, required=True) args = parser.parse_args() out = args.out.resolve() artifact_dir = args.artifact_dir.resolve() capture_dir = out / "lattice-capture-png" mesh_dir = out / "lattice-meshes" for directory in (capture_dir, mesh_dir, artifact_dir): directory.mkdir(parents=True, exist_ok=True) layout = json.loads(args.layout.read_text(encoding="utf-8")) edges = sorted(layout.get("cavern_edges", []), key=lambda row: row["stable_id"]) helix = next((edge for edge in edges if edge["stable_id"] == "lattice/spine/01-helix-ramp-v1"), None) shaft_core = next((room for room in layout.get("cavern_rooms", []) if room["stable_id"] == "lattice/spine/shaft-core-v2"), None) maze_edges = [edge for edge in edges if edge["stable_id"].startswith("lattice/spine/maze-edge-")] if helix is None or shaft_core is None or len(maze_edges) < 4: raise ValueError("layout is missing the resolved open shaft, helix, or maze spine") helix_points = [[value / 1_000_000.0 for value in point] for point in helix["polyline_um"]] core_center = [value / 1_000_000.0 for value in shaft_core["center_um"]] shaft_center = (core_center[0], core_center[1], core_center[2]) top = helix_points[0] bottom = helix_points[-1] whole_mesh = mesh_dir / "lattice-whole-view.vfmesh" export_layout = out / "layouts" / "lattice-capture-layout.json" env = os.environ.copy() env["LP_NUM_THREADS"] = "12" env["XDG_CACHE_HOME"] = str(out / "xdg-cache") export_camera = (top[0], top[1], top[2] + 1.5, 0.0, -8.0) run_step("lattice-whole-view-export", [args.headless, args.fixture, "--workers=12", "--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline", "--force-generate", "--clip-radius=4", "--max-clip-level=0", "--max-level=0", "--render-distance-chunks=0", "--recipe=lattice", "--lattice-strate=1", f"--camera={fmt(export_camera)}", f"--resolved-layout-output={export_layout}", f"--mesh-export={whole_mesh}"], out / "capture-lattice-export.log", env=env, stamped=True) if not whole_mesh.is_file() or whole_mesh.stat().st_size == 0: raise RuntimeError("whole-lattice export produced no mesh") cutaway_mesh = mesh_dir / "lattice-cutaway-y-center.vfmesh" cutaway_triangles = filter_mesh_y_leq(whole_mesh, cutaway_mesh, shaft_center[1]) print(f"[L59LatticeCutaway] plane_y_m={shaft_center[1]:.3f} " f"retained_triangles={cutaway_triangles} mesh={cutaway_mesh}", flush=True) # Start inside the open shaft, offset toward the first tread so the view # follows the well down while keeping the helical ledges in frame. start_dx = top[0] - shaft_center[0] start_dy = top[1] - shaft_center[1] start_radius = max(1.0e-9, math.hypot(start_dx, start_dy)) top_camera = (shaft_center[0] + start_dx / start_radius * 3.5, shaft_center[1] + start_dy / start_radius * 3.5, top[2] + 1.6) top_target = (shaft_center[0], shaft_center[1], bottom[2] + 1.3) eye_index = len(helix_points) // 2 eye_floor = helix_points[eye_index] eye_dx = eye_floor[0] - shaft_center[0] eye_dy = eye_floor[1] - shaft_center[1] eye_radius = max(1.0e-9, math.hypot(eye_dx, eye_dy)) # Put the eye on the walk line, looking across the open core at the far # wall and the ledges above and below. eye_camera = (eye_floor[0], eye_floor[1], eye_floor[2] + 1.35) eye_target = (shaft_center[0], shaft_center[1], eye_camera[2] + 0.35) maze_edge = next(edge for edge in maze_edges if edge["stable_id"] == "lattice/spine/maze-edge-02-v1") maze_points = [[value / 1_000_000.0 for value in point] for point in maze_edge["polyline_um"]] maze_a, maze_b = maze_points[0], maze_points[-1] maze_t = 0.5 maze_floor = tuple(maze_a[axis] + (maze_b[axis] - maze_a[axis]) * maze_t for axis in range(3)) maze_horizontal = max(1.0e-9, math.hypot(maze_b[0] - maze_a[0], maze_b[1] - maze_a[1])) maze_camera = (maze_floor[0] - (maze_b[1] - maze_a[1]) / maze_horizontal * 0.2, maze_floor[1] + (maze_b[0] - maze_a[0]) / maze_horizontal * 0.2, maze_floor[2] + 1.4) maze_target = (maze_camera[0] + (maze_b[0] - maze_a[0]) / maze_horizontal * 8.0, maze_camera[1] + (maze_b[1] - maze_a[1]) / maze_horizontal * 8.0, maze_camera[2] + (maze_b[2] - maze_a[2]) / maze_horizontal * 8.0) overview_camera = (shaft_center[0], shaft_center[1] + 12.0, top[2] + 4.0) overview_target = (shaft_center[0], shaft_center[1], (top[2] + bottom[2]) * 0.5 - 2.0) views = ( ("shaft-top-down", whole_mesh, top_camera, top_target, 22.0, 78.0, None), ("ramp-eye-across", whole_mesh, eye_camera, eye_target, 15.0, 100.0, None), ("maze-corridor", whole_mesh, maze_camera, maze_target, 8.0, 45.0, None), ("shaft-overview", cutaway_mesh, overview_camera, overview_target, 40.0, 80.0, None), ) quality = ROOT / "Standalone/Tools/verify/capture_quality.py" for name, render_mesh, camera, target, focus_radius, fov, clip_plane in views: yaw, pitch = camera_angles(camera, target) if name == "ramp-eye-across": yaw, pitch = camera_angles(camera, target) png = capture_dir / f"lattice-{name}.png" image = artifact_dir / f"lattice-{name}.jpg" height_tint = "off" if name == "maze-corridor" else "on" render_args = [args.renderer, render_mesh, png, "--size=1280x720", f"--fov-deg={fov:.1f}", f"--camera-m={fmt(camera)}", f"--yaw-deg={yaw:.6f}", f"--pitch-deg={pitch:.6f}", "--tile-skirts=off", "--shade=readable", "--headlamp=on", f"--height-tint={height_tint}", f"--height-center-m={(top[2] + bottom[2]) * 0.5:.6f}", "--height-range-m=24", "--tile-edges=off", f"--focus-region={fmt((*target, focus_radius))}"] if clip_plane is not None: render_args.append(f"--clip-plane={fmt(clip_plane)}") run_step(f"{name}-render", render_args, out / f"capture-{name}-render.log", env=env, stamped=True) run_step(f"{name}-jpeg", ["ffmpeg", "-nostdin", "-hide_banner", "-loglevel", "error", "-y", "-i", png, "-frames:v", "1", "-q:v", "5", image], out / f"capture-{name}-jpeg.log", env=env) quality_output = run_step(f"{name}-quality", [sys.executable, quality, image], out / f"capture-{name}-quality.log", env=env) quality_lines = [line for line in quality_output.splitlines() if line.startswith("[CaptureQuality]")] if not quality_lines or not quality_lines[-1].endswith("status=PASS"): raise RuntimeError(f"capture quality gate failed for {name}") size = image.stat().st_size if size > 300_000: raise RuntimeError(f"capture exceeds 300 KB: {image} ({size} bytes)") print(f"[L59LatticeCapture] view={name} file={image} bytes={size} " f"sha256={sha256(image)} status=PASS", flush=True) return 0 if __name__ == "__main__": try: raise SystemExit(main()) except Exception as error: print(f"[L59LatticeCapture] status=FAIL error={error}", file=sys.stderr, flush=True) raise SystemExit(1)