#!/usr/bin/env python3 """Stream selected-LOD tiles and find face windows that pass the ring audit.""" import argparse import array import importlib.util from pathlib import Path import resource import struct import sys def main(): parser = argparse.ArgumentParser() parser.add_argument("scene") parser.add_argument("--lod", type=int, required=True) parser.add_argument("--surface", choices=("floor", "ceiling"), required=True) parser.add_argument("--height-min-m", type=float, required=True) parser.add_argument("--height-max-m", type=float, required=True) parser.add_argument("--preferred-height-m", type=float, help="lock overlapping-face selection near this expected surface height") parser.add_argument("--step-m", type=float, default=0.5) parser.add_argument("--tile-key", nargs=3, type=int, help="audit only this tile's x/y/z key (LOD is supplied separately)") args = parser.parse_args() audit_path = Path(__file__).with_name("AuditRingTerraceL32.py") spec = importlib.util.spec_from_file_location("ring_audit_l32", audit_path) audit = importlib.util.module_from_spec(spec) spec.loader.exec_module(audit) want_up = args.surface == "floor" candidates = [] with open(args.scene, "rb") as stream: if stream.read(8) != b"VFMSH001": raise SystemExit("invalid VFMSH001 scene") version, voxel_um, tile_count = struct.unpack("<3I", stream.read(12)) stream.seek(40, 1) if version != 1: raise SystemExit("unsupported mesh scene") for _ in range(tile_count): header = stream.read(36) key = struct.unpack("<4i", header[:16]) surface_count, _, vertex_count, index_count, mask_count = struct.unpack( "<5I", header[16:]) payload = vertex_count * 24 + index_count * 4 + mask_count if key[3] != args.lod or (args.tile_key and tuple(key[:3]) != tuple(args.tile_key)): stream.seek(payload, 1) continue positions = array.array("f") positions.frombytes(stream.read(vertex_count * 12)) normals = array.array("f") normals.frombytes(stream.read(vertex_count * 12)) indices = array.array("I") indices.frombytes(stream.read(index_count * 4)) masks = stream.read(mask_count) if sys.byteorder != "little": positions.byteswap() normals.byteswap() indices.byteswap() triangles = [] vertex_margin = voxel_um / 1_000_000.0 * 0.5 for offset in range(0, index_count, 3): if masks and masks[offset // 3]: continue ids = indices[offset:offset + 3] if max(ids) >= surface_count: continue verts = [] for index in ids: p = index * 3 verts.append(((positions[p], positions[p + 1], positions[p + 2]), (normals[p], normals[p + 1], normals[p + 2]))) average_nz = sum(vertex[1][2] for vertex in verts) / 3.0 if (want_up and average_nz < 0.35) or (not want_up and average_nz > -0.35): continue z_values = [vertex[0][2] for vertex in verts] mean_z = sum(z_values) / 3.0 if (mean_z < args.height_min_m or mean_z > args.height_max_m or min(z_values) < args.height_min_m - vertex_margin or max(z_values) > args.height_max_m + vertex_margin): continue triangles.append((key[3], average_nz, tuple(verts))) if not triangles: continue extent = 32.0 * (1 << key[3]) * voxel_um / 1_000_000.0 cx, cy = (key[0] + 0.5) * extent, (key[1] + 0.5) * extent radius = extent * 0.5 bounds = (cx - radius, cx + radius, cy - radius, cy + radius) cell_size = max(2.0, 16.0 * voxel_um / 1_000_000.0 * (1 << key[3])) buckets = audit.build_buckets(triangles, want_up, bounds, cell_size) profiles = [] preferred_height = args.preferred_height_m if preferred_height is None: preferred_height = args.height_max_m if want_up else args.height_min_m for _, points in audit.make_lines(cx, cy, radius, args.step_m): profiles.append(audit.audit_profile(points, buckets, want_up, 0.5, 0.0005, 0.1, cell_size, preferred_height)) valid = [profile for profile in profiles if profile["valid"]] coverage = min(profile["coverage"] for profile in profiles) amplitude = max((profile["height_max"] for profile in valid), default=0.0) - \ min((profile["height_min"] for profile in valid), default=0.0) longest = max((run[0] for profile in valid for run in profile["plateaus"]), default=0.0) max_normal = max((profile["max_angle"] for profile in valid), default=float("inf")) passed = (len(valid) == len(profiles) and coverage >= 0.75 and amplitude >= 3.0 and longest < 0.5 and max_normal <= 20.0) candidates.append((passed, amplitude, -max_normal, key, cx, cy, len(triangles), coverage, longest, max_normal)) stream.seek(payload - (vertex_count * 24 + index_count * 4 + mask_count), 1) candidates.sort(reverse=True, key=lambda item: (item[0], item[1], item[2])) print(f"[RingWindowSearch] lod={args.lod} surface={args.surface} tiles={len(candidates)} " f"height_window_m={args.height_min_m:.3f}..{args.height_max_m:.3f} " f"step_m={args.step_m:.3f}") for passed, amplitude, neg_normal, key, cx, cy, count, coverage, longest, normal in candidates[:40]: print(f"[RingWindowCandidate] status={'PASS' if passed else 'FAIL'} key={key} " f"center_m=({cx:.3f},{cy:.3f}) triangles={count} coverage={coverage:.3f} " f"amplitude_m={amplitude:.6f} longest_flat_m={longest:.6f} " f"normal_jump_deg={normal:.6f}") print(f"[RingWindowRSS] peak_rss_kib=" f"{resource.getrusage(resource.RUSAGE_SELF).ru_maxrss}") if __name__ == "__main__": main()