Core.Lattice.PrimitiveField v1, opt-in per strate (legacy Maze/VerticalShaft stay legacy): resolves the real arrival and departure mouths first, plans a mandatory spine, then maze cells and two dead ends (9 rooms, 9 edges, stable IDs). The spine descends 20 m through four helix turns (6 m radius, 3 m clear width, 5 m pitch, 2.6 m clearance; max grade 10.0 deg); each feature is one filleted void (0.57 m measured). Final-field reachability passes at 883 stations; an orphaned shaft control is rejected; exact intervals (0 misses) and a whole-surface audit with 0 flags. Whole view 1.69 s (above the 1 s target). fast/medium/full PASS. Research R27 (self-checked): static stream-to-pool water design. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
165 lines
8.0 KiB
Python
165 lines
8.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Export the L58 native lattice view 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)
|
|
maze_edges = [edge for edge in edges
|
|
if edge["stable_id"].startswith("lattice/spine/maze-edge-")]
|
|
if helix is None or len(maze_edges) < 4:
|
|
raise ValueError("layout is missing the resolved helix or small maze spine")
|
|
helix_points = [[value / 1_000_000.0 for value in point]
|
|
for point in helix["polyline_um"]]
|
|
maze_points = [[value / 1_000_000.0 for value in point]
|
|
for point in maze_edges[0]["polyline_um"]]
|
|
shaft_center = midpoint(helix_points)
|
|
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"[L58LatticeCutaway] plane_y_m={shaft_center[1]:.3f} "
|
|
f"retained_triangles={cutaway_triangles} mesh={cutaway_mesh}", flush=True)
|
|
|
|
# Stand over the clear central opening so the top tread does not occlude
|
|
# the view down the shaft.
|
|
top_camera = (shaft_center[0], shaft_center[1] - 0.8, top[2] + 2.0)
|
|
top_target = (shaft_center[0] + 0.2, shaft_center[1], bottom[2] + 1.3)
|
|
eye_index = len(helix_points) // 2
|
|
eye_floor = helix_points[eye_index]
|
|
eye_tangent = tuple(helix_points[eye_index + 1][axis]
|
|
- helix_points[eye_index - 1][axis] for axis in range(3))
|
|
eye_len = max(1.0e-9, math.hypot(eye_tangent[0], eye_tangent[1]))
|
|
eye_camera = (eye_floor[0], eye_floor[1], eye_floor[2] + 1.45)
|
|
# Look across the shaft center from the ramp so the adjacent higher turn
|
|
# reads in the same frame. The view clips the near half of the shaft wall.
|
|
eye_target = (shaft_center[0], shaft_center[1], eye_camera[2] + 4.2)
|
|
maze_a, maze_b = maze_points[0], maze_points[-1]
|
|
maze_camera = ((maze_a[0] + maze_b[0]) * 0.5,
|
|
(maze_a[1] + maze_b[1]) * 0.5,
|
|
(maze_a[2] + maze_b[2]) * 0.5 + 1.45)
|
|
maze_target = (maze_camera[0] + (maze_b[0] - maze_a[0]) * 0.65,
|
|
maze_camera[1] + (maze_b[1] - maze_a[1]) * 0.65,
|
|
maze_camera[2] + (maze_b[2] - maze_a[2]) * 0.65)
|
|
overview_camera = (shaft_center[0], shaft_center[1] - 28.0,
|
|
(top[2] + bottom[2]) * 0.5)
|
|
overview_target = (shaft_center[0], shaft_center[1], (top[2] + bottom[2]) * 0.5)
|
|
views = (
|
|
("helix-top-down", whole_mesh, top_camera, top_target, 22.0, 70.0, None),
|
|
("ramp-eye-next-turn", whole_mesh, eye_camera, eye_target, 12.0, 76.0,
|
|
(0.0, 1.0, 0.0, -shaft_center[1])),
|
|
("maze-eye-level", whole_mesh, maze_camera, maze_target, 14.0, 82.0, None),
|
|
("cutaway-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-next-turn":
|
|
yaw, pitch = 45.0, 10.0
|
|
png = capture_dir / f"lattice-{name}.png"
|
|
image = artifact_dir / f"lattice-{name}.jpg"
|
|
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", "--height-tint=on",
|
|
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"[L58LatticeCapture] 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"[L58LatticeCapture] status=FAIL error={error}", file=sys.stderr, flush=True)
|
|
raise SystemExit(1)
|