#!/usr/bin/env python3 """Capture L47 cavern entrance views and audit the exported floor profile.""" from __future__ import annotations import argparse import importlib.util import json import math import os from pathlib import Path import subprocess import sys import time ROOT = Path(__file__).resolve().parents[4] BASELINES = ROOT / "Standalone/Tools/verify/baselines" VERIFY = ROOT / "Standalone/Tools/verify" LINUX37 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux37" sys.path.insert(0, str(VERIFY)) from runner import bounded_run, current_stamp, first_stamp def load_module(name: str, path: Path): spec = importlib.util.spec_from_file_location(name, path) if spec is None or spec.loader is None: raise RuntimeError(f"cannot import {path}") module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module PROFILE = load_module("l45_ring_profile", ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py") SHAPE = load_module("l45_shape_audit", LINUX37 / "AuditMouthShapeL37.py") PARSER = argparse.ArgumentParser(description=__doc__) PARSER.add_argument("--layout", type=Path, required=True) PARSER.add_argument("--artifact-dir", type=Path, required=True) PARSER.add_argument("--capture-dir", type=Path, required=True) PARSER.add_argument("--data", type=Path, default=ROOT / "Standalone/data/owner_world.json") OPTIONS = PARSER.parse_args() ARTIFACT_DIR = OPTIONS.artifact_dir.resolve() CAPTURE_DIR = OPTIONS.capture_dir.resolve() LAYOUT_PATH = OPTIONS.layout.resolve() ARTIFACT_DIR.mkdir(parents=True, exist_ok=True) CAPTURE_DIR.mkdir(parents=True, exist_ok=True) PNG_DIR = ARTIFACT_DIR / "capture-png" PNG_DIR.mkdir(parents=True, exist_ok=True) HEADLESS = Path(os.environ["VOXELFORGE_L37_HEADLESS"]) RENDERER = Path(os.environ["VOXELFORGE_L37_RENDERER"]) EXPECTED_STAMP = current_stamp() def run_logged(name: str, command: list[str], *, capture: bool = False) -> str: log = ARTIFACT_DIR / f"{name}.log" started = time.monotonic() with log.open("w", encoding="utf-8") as stream: result = bounded_run(command, timeout=1800, stdout=stream) elapsed = time.monotonic() - started output = log.read_text(encoding="utf-8", errors="replace") if result is None: raise RuntimeError(f"{name} timed out; see {log}") print(f"[L47CaptureRun] step={name} elapsed={elapsed:.1f}s " f"exit={result.returncode} log={log}", flush=True) if result.returncode != 0: raise RuntimeError(f"{name} failed; see {log}") if capture and (first_stamp(output) != EXPECTED_STAMP or first_stamp(bounded_run( [str(command[0]), "--print-source-stamp"], timeout=15).stdout or "") != EXPECTED_STAMP): raise RuntimeError(f"{name} executable source stamp mismatch") return output def stamp_by_id(layout: dict, stable_id: str) -> dict: for stamp in layout.get("stamps", []): if stamp.get("stable_id") == stable_id: return stamp raise RuntimeError(f"missing stamp {stable_id}") def metres(stamp: dict, key: str = "position_um") -> tuple[float, float, float]: return tuple(value / 1_000_000.0 for value in stamp[key]) def camera_angles(camera, target): dx, dy, dz = (target[index] - camera[index] for index in range(3)) return (math.degrees(math.atan2(dx, dy)), math.degrees(math.atan2(dz, math.hypot(dx, dy)))) def fmt(values) -> str: return ",".join(f"{value:.6f}" for value in values) def camera_spec(camera, target): yaw, pitch = camera_angles(camera, target) return (*camera, yaw, pitch) def edge_point(edge: dict, t: float) -> tuple[float, float, float]: points = edge["control_points_um"] one = 1.0 - t weights = (one ** 3, 3 * one ** 2 * t, 3 * one * t ** 2, t ** 3) return tuple(sum(weights[index] * points[index][axis] for index in range(4)) / 1_000_000.0 for axis in range(3)) def cavern_features(layout: dict): if (layout.get("cavern_graph_vocabulary_version") != 1 or len(layout.get("cavern_rooms", [])) != 1 or len(layout.get("cavern_edges", [])) != 1): raise RuntimeError("L47 capture needs one resolved cavern room and edge") room = layout["cavern_rooms"][0] edge = layout["cavern_edges"][0] return room, edge layout = json.loads(LAYOUT_PATH.read_text(encoding="utf-8")) room, edge = cavern_features(layout) portal_stamp = stamp_by_id(layout, "landmark/sw-arrival-chamber-mouth-approach-v1") floor_stamp = stamp_by_id(layout, "landmark/sw-arrival-chamber-v1-floor") dais_stamp = stamp_by_id(layout, "landmark/sw-arrival-chamber-v1-dais") entrance = edge_point(edge, 0.0) next_ramp = edge_point(edge, 0.08) portal = metres(portal_stamp) chamber = metres(room, "center_um") floor = metres(floor_stamp) floor_top = floor[2] + floor_stamp["half_extents_um"][2] / 1_000_000.0 dais = metres(dais_stamp) dais_top = dais[2] + dais_stamp["half_extents_um"][2] / 1_000_000.0 ground_z = entrance[2] direction_length = math.hypot(next_ramp[0] - entrance[0], next_ramp[1] - entrance[1]) direction = ((next_ramp[0] - entrance[0]) / direction_length, (next_ramp[1] - entrance[1]) / direction_length) outward_length = math.hypot(entrance[0] - chamber[0], entrance[1] - chamber[1]) outward = ((entrance[0] - chamber[0]) / outward_length, (entrance[1] - chamber[1]) / outward_length) mouth_normal = (-direction[1], direction[0]) mesh = ARTIFACT_DIR / "landmark-l47.vfmesh" export_anchor = edge_point(edge, 0.25) export_camera = (export_anchor[0], export_anchor[1], export_anchor[2] + 1.4) export_target_floor = edge_point(edge, 0.42) export_target = (export_target_floor[0], export_target_floor[1], export_target_floor[2] + 1.2) run_logged("landmark-export", [str(HEADLESS), str(OPTIONS.data), "--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=surface-world", "--landmark-chamber-demo", f"--camera={fmt(camera_spec(export_camera, export_target))}", f"--mesh-export={mesh}", f"--resolved-layout-output={LAYOUT_PATH}"], capture=True) layout = json.loads(LAYOUT_PATH.read_text(encoding="utf-8")) room, edge = cavern_features(layout) portal_stamp = stamp_by_id(layout, "landmark/sw-arrival-chamber-mouth-approach-v1") floor_stamp = stamp_by_id(layout, "landmark/sw-arrival-chamber-v1-floor") dais_stamp = stamp_by_id(layout, "landmark/sw-arrival-chamber-v1-dais") entrance = edge_point(edge, 0.0) next_ramp = edge_point(edge, 0.08) portal = metres(portal_stamp) chamber = metres(room, "center_um") floor = metres(floor_stamp) floor_top = floor[2] + floor_stamp["half_extents_um"][2] / 1_000_000.0 dais = metres(dais_stamp) dais_top = dais[2] + dais_stamp["half_extents_um"][2] / 1_000_000.0 ground_z = entrance[2] direction_length = math.hypot(next_ramp[0] - entrance[0], next_ramp[1] - entrance[1]) direction = ((next_ramp[0] - entrance[0]) / direction_length, (next_ramp[1] - entrance[1]) / direction_length) outward_length = math.hypot(entrance[0] - chamber[0], entrance[1] - chamber[1]) outward = ((entrance[0] - chamber[0]) / outward_length, (entrance[1] - chamber[1]) / outward_length) if not mesh.is_file() or mesh.stat().st_size == 0: raise RuntimeError(f"missing mesh export {mesh}") entrance_audit = ROOT / "Standalone/Research/WorldAssemblerM3/Linux47/MeasureEntranceL47.py" run_logged("entrance-floor-control-l46", [sys.executable, str(entrance_audit), "--mesh", str(BASELINES / "landmarks/l46-entrance-control.vfmesh"), "--layout", str(BASELINES / "landmarks/l46-entrance-control-layout.json"), "--out", str(ARTIFACT_DIR / "entrance-profile-control-l46.json"), "--expect=fail"]) run_logged("entrance-floor-audit", [sys.executable, str(entrance_audit), "--mesh", str(mesh), "--layout", str(LAYOUT_PATH), "--out", str(ARTIFACT_DIR / "entrance-profile-l47.json"), "--expect=pass"]) voxel_um, triangles, tile_count = PROFILE.read_scene(str(mesh), 0, floor_top - 8.0, entrance[2] + 9.0, (min(entrance[0], chamber[0]) - 20.0, max(entrance[0], chamber[0]) + 20.0, min(entrance[1], chamber[1]) - 20.0, max(entrance[1], chamber[1]) + 20.0)) bounds = (min(entrance[0], chamber[0]) - 20.0, max(entrance[0], chamber[0]) + 20.0, min(entrance[1], chamber[1]) - 20.0, max(entrance[1], chamber[1]) + 20.0) buckets = PROFILE.build_buckets(triangles, True, bounds, cell_size=1.0) ground_candidates = [] for ix in range(-20, 21): for iy in range(-20, 21): x, y = portal[0] + ix * 0.5, portal[1] + iy * 0.5 dx, dy = x - portal[0], y - portal[1] distance = math.hypot(dx, dy) if not 6.0 <= distance <= 10.0: continue hit = PROFILE.sample_surface(buckets, x, y, True, 1.0, preferred_height=ground_z) if hit is None or abs(hit[0] - ground_z) > 3.0 or hit[1][2] < 0.85: continue alignment = max(-1.0, min(1.0, (dx * mouth_normal[0] + dy * mouth_normal[1]) / distance)) angle = math.acos(alignment) score = abs(distance - 7.0) + angle * 4.0 ground_candidates.append((score, x, y, hit[0])) if not ground_candidates: raise RuntimeError("no walkable mesh ground lies 6 to 10 m from the cave mouth") _, ground_x, ground_y, outside_ground = min(ground_candidates) inside_floor = PROFILE.sample_surface(buckets, chamber[0], chamber[1], True, 1.0, preferred_height=floor_top) if inside_floor is None or abs(inside_floor[0] - floor_top) > 1.5: raise RuntimeError(f"chamber floor mesh hit {inside_floor} does not match designed floor {floor_top:.3f} m") floor_top = inside_floor[0] ground_camera = (ground_x, ground_y, outside_ground + 1.6) entry_ground = PROFILE.sample_surface(buckets, entrance[0], entrance[1], True, 1.0, preferred_height=ground_z) if entry_ground is None: raise RuntimeError("cave mouth has no walkable ground surface in the exported mesh") entry_camera = (entrance[0] + mouth_normal[0] * 3.8, entrance[1] + mouth_normal[1] * 3.8, ground_z + 1.6) entry_target_floor = edge_point(edge, 0.30) entry_target = (entry_target_floor[0], entry_target_floor[1], entry_target_floor[2] + 1.4) low_camera = (entrance[0] + outward[0] * 1.0, entrance[1] + outward[1] * 1.0, ground_z + 0.65) low_target_floor = edge_point(edge, 0.24) low_target = (low_target_floor[0], low_target_floor[1], low_target_floor[2] + 0.15) to_dais = (dais[0] - chamber[0], dais[1] - chamber[1]) to_dais_length = math.hypot(*to_dais) opposite_dais = (-to_dais[0] / to_dais_length, -to_dais[1] / to_dais_length) inside_x = chamber[0] inside_y = chamber[1] inside_floor = PROFILE.sample_surface(buckets, inside_x, inside_y, True, 1.0, preferred_height=floor_top) inside_eye_z = inside_floor[0] + 1.6 chamber_camera = (chamber[0] + opposite_dais[0] * 5.5, chamber[1] + opposite_dais[1] * 5.5, inside_eye_z) chamber_target = (dais[0], dais[1], dais_top - 0.25) overview_camera = (entrance[0] + outward[0] * 18.0, entrance[1] + outward[1] * 18.0, outside_ground + 15.0) overview_target = ((entrance[0] + chamber[0]) * 0.5, (entrance[1] + chamber[1]) * 0.5, (ground_z + floor_top) * 0.5) inward = (-outward[0], -outward[1]) clip_point = (entrance[0] + outward[0] * 0.5, entrance[1] + outward[1] * 0.5) overview_clip = (inward[0], inward[1], 0.0, -(inward[0] * clip_point[0] + inward[1] * clip_point[1])) views = ( ("ground-entrance", ground_camera, (portal[0], portal[1], portal[2]), (portal[0], portal[1], ground_z, 20.0), -509.0), ("entrance-looking-in", entry_camera, entry_target, (entrance[0], entrance[1], entrance[2], 16.0), -507.0), ("entrance-floor-low", low_camera, low_target, (entrance[0], entrance[1], entrance[2], 12.0), ground_z), ("inside-to-dais", chamber_camera, chamber_target, (chamber[0], chamber[1], chamber[2], 15.0), chamber[2]), ("overview-entrance-and-chamber", overview_camera, overview_target, ((entrance[0] + chamber[0]) * 0.5, (entrance[1] + chamber[1]) * 0.5, (ground_z + floor_top) * 0.5, 34.0), -509.0), ) for name, camera, target, focus, height_center in views: png = PNG_DIR / f"{name}.png" jpg = CAPTURE_DIR / f"{name}.jpg" yaw, pitch = camera_angles(camera, target) fov = (80.0 if name == "overview-entrance-and-chamber" else 70.0 if name in ("inside-to-dais", "entrance-looking-in") else 58.0 if name == "entrance-floor-low" else 65.0 if name == "ground-entrance" else 60.0) if name == "ground-entrance": fov = 55.0 print(f"[L47CaptureCamera] view={name} camera_m=({fmt(camera)}) " f"target_m=({fmt(target)}) yaw_deg={yaw:.3f} pitch_deg={pitch:.3f} " f"fov_deg={fov:.1f} entrance_distance_m={math.dist(camera, portal):.3f}", flush=True) render = [str(RENDERER), str(mesh), str(png), "--size=1600x900", f"--fov-deg={fov:.1f}", f"--camera-m={fmt(camera)}", f"--yaw-deg={yaw:.6f}", f"--pitch-deg={pitch:.6f}", "--shade=readable", "--headlamp=on", "--height-tint=on", "--light-direction=0.3,-0.4,0.85", f"--height-center-m={(floor_top if name == 'inside-to-dais' else height_center):.6f}", f"--height-range-m={4 if name == 'inside-to-dais' else 6 if name == 'entrance-floor-low' else 16}", "--tile-skirts=off", "--tile-edges=off", f"--focus-region={fmt(focus)}"] if name == "overview-entrance-and-chamber": render.append(f"--clip-plane={fmt(overview_clip)}") run_logged(f"{name}-render", render, capture=True) for quality in (4, 6, 8): run_logged(f"{name}-jpeg-q{quality}", ["ffmpeg", "-nostdin", "-hide_banner", "-loglevel", "error", "-y", "-i", str(png), "-frames:v", "1", "-q:v", str(quality), str(jpg)]) if jpg.is_file() and 0 < jpg.stat().st_size <= 300_000: break if not jpg.is_file() or not 0 < jpg.stat().st_size <= 300_000: raise RuntimeError(f"{jpg.name} exceeds the 300 KB capture limit") quality_log = run_logged(f"{name}-quality", [sys.executable, str(VERIFY / "capture_quality.py"), str(jpg)]) print(f"[L47CaptureJPEG] file={jpg} bytes={jpg.stat().st_size} " f"quality_log={quality_log.strip().replace(chr(10), ' ')} status=PASS", flush=True) def region_triangles(center, radius_xy, z_min, z_max): selected = [] for triangle in triangles: points = tuple(vertex[0] for vertex in triangle[2]) centroid = tuple(sum(point[axis] for point in points) / 3.0 for axis in range(3)) if (math.hypot(centroid[0] - center[0], centroid[1] - center[1]) <= radius_xy and z_min <= centroid[2] <= z_max): selected.append(triangle) return selected def tunnel_wall_triangles(edge, t, height_above_floor): center = edge_point(edge, t) before = edge_point(edge, max(0.0, t - 0.01)) after = edge_point(edge, min(1.0, t + 0.01)) tx, ty = after[0] - before[0], after[1] - before[1] tangent_length = math.hypot(tx, ty) tx, ty = tx / tangent_length, ty / tangent_length expected_radius = edge["bore_radius_um"] / 1_000_000.0 * math.sqrt( 1.0 - (height_above_floor / (edge["bore_height_um"] / 1_000_000.0)) ** 2) selected = [] for triangle in triangles: points = tuple(vertex[0] for vertex in triangle[2]) centroid = tuple(sum(point[axis] for point in points) / 3.0 for axis in range(3)) dx, dy = centroid[0] - center[0], centroid[1] - center[1] longitudinal = dx * tx + dy * ty lateral = abs(-dy * tx + dx * ty) vertical = centroid[2] - center[2] if (abs(longitudinal) <= 1.5 and abs(lateral - expected_radius) <= 0.7 and abs(vertical - height_above_floor) <= 0.75): selected.append(triangle) print(f"[L47TunnelWallWindow] t={t:.3f} center_m=({center[0]:.3f}," f"{center[1]:.3f},{center[2]:.3f}) expected_radius_m={expected_radius:.3f} " f"longitudinal_half_width_m=1.500 vertical_half_width_m=0.750 " f"triangles={len(selected)}", flush=True) return selected def audit_shape(name, region, center): metrics = SHAPE.analyze_triangles(region, center[:2], center[2], crop_to_center=False, include_sides=True) passed = (metrics["triangles"] > 0 and metrics["planar_area"] < SHAPE.PLANE_LIMIT_M2 and metrics["straight"] <= SHAPE.STRAIGHT_LIMIT_M and metrics["corners"] == 0) print(f"[LandmarkBuiltShape] region={name} triangles={metrics['triangles']} " f"planar_patch_m2={metrics['planar_area']:.4f} " f"straight_run_m={metrics['straight']:.4f} sharp_corners={metrics['corners']} " f"corner_turn_max_deg={metrics['corner_turn_max']:.2f} " f"thresholds=({SHAPE.PLANE_LIMIT_M2:.2f}m2,{SHAPE.STRAIGHT_LIMIT_M:.2f}m," f"{SHAPE.CORNER_TURN_LIMIT_DEG:.0f}deg) status={'PASS' if passed else 'FAIL'}", flush=True) if not passed: raise RuntimeError(f"L37 built-shape thresholds failed in {name}") return metrics entrance_metrics = audit_shape("cave-mouth", region_triangles( portal, 5.0, ground_z + 0.8, ground_z + 4.5), (portal[0], portal[1], ground_z)) tunnel_mid = edge_point(edge, 0.64) tunnel_metrics = audit_shape("tunnel-wall-edge", tunnel_wall_triangles( edge, 0.64, 1.2), (tunnel_mid[0], tunnel_mid[1], tunnel_mid[2] + 1.2)) dais_surface = PROFILE.sample_surface(buckets, dais[0], dais[1], True, 1.0, preferred_height=dais_top - 0.4) if dais_surface is None: raise RuntimeError("dais stamp has no upward-facing surface in the exported mesh") dais_rise = dais_surface[0] - floor_top print(f"[LandmarkDaisSurface] center_m=({dais[0]:.3f},{dais[1]:.3f}) " f"floor_z_m={floor_top:.3f} top_z_m={dais_surface[0]:.3f} " f"rise_m={dais_rise:.3f} minimum_m=1.000 " f"status={'PASS' if dais_rise >= 1.0 else 'FAIL'}", flush=True) if dais_rise < 1.0: raise RuntimeError(f"dais rises only {dais_rise:.3f} m above the chamber floor") dais_metrics = audit_shape("dais-platform", region_triangles( dais, 1.7, floor_top + 0.6, dais_top + 0.4), (dais[0], dais[1], dais_surface[0])) metrics_path = ARTIFACT_DIR / "landmark-shape-metrics.json" metrics_path.write_text(json.dumps({ "graph_vocabulary_version": layout["cavern_graph_vocabulary_version"], "entry_m": entrance, "portal_m": portal, "entry_ground_m": ground_z, "chamber_center_m": chamber, "chamber_floor_m": floor_top, "tunnel_midpoint_m": tunnel_mid, "dais_center_m": dais, "dais_top_m": dais_top, "dais_surface_m": dais_surface[0], "dais_rise_m": dais_rise, "l37_thresholds": {"planar_patch_m2_lt": SHAPE.PLANE_LIMIT_M2, "straight_run_m_lte": SHAPE.STRAIGHT_LIMIT_M, "corner_turn_deg_lt": SHAPE.CORNER_TURN_LIMIT_DEG}, "entrance_arch": entrance_metrics, "tunnel_wall": tunnel_metrics, "dais_platform": dais_metrics, "mesh_tiles": tile_count, "mesh_voxel_um": voxel_um, }, indent=2) + "\n", encoding="utf-8") print(f"[L47CaptureShapeMetrics] file={metrics_path} status=PASS", flush=True)