diff --git a/Standalone/Research/WorldAssemblerM3/Linux48/AuditWholeSurfaceL48.py b/Standalone/Research/WorldAssemblerM3/Linux48/AuditWholeSurfaceL48.py
new file mode 100644
index 0000000..ab15a02
--- /dev/null
+++ b/Standalone/Research/WorldAssemblerM3/Linux48/AuditWholeSurfaceL48.py
@@ -0,0 +1,709 @@
+#!/usr/bin/env python3
+"""Audit every LOD0 mesh triangle in a cavern corridor for clustered defects."""
+from __future__ import annotations
+
+import argparse
+import array
+import json
+import math
+from pathlib import Path
+import struct
+import sys
+
+
+ROOT = Path(__file__).resolve().parents[4]
+CELL_M = 1.0
+CORRIDOR_MARGIN_M = 3.0
+OPEN_GROUND_EXTENSION_M = 5.0
+WELD_M = 0.0001
+
+
+class Triangle:
+ __slots__ = ("verts", "center", "area", "normal", "smooth", "quality",
+ "route_distance", "route_lateral", "route_s", "floor_z",
+ "in_region", "reference")
+
+ def __init__(self, verts, area, normal, smooth, quality):
+ self.verts = verts
+ self.center = tuple(sum(v[i] for v in verts) / 3.0 for i in range(3))
+ self.area = area
+ self.normal = normal
+ self.smooth = smooth
+ self.quality = quality
+ self.route_distance = 0.0
+ self.route_lateral = 0.0
+ self.route_s = 0.0
+ self.floor_z = 0.0
+ self.in_region = False
+ self.reference = False
+
+
+def normalize(value):
+ length = math.sqrt(sum(item * item for item in value))
+ return tuple(item / length for item in value) if length > 1.0e-12 else (0.0, 0.0, 0.0)
+
+
+def dot(a, b):
+ return sum(a[i] * b[i] for i in range(3))
+
+
+def angle_degrees(a, b):
+ return math.degrees(math.acos(max(-1.0, min(1.0, dot(a, b)))))
+
+
+def quantile(values, fraction):
+ if not values:
+ return 0.0
+ ordered = sorted(values)
+ return ordered[min(len(ordered) - 1, max(0, int(math.ceil(fraction * len(ordered))) - 1))]
+
+
+def read_mesh(path: Path):
+ triangles = []
+ selected_tiles = 0
+ lod_counts = {}
+ with path.open("rb") as stream:
+ prefix = stream.read(60)
+ if len(prefix) != 60 or prefix[:8] != b"VFMSH001":
+ raise ValueError(f"{path}: invalid VFMSH001 mesh header")
+ version, voxel_um, tile_count = struct.unpack_from("<3I", prefix, 8)
+ if version != 1 or not voxel_um or tile_count > 1_000_000:
+ raise ValueError(f"{path}: unsupported mesh version or tile count")
+ for _ in range(tile_count):
+ header = stream.read(36)
+ if len(header) != 36:
+ raise ValueError(f"{path}: truncated tile header")
+ key = struct.unpack_from("<4i", header, 0)
+ surface_count, _surface_aux, vertex_count, index_count, mask_count = \
+ struct.unpack_from("<5I", header, 16)
+ lod_counts[key[3]] = lod_counts.get(key[3], 0) + 1
+ position_bytes = stream.read(vertex_count * 12)
+ normal_bytes = stream.read(vertex_count * 12)
+ index_bytes = stream.read(index_count * 4)
+ masks = stream.read(mask_count)
+ if (len(position_bytes) != vertex_count * 12
+ or len(normal_bytes) != vertex_count * 12
+ or len(index_bytes) != index_count * 4
+ or len(masks) != mask_count):
+ raise ValueError(f"{path}: truncated tile payload")
+ if key[3] != 0:
+ continue
+ selected_tiles += 1
+ positions = array.array("f")
+ positions.frombytes(position_bytes)
+ normals = array.array("f")
+ normals.frombytes(normal_bytes)
+ indices = array.array("I")
+ indices.frombytes(index_bytes)
+ if sys.byteorder != "little":
+ positions.byteswap()
+ normals.byteswap()
+ indices.byteswap()
+ for offset in range(0, index_count - 2, 3):
+ if masks and masks[offset // 3]:
+ continue
+ ids = indices[offset:offset + 3]
+ if max(ids) >= surface_count:
+ continue
+ verts = tuple(tuple(float(positions[index * 3 + axis]) for axis in range(3))
+ for index in ids)
+ edge_a = tuple(verts[1][axis] - verts[0][axis] for axis in range(3))
+ edge_b = tuple(verts[2][axis] - verts[0][axis] for axis in range(3))
+ cross = (edge_a[1] * edge_b[2] - edge_a[2] * edge_b[1],
+ edge_a[2] * edge_b[0] - edge_a[0] * edge_b[2],
+ edge_a[0] * edge_b[1] - edge_a[1] * edge_b[0])
+ cross_length = math.sqrt(dot(cross, cross))
+ if cross_length <= 1.0e-12:
+ continue
+ face_normal = tuple(value / cross_length for value in cross)
+ area = cross_length * 0.5
+ smooth_normal = normalize(tuple(
+ sum(float(normals[index * 3 + axis]) for index in ids) for axis in range(3)))
+ if dot(face_normal, smooth_normal) < 0.0:
+ face_normal = tuple(-value for value in face_normal)
+ edge_squares = 0.0
+ for a, b in ((verts[0], verts[1]), (verts[1], verts[2]),
+ (verts[2], verts[0])):
+ edge_squares += sum((a[i] - b[i]) ** 2 for i in range(3))
+ quality = 4.0 * math.sqrt(3.0) * area / max(edge_squares, 1.0e-15)
+ triangles.append(Triangle(verts, area, face_normal, smooth_normal, quality))
+ if stream.read(1):
+ raise ValueError(f"{path}: trailing bytes after mesh tiles")
+ if not triangles:
+ raise ValueError(f"{path}: no LOD0 surface triangles")
+ return triangles, voxel_um, selected_tiles, lod_counts
+
+
+def bezier(points, t):
+ one = 1.0 - t
+ weights = (one ** 3, 3.0 * one * one * t, 3.0 * one * t * t, t ** 3)
+ return tuple(sum(weights[i] * points[i][axis] for i in range(4)) for axis in range(3))
+
+
+def make_route(layout):
+ edges = layout.get("cavern_edges", [])
+ rooms = layout.get("cavern_rooms", [])
+ if len(edges) != 1 or len(rooms) != 1:
+ raise ValueError("cavern audit expects one resolved room and one edge in its layout")
+ edge = edges[0]
+ points = [[value / 1_000_000.0 for value in row]
+ for row in edge["control_points_um"]]
+ bore_radius = edge["bore_radius_um"] / 1_000_000.0
+ bore_height = edge["bore_height_um"] / 1_000_000.0
+ samples = [bezier(points, index / 160.0) for index in range(161)]
+ horizontal_s = [0.0]
+ for a, b in zip(samples, samples[1:]):
+ horizontal_s.append(horizontal_s[-1] + math.hypot(b[0] - a[0], b[1] - a[1]))
+ first_dx = samples[1][0] - samples[0][0]
+ first_dy = samples[1][1] - samples[0][1]
+ first_len = max(1.0e-9, math.hypot(first_dx, first_dy))
+ extension = (samples[0][0] - first_dx / first_len * OPEN_GROUND_EXTENSION_M,
+ samples[0][1] - first_dy / first_len * OPEN_GROUND_EXTENSION_M,
+ samples[0][2] - (samples[1][2] - samples[0][2]) / first_len
+ * OPEN_GROUND_EXTENSION_M)
+ route_points = [extension, *samples]
+ route_s = [-OPEN_GROUND_EXTENSION_M, *horizontal_s]
+ segments = []
+ for i, (a, b) in enumerate(zip(route_points, route_points[1:])):
+ segments.append((a, b, route_s[i], route_s[i + 1]))
+ room = rooms[0]
+ room_center = tuple(value / 1_000_000.0 for value in room["center_um"])
+ room_half = tuple(value / 1_000_000.0 for value in room["half_extents_um"])
+ room_floor = room["floor_um"] / 1_000_000.0
+ return {
+ "segments": segments,
+ "edge": edge,
+ "bore_radius": bore_radius,
+ "bore_height": bore_height,
+ "room_center": room_center,
+ "room_half": room_half,
+ "room_floor": room_floor,
+ "route_points": samples,
+ "route_length": horizontal_s[-1],
+ }
+
+
+def route_nearest(point, route):
+ best = None
+ for a, b, s0, s1 in route["segments"]:
+ dx, dy = b[0] - a[0], b[1] - a[1]
+ denominator = dx * dx + dy * dy
+ t = 0.0 if denominator <= 1.0e-12 else max(0.0, min(1.0,
+ ((point[0] - a[0]) * dx + (point[1] - a[1]) * dy) / denominator))
+ x, y = a[0] + dx * t, a[1] + dy * t
+ distance = math.hypot(point[0] - x, point[1] - y)
+ if best is None or distance < best[0]:
+ segment_length = math.sqrt(max(denominator, 1.0e-12))
+ lateral = (dx * (point[1] - y) - dy * (point[0] - x)) / segment_length
+ best = (distance, s0 + (s1 - s0) * t,
+ a[2] + (b[2] - a[2]) * t, lateral)
+ return best
+
+
+def mark_region(triangles, route):
+ room_c = route["room_center"]
+ room_h = route["room_half"]
+ radius = route["bore_radius"] + CORRIDOR_MARGIN_M
+ for tri in triangles:
+ x, y, z = tri.center
+ distance, distance_s, floor_z, lateral = route_nearest(tri.center, route)
+ tri.route_distance = distance
+ tri.route_lateral = lateral
+ tri.route_s = distance_s
+ tri.floor_z = floor_z
+ corridor = (-OPEN_GROUND_EXTENSION_M <= distance_s <= route["route_length"]
+ and distance <= radius
+ and floor_z - CORRIDOR_MARGIN_M <= z
+ <= floor_z + route["bore_height"] + CORRIDOR_MARGIN_M)
+ room_box = (abs(x - room_c[0]) <= room_h[0] + CORRIDOR_MARGIN_M
+ and abs(y - room_c[1]) <= room_h[1] + CORRIDOR_MARGIN_M
+ and route["room_floor"] - CORRIDOR_MARGIN_M <= z
+ <= route["room_floor"] + 2.0 * room_h[2] + CORRIDOR_MARGIN_M)
+ tri.in_region = corridor or room_box
+ outside_bore = (distance > route["bore_radius"] + 0.45
+ or abs(x - room_c[0]) > room_h[0] + 0.45
+ or abs(y - room_c[1]) > room_h[1] + 0.45)
+ # Keep the reference on native, ground-facing terrain. The outer mesh
+ # also contains steep cavern walls and room edges, which are features.
+ tri.reference = tri.in_region and outside_bore and tri.smooth[2] >= 0.45
+
+
+def vertex_key(point):
+ return tuple(int(round(value / WELD_M)) for value in point)
+
+
+def make_edges(triangles, indices):
+ edge_map = {}
+ for index in indices:
+ tri = triangles[index]
+ points = tri.verts
+ for a, b in ((0, 1), (1, 2), (2, 0)):
+ ka, kb = vertex_key(points[a]), vertex_key(points[b])
+ if ka == kb:
+ continue
+ key = (ka, kb) if ka < kb else (kb, ka)
+ delta = tuple(points[a][axis] - points[b][axis] for axis in range(3))
+ length = math.sqrt(dot(delta, delta))
+ midpoint = tuple((points[a][axis] + points[b][axis]) * 0.5 for axis in range(3))
+ edge_map.setdefault(key, []).append((index, length, midpoint))
+ pairs = []
+ for incident in edge_map.values():
+ unique = {}
+ for index, length, midpoint in incident:
+ unique[index] = (length, midpoint)
+ incident = list(unique.items())
+ if len(incident) < 2:
+ continue
+ if len(incident) > 4:
+ incident = incident[:4]
+ for left in range(len(incident)):
+ for right in range(left + 1, len(incident)):
+ i, (length, midpoint) = incident[left]
+ j, _ = incident[right]
+ angle = max(angle_degrees(triangles[i].normal, triangles[j].normal),
+ 0.75 * angle_degrees(triangles[i].smooth, triangles[j].smooth))
+ pairs.append((i, j, angle, length, midpoint))
+ return pairs
+
+
+def cell_of(point):
+ return tuple(math.floor(value / CELL_M) for value in point)
+
+
+def score_surface(triangles, considered, pairs, edge_cutoff, rough_cutoff,
+ ref_cell_density):
+ considered_set = set(considered)
+ variation = {index: 0.0 for index in considered}
+ max_jump = {index: 0.0 for index in considered}
+ severe_count = {index: 0.0 for index in considered}
+ for i, j, angle, length, _midpoint in pairs:
+ if i not in considered_set or j not in considered_set:
+ continue
+ weighted = angle * length * 0.5
+ variation[i] += weighted
+ variation[j] += weighted
+ max_jump[i] = max(max_jump[i], angle)
+ max_jump[j] = max(max_jump[j], angle)
+ if angle >= edge_cutoff:
+ severe_count[i] += 0.5
+ severe_count[j] += 0.5
+ cells = {}
+ for index in considered:
+ tri = triangles[index]
+ key = cell_of(tri.center)
+ cell = cells.setdefault(key, {"area": 0.0, "variation": 0.0, "severe": 0.0,
+ "sliver_area": 0.0, "slivers": 0, "triangles": 0, "max_jump": 0.0,
+ "reference_area": 0.0, "reference_variation": 0.0,
+ "reference_severe": 0.0, "reference_sliver_area": 0.0,
+ "reference_slivers": 0})
+ cell["area"] += tri.area
+ cell["variation"] += variation[index]
+ cell["severe"] += severe_count[index]
+ cell["triangles"] += 1
+ cell["max_jump"] = max(cell["max_jump"], max_jump[index])
+ sliver = tri.quality < 0.07 and tri.area < 0.12
+ if sliver:
+ cell["sliver_area"] += tri.area
+ cell["slivers"] += 1
+ if tri.reference:
+ cell["reference_area"] += tri.area
+ cell["reference_variation"] += variation[index]
+ cell["reference_severe"] += severe_count[index]
+ if sliver:
+ cell["reference_sliver_area"] += tri.area
+ cell["reference_slivers"] += 1
+ ref_rough = []
+ ref_density = []
+ ref_sliver_fraction = []
+ for cell in cells.values():
+ if cell["reference_area"] >= max(0.15, cell["area"] * 0.65):
+ ref_rough.append(cell["reference_variation"] / max(cell["reference_area"], 1.0e-9))
+ ref_density.append(cell["reference_severe"] / max(cell["reference_area"], 1.0e-9))
+ ref_sliver_fraction.append(cell["reference_sliver_area"]
+ / max(cell["reference_area"], 1.0e-9))
+ reference_median = quantile(ref_rough, 0.5)
+ reference_mad = quantile([abs(value - reference_median) for value in ref_rough], 0.5)
+ cell_rough_limit = max(10.0, quantile(ref_rough, 0.995) * 1.5,
+ reference_median + 8.0 * reference_mad)
+ edge_density_limit = max(1.5, quantile(ref_density, 0.995) * 1.7,
+ ref_cell_density * 1.7)
+ sliver_limit = max(0.22, quantile(ref_sliver_fraction, 0.995) + 0.18)
+ hot = set()
+ for key, cell in cells.items():
+ area = max(cell["area"], 1.0e-9)
+ roughness = cell["variation"] / area
+ edge_density = cell["severe"] / area
+ sliver_fraction = cell["sliver_area"] / area
+ rough_cluster = roughness > cell_rough_limit and cell["severe"] >= 2.0
+ edge_cluster = cell["severe"] >= 4.0 and edge_density > edge_density_limit
+ sliver_cluster = cell["slivers"] >= 5 and sliver_fraction > sliver_limit
+ if rough_cluster or edge_cluster or sliver_cluster:
+ hot.add(key)
+ cell["roughness_deg_per_m"] = roughness
+ cell["severe_edge_density_m2"] = edge_density
+ cell["sliver_area_fraction"] = sliver_fraction
+ cell["reasons"] = [name for name, active in (("normal-variation", rough_cluster),
+ ("edge-density", edge_cluster), ("slivers", sliver_cluster)) if active]
+ clusters = []
+ flagged_cells = set()
+ remaining = set(hot)
+ while remaining:
+ stack = [remaining.pop()]
+ members = []
+ while stack:
+ current = stack.pop()
+ members.append(current)
+ x, y, z = current
+ neighbors = [(x + dx, y + dy, z + dz)
+ for dx in (-1, 0, 1) for dy in (-1, 0, 1) for dz in (-1, 0, 1)
+ if dx or dy or dz]
+ for neighbor in neighbors:
+ if neighbor in remaining:
+ remaining.remove(neighbor)
+ stack.append(neighbor)
+ cells_in_cluster = [cells[key] for key in members]
+ cluster_area = sum(cell["area"] for cell in cells_in_cluster)
+ cluster_severe = sum(cell["severe"] for cell in cells_in_cluster)
+ cluster_slivers = sum(cell["slivers"] for cell in cells_in_cluster)
+ accepted = len(members) >= 2 and cluster_area >= 0.25 and \
+ (cluster_severe >= 8.0 or cluster_slivers >= 12)
+ if accepted:
+ flagged_cells.update(members)
+ clusters.append({"cells": len(members), "area_m2": cluster_area,
+ "severe_edges": cluster_severe, "sliver_triangles": cluster_slivers,
+ "bounds_m": [[min(key[i] for key in members) * CELL_M,
+ (max(key[i] for key in members) + 1) * CELL_M]
+ for i in range(3)],
+ "reasons": sorted({reason for cell in cells_in_cluster
+ for reason in cell["reasons"]})})
+ clusters.sort(key=lambda cluster: (cluster["severe_edges"], cluster["area_m2"]), reverse=True)
+ flagged_triangles = [index for index in considered
+ if cell_of(triangles[index].center) in flagged_cells]
+ return {"cells": cells, "hot_cells": hot, "flagged_cells": flagged_cells,
+ "clusters": clusters, "hot_triangles": flagged_triangles,
+ "thresholds": {"edge_angle_deg": edge_cutoff,
+ "roughness_deg_per_m": cell_rough_limit,
+ "severe_edge_density_m2": edge_density_limit,
+ "sliver_area_fraction": sliver_limit,
+ "reference_cell_roughness_p995_deg_per_m": quantile(ref_rough, 0.995),
+ "reference_cell_roughness_median_deg_per_m": reference_median}}
+
+
+def floor_profile_audit(triangles, considered, region_indices, route):
+ """Measure the whole floor patch as area-weighted bins, not selected rails."""
+ if route is None:
+ return {"present": False, "detected": False, "triangles": 0, "bins": 0,
+ "lateral_bands": 0, "ripple_amplitude_m": 0.0,
+ "max_adjacent_step_m": 0.0}
+ along_step = 0.25
+ lateral_step = 0.5
+ bins = {}
+ floor_triangles = 0
+ wall_triangles = 0
+ wall_bins = set()
+ for index in region_indices:
+ tri = triangles[index]
+ height = tri.center[2] - tri.floor_z
+ if (0.0 <= tri.route_s <= 10.0
+ and route["bore_radius"] - 0.9 <= tri.route_distance
+ <= route["bore_radius"] + 1.0
+ and 0.45 <= height <= route["bore_height"] + 0.5
+ and abs(tri.smooth[2]) <= 0.45):
+ wall_triangles += 1
+ wall_bins.add(int(tri.route_s))
+ for index in considered:
+ tri = triangles[index]
+ if (tri.route_s < 0.0 or tri.route_s > min(10.0, route["route_length"])
+ or tri.route_distance > min(1.9, route["bore_radius"] - 0.1)
+ or abs(tri.center[2] - tri.floor_z) > 0.60
+ or tri.smooth[2] < 0.55):
+ continue
+ floor_triangles += 1
+ along_bin = int(math.floor(tri.route_s / along_step))
+ lateral_bin = int(math.floor((tri.route_lateral + 2.0) / lateral_step))
+ key = (lateral_bin, along_bin)
+ row = bins.setdefault(key, [0.0, 0.0, 0])
+ residual = tri.center[2] - tri.floor_z
+ row[0] += residual * tri.area
+ row[1] += tri.area
+ row[2] += 1
+ by_lateral = {}
+ for (lateral_bin, along_bin), (weighted_residual, area, count) in bins.items():
+ if area < 0.002:
+ continue
+ by_lateral.setdefault(lateral_bin, {})[along_bin] = weighted_residual / area
+ amplitudes = []
+ steps = []
+ valid_bins = 0
+ for values in by_lateral.values():
+ ordered = sorted(values.items())
+ run = []
+ runs = []
+ for item in ordered:
+ if run and item[0] != run[-1][0] + 1:
+ runs.append(run)
+ run = []
+ run.append(item)
+ if run:
+ runs.append(run)
+ for segment in runs:
+ if len(segment) < 8:
+ continue
+ valid_bins += len(segment)
+ raw = [value for _, value in segment]
+ highpass = []
+ for position, value in enumerate(raw):
+ nearby = raw[max(0, position - 4):min(len(raw), position + 5)]
+ highpass.append(value - sum(nearby) / len(nearby))
+ amplitudes.append(max(highpass) - min(highpass))
+ steps.extend(abs(raw[i] - raw[i - 1]) for i in range(1, len(raw)))
+ max_ripple = max(amplitudes, default=0.0) * 0.5
+ max_step = max(steps, default=0.0)
+ present = (floor_triangles >= 24 and valid_bins >= 8
+ and wall_triangles >= 24 and len(wall_bins) >= 3)
+ detected = present and (max_ripple > 0.015 or max_step > 0.050)
+ return {"present": present, "detected": detected, "triangles": floor_triangles,
+ "bins": valid_bins, "lateral_bands": len(by_lateral),
+ "wall_triangles": wall_triangles, "wall_bins": len(wall_bins),
+ "ripple_amplitude_m": max_ripple, "max_adjacent_step_m": max_step,
+ "thresholds": {"ripple_amplitude_m": 0.015, "adjacent_step_m": 0.050}}
+
+
+def wall_band_measure(pairs, route, edge_cutoff):
+ """Locate high dihedrals around the bore-foot and the L47 0.25 m band edge."""
+ if route is None:
+ return {}
+ bins = {}
+ radius = route["bore_radius"]
+ for _i, _j, angle, _length, midpoint in pairs:
+ distance, route_s, floor_z, _lateral = route_nearest(midpoint, route)
+ height = midpoint[2] - floor_z
+ if (0.0 <= route_s <= 10.0 and abs(distance - radius) <= 0.75
+ and -0.25 <= height <= 0.875):
+ index = int(math.floor((height + 0.25) / 0.125))
+ entry = bins.setdefault(index, [])
+ entry.append(angle)
+ result = {}
+ for index, angles in sorted(bins.items()):
+ center_height = (index + 0.5) * 0.125 - 0.25
+ result[f"{center_height:.3f}"] = {
+ "edges": len(angles),
+ "over_reference_cutoff": sum(value >= edge_cutoff for value in angles),
+ "p95_dihedral_deg": quantile(angles, 0.95),
+ "max_dihedral_deg": max(angles, default=0.0),
+ }
+ return result
+
+
+def radial_wall_measure(pairs, route, edge_cutoff):
+ """Measure dihedrals across the hard bore-radius gate at the entrance foot."""
+ if route is None:
+ return {}
+ bins = {}
+ radius = route["bore_radius"]
+ for _i, _j, angle, _length, midpoint in pairs:
+ distance, route_s, floor_z, _lateral = route_nearest(midpoint, route)
+ height = midpoint[2] - floor_z
+ if (-5.0 <= route_s <= 3.0 and -1.0 <= height <= 0.75
+ and abs(distance - radius) <= 1.0):
+ index = int(math.floor((distance - (radius - 1.0)) / 0.125))
+ bins.setdefault(index, []).append(angle)
+ return {f"{radius - 1.0 + (index + 0.5) * 0.125:.3f}": {
+ "edges": len(angles),
+ "over_reference_cutoff": sum(value >= edge_cutoff for value in angles),
+ "p95_dihedral_deg": quantile(angles, 0.95),
+ "max_dihedral_deg": max(angles, default=0.0),
+ } for index, angles in sorted(bins.items())}
+
+
+def svg_for(path, triangles, route, hot_triangles, label):
+ selected = [triangles[index] for index in hot_triangles]
+ all_region = [tri for tri in triangles if tri.in_region]
+ points = [point for tri in all_region for point in tri.verts]
+ if not points:
+ return
+ xmin, xmax = min(p[0] for p in points), max(p[0] for p in points)
+ ymin, ymax = min(p[1] for p in points), max(p[1] for p in points)
+ width, height, margin = 1200.0, 900.0, 35.0
+ sx = (width - 2 * margin) / max(1.0e-6, xmax - xmin)
+ sy = (height - 2 * margin) / max(1.0e-6, ymax - ymin)
+ scale = min(sx, sy)
+ draw_w, draw_h = (xmax - xmin) * scale, (ymax - ymin) * scale
+ ox, oy = (width - draw_w) * 0.5, (height - draw_h) * 0.5
+ def project(point):
+ return ox + (point[0] - xmin) * scale, height - oy - (point[1] - ymin) * scale
+ lines = [f'')
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_text("\n".join(lines) + "\n", encoding="utf-8")
+
+
+def audit_case(label, mesh_path, layout_path, expect, focus, svg_dir):
+ triangles, voxel_um, tile_count, lod_counts = read_mesh(mesh_path)
+ route = None
+ if layout_path != Path("-"):
+ layout = json.loads(layout_path.read_text(encoding="utf-8"))
+ route = make_route(layout)
+ mark_region(triangles, route)
+ region_indices = [i for i, tri in enumerate(triangles) if tri.in_region]
+ else:
+ region_indices = list(range(len(triangles)))
+ for tri in triangles:
+ tri.in_region = True
+ tri.reference = True
+ if len(region_indices) < 24:
+ raise ValueError(f"{label}: only {len(region_indices)} triangles intersect the ROI")
+ reference_indices = [i for i in region_indices if triangles[i].reference]
+ if len(reference_indices) < 24:
+ reference_indices = region_indices
+ for index in region_indices:
+ triangles[index].reference = True
+ edge_pairs = make_edges(triangles, region_indices)
+ reference_edges = [edge[2] for edge in edge_pairs
+ if triangles[edge[0]].reference and triangles[edge[1]].reference]
+ edge_cutoff = max(35.0, quantile(reference_edges, 0.995))
+ reference_set = set(reference_indices)
+ ref_roughness = {index: 0.0 for index in reference_indices}
+ for i, j, angle, length, _midpoint in edge_pairs:
+ if i in reference_set and j in reference_set:
+ ref_roughness[i] += angle * length * 0.5
+ ref_roughness[j] += angle * length * 0.5
+ ref_values = [ref_roughness[index] / max(triangles[index].area, 1.0e-9)
+ for index in reference_indices]
+ rough_cutoff = 10.0
+ ref_density = 0.0
+ ref_area = sum(triangles[index].area for index in reference_indices)
+ ref_severe = 0.0
+ for i, j, angle, _length, _midpoint in edge_pairs:
+ if i in reference_set and j in reference_set and angle >= edge_cutoff:
+ ref_severe += 1.0
+ ref_density = ref_severe / max(ref_area, 1.0e-9)
+ if focus == "floor":
+ considered = [i for i in region_indices if route is not None
+ and triangles[i].route_s >= 1.5
+ and triangles[i].route_s <= 10.0
+ and triangles[i].route_distance <= 1.25
+ and abs(triangles[i].center[2] - triangles[i].floor_z) <= 0.24
+ and triangles[i].smooth[2] >= 0.55]
+ else:
+ considered = region_indices
+ if len(considered) < 24:
+ raise ValueError(f"{label}: focus={focus} has only {len(considered)} triangles")
+ scores = score_surface(triangles, considered, edge_pairs, edge_cutoff,
+ rough_cutoff, ref_density)
+ floor_profile = floor_profile_audit(triangles, considered, region_indices, route)
+ wall_band = wall_band_measure(edge_pairs, route, edge_cutoff)
+ radial_wall = radial_wall_measure(edge_pairs, route, edge_cutoff)
+ found = bool(scores["clusters"]) or floor_profile["detected"]
+ passed = expect == "measure" or (expect == "fail" and found) or (expect == "pass" and not found)
+ if svg_dir and route is not None:
+ svg_for(svg_dir / f"flagged-regions-{label}.svg", triangles, route,
+ scores["hot_triangles"], label)
+ cluster_summary = scores["clusters"][:8]
+ result = {
+ "label": label,
+ "mesh": str(mesh_path),
+ "layout": None if layout_path == Path("-") else str(layout_path),
+ "focus": focus,
+ "expected": expect,
+ "observed": "fail" if found else "pass",
+ "status": "PASS" if passed else "FAIL",
+ "voxel_um": voxel_um,
+ "lod0_tiles": tile_count,
+ "lod_tile_counts": lod_counts,
+ "triangles_loaded_lod0": len(triangles),
+ "triangles_in_roi": len(region_indices),
+ "triangles_audited": len(considered),
+ "adjacent_pairs": len(edge_pairs),
+ "reference_triangles": len(reference_indices),
+ "reference_edge_p995_deg": quantile(reference_edges, 0.995),
+ "reference_p995_normal_variation_deg_per_m": quantile(ref_values, 0.995),
+ "thresholds": scores["thresholds"],
+ "flagged_cells": len(scores["hot_cells"]),
+ "flagged_triangles": len(scores["hot_triangles"]),
+ "floor_profile": floor_profile,
+ "wall_band_height_histogram": wall_band,
+ "radial_wall_histogram": radial_wall,
+ "clusters": cluster_summary,
+ }
+ print(f"[WholeSurfaceAudit] label={label} focus={focus} lod0_tiles={tile_count} "
+ f"triangles_roi={len(region_indices)} audited={len(considered)} "
+ f"adjacent_pairs={len(edge_pairs)} reference={len(reference_indices)} "
+ f"edge_cutoff_deg={edge_cutoff:.2f} "
+ f"roughness_cutoff_deg_m={scores['thresholds']['roughness_deg_per_m']:.2f} "
+ f"hot_cells={len(scores['hot_cells'])} flagged_triangles={len(scores['hot_triangles'])} "
+ f"clusters={len(scores['clusters'])} observed={result['observed'].upper()} "
+ f"expected={expect.upper()} status={result['status']}")
+ print(f"[WholeSurfaceFloorProfile] label={label} triangles={floor_profile['triangles']} "
+ f"bins={floor_profile['bins']} lateral_bands={floor_profile['lateral_bands']} "
+ f"wall_triangles={floor_profile.get('wall_triangles', 0)} "
+ f"wall_bins={floor_profile.get('wall_bins', 0)} "
+ f"ripple_amplitude_m={floor_profile['ripple_amplitude_m']:.4f} "
+ f"max_adjacent_step_m={floor_profile['max_adjacent_step_m']:.4f} "
+ f"observed={'FAIL' if floor_profile['detected'] else ('PASS' if floor_profile['present'] else 'NOT_PRESENT')}")
+ if wall_band:
+ print(f"[WholeSurfaceWallBandMeasure] label={label} voxel_m={voxel_um / 1_000_000.0:.3f} "
+ f"recarve_edge_height_m=0.250 radial_band_m={route['bore_radius']:.3f} "
+ f"height_histogram={json.dumps(wall_band, sort_keys=True, separators=(',', ':'))}")
+ print(f"[WholeSurfaceRadialWallMeasure] label={label} route_s_m=-5..3 "
+ f"height_above_floor_m=-1..0.75 bore_radius_m={route['bore_radius']:.3f} "
+ f"radial_histogram={json.dumps(radial_wall, sort_keys=True, separators=(',', ':'))}")
+ for index, cluster in enumerate(cluster_summary):
+ print(f"[WholeSurfaceCluster] label={label} index={index} cells={cluster['cells']} "
+ f"area_m2={cluster['area_m2']:.3f} severe_edges={cluster['severe_edges']:.1f} "
+ f"slivers={cluster['sliver_triangles']} bounds_m={cluster['bounds_m']} "
+ f"reasons={','.join(cluster['reasons'])}")
+ hot_cells = scores["cells"]
+ for key in sorted(scores["hot_cells"], key=lambda item: (
+ hot_cells[item]["severe"], hot_cells[item]["variation"]), reverse=True)[:12]:
+ cell = hot_cells[key]
+ local = route_nearest(tuple((axis + 0.5) * CELL_M for axis in key), route) if route else None
+ print(f"[WholeSurfaceHotCell] label={label} cell_m=({key[0]},{key[1]},{key[2]}) "
+ f"route_s_m={local[1] if local else 0.0:.2f} radial_m={local[0] if local else 0.0:.2f} "
+ f"height_above_floor_m={(key[2] + 0.5 - local[2]) if local else 0.0:.2f} "
+ f"area_m2={cell['area']:.3f} roughness_deg_m={cell['roughness_deg_per_m']:.2f} "
+ f"severe_edges={cell['severe']:.1f} edge_density_m2={cell['severe_edge_density_m2']:.2f} "
+ f"slivers={cell['slivers']} max_jump_deg={cell['max_jump']:.2f} "
+ f"reasons={','.join(cell['reasons'])}")
+ return result
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--case", nargs=5, action="append", required=True,
+ metavar=("LABEL", "MESH", "LAYOUT_OR_DASH", "EXPECT", "FOCUS"))
+ parser.add_argument("--json", type=Path, required=True)
+ parser.add_argument("--svg-dir", type=Path)
+ args = parser.parse_args()
+ results = []
+ for label, mesh, layout, expect, focus in args.case:
+ if expect not in ("pass", "fail", "measure") or focus not in ("whole", "floor"):
+ parser.error("EXPECT must be pass/fail/measure and FOCUS must be whole/floor")
+ results.append(audit_case(label, Path(mesh), Path(layout), expect, focus, args.svg_dir))
+ args.json.parent.mkdir(parents=True, exist_ok=True)
+ args.json.write_text(json.dumps({"audit": "whole-surface-l48-v1", "cases": results},
+ indent=2) + "\n", encoding="utf-8")
+ failures = [result for result in results if result["status"] != "PASS"]
+ print(f"[WholeSurfaceAuditSummary] cases={len(results)} failed={len(failures)} "
+ f"json={args.json} status={'FAIL' if failures else 'PASS'}")
+ return 1 if failures else 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/Standalone/Research/WorldAssemblerM3/Linux48/CaptureLandmarkL48.py b/Standalone/Research/WorldAssemblerM3/Linux48/CaptureLandmarkL48.py
new file mode 100644
index 0000000..6be541b
--- /dev/null
+++ b/Standalone/Research/WorldAssemblerM3/Linux48/CaptureLandmarkL48.py
@@ -0,0 +1,410 @@
+#!/usr/bin/env python3
+"""Capture L48 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"[L48CaptureRun] 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("L48 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-l48.vfmesh"
+native_mesh = ARTIFACT_DIR / "control-plain-native.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)
+run_logged("plain-native-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",
+ f"--camera={fmt(camera_spec(export_camera, export_target))}",
+ f"--mesh-export={native_mesh}",
+ f"--resolved-layout-output={ARTIFACT_DIR / 'control-plain-native-layout.json'}"],
+ 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-l48.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"[L48CaptureCamera] 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"[L48CaptureJPEG] 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"[L48CaptureShapeMetrics] file={metrics_path} status=PASS", flush=True)
diff --git a/Standalone/Research/WorldAssemblerM3/Linux48/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux48/REPORT.md
new file mode 100644
index 0000000..d2e4df0
--- /dev/null
+++ b/Standalone/Research/WorldAssemblerM3/Linux48/REPORT.md
@@ -0,0 +1,60 @@
+# Linux48 report
+
+## Whole-surface audit
+
+`AuditWholeSurfaceL48.py` scans every LOD0 triangle in the route corridor plus 3 m and the chamber box plus 3 m. It welds shared edges, compares geometric face normals, and scores area-weighted normal variation, high-angle edge density, maximum jumps and sliver area in 1 m cells. Connected hot cells become located clusters. A separate floor pass bins the full floor patch at 0.25 m along the route and 0.5 m across it, removes a local moving-mean trend, and checks ripple and adjacent steps. Thresholds use ground-facing native patches from the same export. JSON results and red-triangle XY maps are emitted.
+
+The detector has no general quadric residual or periodic-spectrum metric yet; terraces outside its floor patch rely on the normal-cluster measures. The control results are:
+
+| Mesh | Result | Evidence |
+|---|---|---|
+| L45 ramp | Expected fail | One normal cluster, 0.669 m² |
+| L46 entrance | Expected fail | 84.3 mm floor ripple; 159.1 mm largest step |
+| L47 wall-foot | Expected fail | One 5-cell cluster, 5.243 m², 42 severe edges; floor profile 34.5/57.6 mm ripple/step |
+| Plain native | Pass | No clusters; floor profile not present |
+| L47 smooth floor | Pass | 0.59 mm ripple; 1.15 mm largest step |
+| L48 candidate | **Fail** | Two clusters, 7.095 m² total, 65.5 severe edges, 303 flagged triangles; floor profile 34.5/57.6 mm |
+
+The L47 cluster is at approximately `x=[-5,0], y=[22,25], z=[-505,-504] m`. At 0.188 m above the floor its wall-band p95 dihedral is 54.6° with a 160.4° maximum; the selected floor rails did not reach this band. The complete control output is in `Saved/Linux/l48/whole-surface-audit.json`.
+
+## Teeth measurement and attempted fix
+
+The L47 measurements localize the teeth to the floor-cap/bore-wall junction. The post-stamp cap has a hard radial cutoff there, and the floor-adjacent re-carve also uses a non-SDF weighted blend. A re-carve fade alone left the teeth in the capture. The final trial replaced both cavern cap gates with a rounded max of the sloped-floor distance and radial bore distance, restoring the L47 re-carve weight. This reduced the apparent tooth height in the capture but did not clear the whole-surface gate; the final mesh has more flagged area and a second cluster. The hard cap edge is implicated, but the cause is not proven and this is not a validated fix.
+
+## Hashes and cost
+
+Clang and GCC builds used source stamp `05ea28750b2ee89aa61655c386c6d74a6841c4d071374cda03c73a89b5efb578`.
+
+Demo-off whole-view hashes match L47 exactly at W1 and W12:
+
+- floating-islands: density `598e9daaa1744ccb`, mesh `3114e0f4f6dbf734`, payload `c49c4e48bff74089`, topology `cadd4edd10d4d8ef`.
+- surface-world: density `7dc75e2c09302258`, mesh `0cb34c40ceec4ce3`, payload `9d23e8c46b515cf1`, topology `2962c9464991eedd`.
+
+Fast took 202.7 s. Medium took 637.6 s; the whole-surface stage took 161.8 s and failed on L48. Full and its five-pair cost comparison were not run because medium did not pass. All build and verification processes used `TMPDIR=Saved/Linux/l48/tmp`; no command wrote to `/tmp`.
+
+## Tiers and captures
+
+| Tier | Result |
+|---|---|
+| Fast | Pass, 202.7 s |
+| Medium | Fail, 637.6 s: L48 whole-surface gate |
+| Full | Not run: medium prerequisite failed |
+
+The five captures are `ground-entrance.jpg`, `entrance-looking-in.jpg`, `overview-entrance-and-chamber.jpg`, `inside-to-dais.jpg`, and `entrance-floor-low.jpg` in `captures/`. Flag maps: `flagged-regions-L47-wall-foot-control.svg` and `flagged-regions-L48-final.svg` (red flagged triangles, gray ROI, blue route).
+
+## Failed runs
+
+- The first fast invocation had no built binary; after the Clang/GCC builds, fast passed.
+- Bypassing the post-stamp floor cap failed entrance rails. Broad SmoothMin/cap-mask trials either closed the mouth or failed the built-shape check; one planar mouth patch measured 5.575 m².
+- Radial re-carve fades at 0.5 m and 1 m preserved the rails but left visible teeth and failed the whole-surface audit. Changing the support inset had no mesh effect.
+- The final rounded-cap trial passed the legacy rails and shape checks, but failed medium with the clusters above. The 15-trial budget is exhausted. No full tier, commit, push or stash was performed.
+
+## Orchestrator review (Claude)
+
+Split. **The whole-surface audit is the result of this round,** and it's committed here as a tool, not yet wired into
+the tiers. All three historical defects fail it (L45 ramp, L46 entrance, L47 wall foot) and plain native terrain
+passes. It also shows that L47's floor near the walls carries 34.5 mm ripple and a 57.6 mm step, which the three
+rails never sampled. **The Core change (rounded cap) did not pass** and is parked with the tier wiring on
+`wip/l48-surface-audit`. Wiring the gate now would turn medium red on HEAD, because the flagged demo still has the
+teeth. `assembler/world` stays on L47 Core. L49 builds the floor and bore as one SDF, with a filleted floor/wall
+corner, before stamps, instead of patching with a post-pass cap.
diff --git a/Standalone/Research/WorldAssemblerM3/Linux48/captures/entrance-floor-low.jpg b/Standalone/Research/WorldAssemblerM3/Linux48/captures/entrance-floor-low.jpg
new file mode 100644
index 0000000..60c3d7e
Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux48/captures/entrance-floor-low.jpg differ
diff --git a/Standalone/Research/WorldAssemblerM3/Linux48/captures/entrance-looking-in.jpg b/Standalone/Research/WorldAssemblerM3/Linux48/captures/entrance-looking-in.jpg
new file mode 100644
index 0000000..5669e25
Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux48/captures/entrance-looking-in.jpg differ
diff --git a/Standalone/Research/WorldAssemblerM3/Linux48/captures/flagged-regions-L45-ramp-control.svg b/Standalone/Research/WorldAssemblerM3/Linux48/captures/flagged-regions-L45-ramp-control.svg
new file mode 100644
index 0000000..4724129
--- /dev/null
+++ b/Standalone/Research/WorldAssemblerM3/Linux48/captures/flagged-regions-L45-ramp-control.svg
@@ -0,0 +1,57325 @@
+
diff --git a/Standalone/Research/WorldAssemblerM3/Linux48/captures/flagged-regions-L46-entrance-control.svg b/Standalone/Research/WorldAssemblerM3/Linux48/captures/flagged-regions-L46-entrance-control.svg
new file mode 100644
index 0000000..79b819b
--- /dev/null
+++ b/Standalone/Research/WorldAssemblerM3/Linux48/captures/flagged-regions-L46-entrance-control.svg
@@ -0,0 +1,48273 @@
+
diff --git a/Standalone/Research/WorldAssemblerM3/Linux48/captures/flagged-regions-L47-smooth-floor-control.svg b/Standalone/Research/WorldAssemblerM3/Linux48/captures/flagged-regions-L47-smooth-floor-control.svg
new file mode 100644
index 0000000..98c77fa
--- /dev/null
+++ b/Standalone/Research/WorldAssemblerM3/Linux48/captures/flagged-regions-L47-smooth-floor-control.svg
@@ -0,0 +1,53883 @@
+
diff --git a/Standalone/Research/WorldAssemblerM3/Linux48/captures/flagged-regions-L47-wall-foot-control.svg b/Standalone/Research/WorldAssemblerM3/Linux48/captures/flagged-regions-L47-wall-foot-control.svg
new file mode 100644
index 0000000..b4dfae8
--- /dev/null
+++ b/Standalone/Research/WorldAssemblerM3/Linux48/captures/flagged-regions-L47-wall-foot-control.svg
@@ -0,0 +1,54131 @@
+
diff --git a/Standalone/Research/WorldAssemblerM3/Linux48/captures/flagged-regions-L48-final.svg b/Standalone/Research/WorldAssemblerM3/Linux48/captures/flagged-regions-L48-final.svg
new file mode 100644
index 0000000..3f95756
--- /dev/null
+++ b/Standalone/Research/WorldAssemblerM3/Linux48/captures/flagged-regions-L48-final.svg
@@ -0,0 +1,54362 @@
+
diff --git a/Standalone/Research/WorldAssemblerM3/Linux48/captures/flagged-regions-L48-smooth-floor.svg b/Standalone/Research/WorldAssemblerM3/Linux48/captures/flagged-regions-L48-smooth-floor.svg
new file mode 100644
index 0000000..dbf7cae
--- /dev/null
+++ b/Standalone/Research/WorldAssemblerM3/Linux48/captures/flagged-regions-L48-smooth-floor.svg
@@ -0,0 +1,54059 @@
+
diff --git a/Standalone/Research/WorldAssemblerM3/Linux48/captures/flagged-regions-plain-native-control.svg b/Standalone/Research/WorldAssemblerM3/Linux48/captures/flagged-regions-plain-native-control.svg
new file mode 100644
index 0000000..ae6cfdd
--- /dev/null
+++ b/Standalone/Research/WorldAssemblerM3/Linux48/captures/flagged-regions-plain-native-control.svg
@@ -0,0 +1,23392 @@
+
diff --git a/Standalone/Research/WorldAssemblerM3/Linux48/captures/ground-entrance.jpg b/Standalone/Research/WorldAssemblerM3/Linux48/captures/ground-entrance.jpg
new file mode 100644
index 0000000..9e91e4c
Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux48/captures/ground-entrance.jpg differ
diff --git a/Standalone/Research/WorldAssemblerM3/Linux48/captures/inside-to-dais.jpg b/Standalone/Research/WorldAssemblerM3/Linux48/captures/inside-to-dais.jpg
new file mode 100644
index 0000000..9422668
Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux48/captures/inside-to-dais.jpg differ
diff --git a/Standalone/Research/WorldAssemblerM3/Linux48/captures/overview-entrance-and-chamber.jpg b/Standalone/Research/WorldAssemblerM3/Linux48/captures/overview-entrance-and-chamber.jpg
new file mode 100644
index 0000000..106e254
Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux48/captures/overview-entrance-and-chamber.jpg differ
diff --git a/Standalone/Research/WorldAssemblerM3/Research15/REPORT.md b/Standalone/Research/WorldAssemblerM3/Research15/REPORT.md
new file mode 100644
index 0000000..249db7f
--- /dev/null
+++ b/Standalone/Research/WorldAssemblerM3/Research15/REPORT.md
@@ -0,0 +1,52 @@
+# R15 — Whole-surface artifact audit
+
+## Key recommendations
+
+1. Scan every LOD0 triangle in the exported region using three area-weighted metrics over overlapping surface patches: **detrended geometric residual**, **smoothed normal variation**, and **periodic-band coherence**. Group adjacent anomalous patches into clusters before failing the mesh.
+2. Calibrate each scale against plain native terrain exported by the same mesher at 25 cm. Choose thresholds on held-out clean regions, then lock them. The gate should pass clean references and reject **each of the five pre-fix defect meshes**; if one slips through, improve the detector rather than tune the threshold around that mesh.
+3. Add the offline check after mesh export in `verify.py`’s medium capture-and-audit stage. Report cluster coordinates and scores, plus a false-color mesh or marked capture. Full tier already reuses passed medium evidence; fast would need a small LOD0 export to include this gate.
+
+## Metrics
+
+For triangle \(f=(v_0,v_1,v_2)\), compute area \(A_f=\frac12\|(v_1-v_0)\times(v_2-v_0)\|\) and **geometric** face normal \(n_f\) from its positions. Use these normals for shape checks; stored smooth MC normals can hide or exaggerate geometric changes.
+
+- **Detrended residual \(D_r\):** Fit a robust local quadric \(q_r\) over a geodesic neighborhood of radius \(r\), and measure the area-weighted RMS signed normal distance:
+ \[
+ D_r(P)=\sqrt{\frac{\sum_{v\in P}A_v\,\delta_r(v)^2}{\sum_{v\in P}A_v}},
+ \qquad \delta_r(v)=(v-q_r(v))\cdot n_r(v).
+ \]
+ Also retain the 95th percentile and largest adjacent residual jump in the patch. This measures relief and steps without assuming the surface is a floor or wall.
+
+- **Smoothed normal variation \(G_r\):** For adjacent faces \(f,g\), let \(\theta_{fg}=\cos^{-1}(n_f\cdot n_g)\) and \(d_{fg}\) be their geodesic center distance. Average normals over radius \(r\) first, then compute an area-weighted RMS of \(\theta_{fg}/d_{fg}\). This catches clustered creases and teeth while reducing single-triangle MC facet noise.
+
+- **Periodic coherence \(C_\lambda\):** Take a spectrum or autocorrelation of \(\delta_r\) across overlapping surface patches, in all local tangent directions. Score the strongest narrow-band energy relative to neighboring frequencies, and require phase coherence across adjacent patches. Search wavelengths from roughly two to 32 voxels (0.5–8 m); use multiple window sizes so the longest periods have enough support. This detects repeated ribs, washboard, and seams without relying on a chosen rail.
+
+Use all three as a small, interpretable suite. A raw dihedral histogram loses location and mixes ordinary MC facets with real creases. Discrete Gaussian curvature \(K_i=(2\pi-\sum_j\theta_{ij})/A_i^*\) and cotangent-Laplacian mean curvature are useful visual diagnostics, but raw one-ring estimates can be noisy on irregular MC triangles; prefer multi-scale smoothing or quadric fits. Triangle quality, such as \(2r/R\) or minimum altitude-to-edge ratio, is a useful secondary mesh-health check, not a primary artifact detector: ridges and staircases need not contain slivers.
+
+MC linearly interpolates grid-edge crossings; the output can miss undersampled details, contain poor-shaped facets, and fail to preserve sharp features. That makes universal angle, curvature, or triangle-quality cutoffs unreliable. [CGAL’s isosurfacing manual](https://cgal.github.io/9465/doc/Isosurfacing_3/index.html) and [Dietrich et al. on MC triangle quality](https://doi.org/10.1109/TVCG.2008.60) document these limitations. [MeshLab](https://github.com/cnr-isti-vclab/meshlab/blob/main/docs/dox/filter.dox) and [libigl](https://libigl.github.io/tutorial/) provide useful curvature and quality measures; libigl also documents quadric curvature fitting. The specific three-metric combination above is a recommendation inferred from those methods and VoxelForge’s regressions, not a published universal detector.
+
+## Reference and threshold method
+
+Build a reference set from native-only LOD0 meshes using the same 25 cm mesher, recipes, and export path. Match patches by surface orientation and broad shape class; compare patch scores, not individual triangle scores. Use a held-out clean set to set per-scale thresholds from the distribution of **per-export maximum cluster scores** (with block bootstrap or equivalent uncertainty), so scanning more surface does not silently increase false alarms.
+
+Threshold a connected cluster of adjacent anomalous patches, with a minimum footprint calibrated to the smallest visible defect. Ignore isolated single-triangle excursions unless a separate topology or fragment check catches them. Lock thresholds before scoring candidate fixes. Require every pre-fix defect mesh to fail individually and clean native references to pass. Keep all five bad meshes as fixtures; the native holdout guards against thresholds that simply reject rough terrain.
+
+## Defect-to-metric map
+
+| Known defect | Primary detector | Evidence and expected signal |
+|---|---|---|
+| **L35 wall ribs** | \(C_\lambda + G_r\) | The old wall has measured periods near 4.75 m in normals and 7.44 m in radius. A coherent band should cluster over the wall area. [L35 report](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux35/REPORT.md:5) |
+| **L36 floor washboard** | \(C_\lambda + D_r\) | The old floor repeats at 1.438 m and 7.250 m, with 2.25–2.48 cm amplitude. Whole-surface patches include the tread that the wall-only scan missed. [L36 report](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux36/REPORT.md:5) |
+| **L45 segmented ramp** | \(C_\lambda + D_r\) | Segment seams create repeated surface residuals along the ramp. The L45 control already fails its floor-ripple gate at 17.9 mm against a 15 mm limit. [L45 report](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux45/REPORT.md:42) [L46 report](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux46/REPORT.md:15) |
+| **L46 entrance staircase** | \(D_r\), including adjacent residual jump; \(G_r\) | The first 7 m measures 39.9–51.4 mm ripple and 81.8–82.9 mm residual steps. Detrending against the sloped floor separates the steps from the intended grade. [L47 report](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux47/REPORT.md:5) |
+| **L47 wall-foot sawteeth** | \(G_r + D_r\), clustered | Scan all connected faces, including the wall foot. The existing three floor rails do not reach it. [L47 report](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux47/REPORT.md:31) |
+
+This three-metric set should catch all five while passing smooth native terrain **if** the matched reference and control gate confirm the separation. The reports establish the defects and measured signals; they do not establish final mesh-wide thresholds.
+
+## Implementation and reporting
+
+The existing `.vfmesh` readers expose tile LOD, voxel size, positions, normals, indices, and masks. Extend the tile-streaming approach used by [the ring audit](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py) and [the tile scanner](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux32/ScanMeshSurfaceTilesL32.py): analyze the whole target region at LOD0, include all face orientations, and weld duplicate vertices at same-LOD tile boundaries. Keep the audit offline on exported geometry.
+
+The natural insertion point is after `CaptureLandmarkL47.py` and passage mesh exports in [`run_captures_and_mesh_audits()`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Tools/verify/verify.py:651). That stage runs in medium and is reused by full; fast currently has no equivalent capture export. A fast-tier version would need a small targeted LOD0 export. [Voxel Tools’ smooth-terrain documentation](https://voxel-tools.readthedocs.io/en/latest/smooth_terrain/) notes that voxel LOD stitches can still show sharp steps, while [SAGA’s Terrain Ruggedness Index](https://saga-gis.sourceforge.io/saga_tool_doc/7.6.1/ta_morphometry_16.html) illustrates the value of neighborhood-scale terrain measures; its DEM assumptions do not cover arbitrary tunnel surfaces.
+
+For each cluster, report world-space centroid, bounds, geodesic area and extent, triangle IDs, strongest metric, scale or wavelength, value, threshold, LOD, voxel size, and mesh/layout identity. Emit JSON plus a compact CSV, and a false-color mesh or marked image with cluster IDs and a reproducible camera pose. MeshLab’s face-quality coloring is one available review path.
\ No newline at end of file