fix(core): smooth tunnel entrance floor; verifier keeps baselines out of /tmp (L47); research R14
The demo tunnel's hard floor cap used a vertical Z - floorZ on a sloped Bezier floor, which is not a signed distance, so marching cubes produced a staircase through the first 7 m (40-51 mm ripple, 82 mm steps). The cap now uses the grade-normalised signed distance with a floor-adjacent bore re-application fading over the entrance; mesh tile Z bounds include cavern bounds. New three-rail entrance audit: L46 fails, L47 passes. verify: the cost baseline exports only build inputs into Saved/Linux/<out>/baseline-<commit> and removes it; /tmp unchanged after a full tier. Demo off: owner hashes identical to L46. fast/medium/full PASS. Known: sawtooth at the tunnel wall foot (next round). Research R14: Windows/MSVC readiness review. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
This commit is contained in:
1 parent
e016f6a20f
commit
c091e4434f
13 files changed
+839
-27
No files matched your search
@@ -0,0 +1,401 @@
|
||||
#!/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]
|
||||
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(ROOT / "Saved/Linux/l46/landmark-l46.vfmesh"),
|
||||
"--layout", str(ROOT / "Saved/Linux/l46/layouts/landmark-reachability-clang.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)
|
||||
Reference in new issue
Block a user