- Unreal: ready queue depth 8 -> 16 (cap 4 worker-sets / 64), startup mesh-apply cap 12 -> 24 within the unchanged 14 ms budget. Owner SW arrival, same 2,850 keys: initial set ready 4.78 -> 3.74 s, queue p95 123 -> 35 ms, request-to-apply p95 139 -> 45 ms; walkable gate 1.07 -> 1.11 s; per-frame apply p95 0.73 ms. - Core composer: mouth-support segments filtered by tile bounds, mouth relief result reused (route checks -80/-87% in passage tiles). Hashes identical W1/W8/W12, Clang and GCC. - Opt-in per-tile composer counters. Verify control meshes stored as .xz (78.5 -> 18.4 MB). - Fast tier 269 PASS. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
546 lines
24 KiB
Python
546 lines
24 KiB
Python
#!/usr/bin/env python3
|
|
"""Measure player-scale planar aprons, straight boundaries and corners at passage mouths."""
|
|
import argparse
|
|
import importlib.util
|
|
import math
|
|
from collections import defaultdict, deque
|
|
import os
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[4]
|
|
BASELINES = Path(os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l37")) / "baselines"
|
|
PROFILE_PATH = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py"
|
|
SPEC = importlib.util.spec_from_file_location("ring_profile_l37", PROFILE_PATH)
|
|
PROFILE = importlib.util.module_from_spec(SPEC)
|
|
SPEC.loader.exec_module(PROFILE)
|
|
|
|
# Frozen from player scale before measuring the L37 candidate. The 0.75 m line limit
|
|
# is just over the 0.68 m capsule width; 0.50 m2 is one capsule-width footprint.
|
|
STRAIGHT_LIMIT_M = 0.75
|
|
PLANE_LIMIT_M2 = 0.50
|
|
CORNER_TURN_LIMIT_DEG = 30.0
|
|
PLANE_TOLERANCE_M = 0.02
|
|
NORMAL_BUCKET = 0.07
|
|
PLANE_BUCKET_M = 0.03
|
|
MOUTH_CORRIDOR_HALF_WIDTH_M = 1.50
|
|
MOUTH_CORRIDOR_LENGTH_M = 12.0
|
|
MOUTH_Z_BELOW_M = 4.0
|
|
MOUTH_Z_ABOVE_M = 3.0
|
|
PLAYER_FLOOR_OFFSET_M = 1.13
|
|
DELTA_THRESHOLD_M = 0.05
|
|
DELTA_GRID_M = 0.125
|
|
SCENES = {
|
|
"owner-fi": ("owner-fi-ground-mouth.vfmesh", "layout-owner-fi.json", "FloatingIslands", "upper"),
|
|
"owner-sw": ("owner-sw-ground-mouth.vfmesh", "layout-owner-sw.json", "SurfaceWorld", "lower"),
|
|
"style-override-upper-sw-fixture-style0": (
|
|
"style-override-upper-sw-fixture-style0-ground-mouth.vfmesh",
|
|
"layout-style-override-upper-sw-fixture-style0.json", "SurfaceWorld", "upper"),
|
|
}
|
|
|
|
|
|
def sub(a, b):
|
|
return tuple(a[i] - b[i] for i in range(3))
|
|
|
|
|
|
def dot(a, b):
|
|
return sum(a[i] * b[i] for i in range(3))
|
|
|
|
|
|
def cross(a, b):
|
|
return (a[1] * b[2] - a[2] * b[1],
|
|
a[2] * b[0] - a[0] * b[2],
|
|
a[0] * b[1] - a[1] * b[0])
|
|
|
|
|
|
def length(a):
|
|
return math.sqrt(dot(a, a))
|
|
|
|
|
|
def face(points):
|
|
n = cross(sub(points[1], points[0]), sub(points[2], points[0]))
|
|
twice_area = length(n)
|
|
if twice_area <= 1.0e-12:
|
|
return None
|
|
area = twice_area * 0.5
|
|
n = tuple(v / twice_area for v in n)
|
|
if n[2] < 0.0:
|
|
n = tuple(-v for v in n)
|
|
d = -dot(n, points[0])
|
|
return n, d, area
|
|
|
|
|
|
def vertex_key(point):
|
|
return tuple(round(value, 4) for value in point)
|
|
|
|
|
|
def edge_key(a, b):
|
|
ka, kb = vertex_key(a), vertex_key(b)
|
|
return (ka, kb) if ka < kb else (kb, ka)
|
|
|
|
|
|
def rdp(points, epsilon):
|
|
if len(points) <= 2:
|
|
return points
|
|
a, b = points[0], points[-1]
|
|
ab = sub(b, a)
|
|
ab2 = dot(ab, ab)
|
|
maximum, index = -1.0, -1
|
|
for i in range(1, len(points) - 1):
|
|
ap = sub(points[i], a)
|
|
t = max(0.0, min(1.0, dot(ap, ab) / ab2)) if ab2 else 0.0
|
|
closest = tuple(a[j] + t * ab[j] for j in range(3))
|
|
distance = length(sub(points[i], closest))
|
|
if distance > maximum:
|
|
maximum, index = distance, i
|
|
if maximum <= epsilon:
|
|
return [a, b]
|
|
left = rdp(points[:index + 1], epsilon)
|
|
right = rdp(points[index:], epsilon)
|
|
return left[:-1] + right
|
|
|
|
|
|
def simplified_boundary(component, records):
|
|
counts = defaultdict(int)
|
|
actual_edges = {}
|
|
for index in component:
|
|
points = records[index]["points"]
|
|
for a, b in ((points[0], points[1]), (points[1], points[2]),
|
|
(points[2], points[0])):
|
|
key = edge_key(a, b)
|
|
counts[key] += 1
|
|
actual_edges[key] = (key[0], key[1])
|
|
boundary = [edge for edge, count in counts.items() if count == 1]
|
|
graph = defaultdict(list)
|
|
for a, b in boundary:
|
|
graph[a].append(b)
|
|
graph[b].append(a)
|
|
if not graph:
|
|
return [], []
|
|
|
|
def direction(a, b):
|
|
dx, dy = b[0] - a[0], b[1] - a[1]
|
|
magnitude = math.hypot(dx, dy)
|
|
return ((dx / magnitude, dy / magnitude), magnitude) if magnitude > 1.0e-8 else (None, 0.0)
|
|
|
|
def straight_run(origin, first):
|
|
prior, current = origin, first
|
|
initial, edge_length = direction(prior, current)
|
|
if initial is None:
|
|
return 0.0
|
|
total = edge_length
|
|
seen = {origin, first}
|
|
while current in graph and len(graph[current]) == 2:
|
|
choices = [value for value in graph[current] if value != prior]
|
|
if len(choices) != 1 or choices[0] in seen:
|
|
break
|
|
nxt = choices[0]
|
|
outgoing, edge_length = direction(current, nxt)
|
|
if outgoing is None:
|
|
break
|
|
deflection = math.degrees(math.acos(max(-1.0, min(1.0,
|
|
initial[0] * outgoing[0] + initial[1] * outgoing[1]))))
|
|
if deflection > 8.0:
|
|
break
|
|
total += edge_length
|
|
prior, current = current, nxt
|
|
initial = outgoing
|
|
seen.add(current)
|
|
return total
|
|
|
|
longest = 0.0
|
|
corners = []
|
|
for vertex, neighbours in graph.items():
|
|
if len(neighbours) == 2:
|
|
incoming, _ = direction(neighbours[0], vertex)
|
|
outgoing, _ = direction(vertex, neighbours[1])
|
|
if incoming and outgoing:
|
|
turn = math.degrees(math.acos(max(-1.0, min(1.0,
|
|
incoming[0] * outgoing[0] + incoming[1] * outgoing[1]))))
|
|
if turn >= CORNER_TURN_LIMIT_DEG:
|
|
left_run = straight_run(vertex, neighbours[0])
|
|
right_run = straight_run(vertex, neighbours[1])
|
|
if left_run >= STRAIGHT_LIMIT_M and right_run >= STRAIGHT_LIMIT_M:
|
|
corners.append(turn)
|
|
for neighbour in neighbours:
|
|
longest = max(longest, straight_run(vertex, neighbour))
|
|
return [longest], corners
|
|
|
|
|
|
def analyze_triangles(triangles, center, center_z, crop_to_center=True,
|
|
include_sides=False):
|
|
records = []
|
|
for lod, average_nz, vertices in triangles:
|
|
points = tuple(vertex[0] for vertex in vertices)
|
|
centroid = tuple(sum(point[axis] for point in points) / 3.0 for axis in range(3))
|
|
if (crop_to_center and
|
|
(abs(centroid[0] - center[0]) > MOUTH_CORRIDOR_LENGTH_M + 2.0
|
|
or abs(centroid[1] - center[1]) > MOUTH_CORRIDOR_LENGTH_M + 2.0
|
|
or centroid[2] < center_z - MOUTH_Z_BELOW_M
|
|
or centroid[2] > center_z + MOUTH_Z_ABOVE_M)):
|
|
continue
|
|
measured = face(points)
|
|
if not measured:
|
|
continue
|
|
normal, plane_d, area = measured
|
|
if not include_sides and (normal[2] < 0.35 or average_nz < 0.25):
|
|
continue
|
|
bucket = (round(normal[0] / NORMAL_BUCKET), round(normal[1] / NORMAL_BUCKET),
|
|
round(normal[2] / NORMAL_BUCKET), round(plane_d / PLANE_BUCKET_M))
|
|
records.append({"points": points, "centroid": centroid, "normal": normal,
|
|
"d": plane_d, "area": area, "bucket": bucket})
|
|
|
|
by_bucket = defaultdict(list)
|
|
for index, record in enumerate(records):
|
|
by_bucket[record["bucket"]].append(index)
|
|
edge_indices = defaultdict(list)
|
|
for index, record in enumerate(records):
|
|
points = record["points"]
|
|
for a, b in ((points[0], points[1]), (points[1], points[2]),
|
|
(points[2], points[0])):
|
|
edge_indices[edge_key(a, b)].append(index)
|
|
adjacency = defaultdict(list)
|
|
for indices in edge_indices.values():
|
|
if len(indices) == 2:
|
|
a, b = indices
|
|
# Adjacent triangles can differ by one normal bucket on a smooth mesh.
|
|
# Plane membership is checked against the seed below, so let that test
|
|
# decide whether a connected patch is planar instead of splitting it at
|
|
# the bucket boundary.
|
|
adjacency[a].append(b)
|
|
adjacency[b].append(a)
|
|
|
|
max_area = 0.0
|
|
max_straight = 0.0
|
|
corner_count = 0
|
|
max_corner_turn = 0.0
|
|
max_bounds = None
|
|
max_normal = None
|
|
visited = set()
|
|
for indices in by_bucket.values():
|
|
for seed in indices:
|
|
if seed in visited:
|
|
continue
|
|
component = []
|
|
queue = deque([seed])
|
|
queued = {seed}
|
|
seed_record = records[seed]
|
|
while queue:
|
|
index = queue.popleft()
|
|
record = records[index]
|
|
if (abs(dot(seed_record["normal"], record["normal"])) < math.cos(math.radians(4.5))
|
|
or max(abs(dot(seed_record["normal"], point) + seed_record["d"])
|
|
for point in record["points"]) > PLANE_TOLERANCE_M):
|
|
continue
|
|
visited.add(index)
|
|
component.append(index)
|
|
for neighbour in adjacency[index]:
|
|
if neighbour not in visited and neighbour not in queued:
|
|
queued.add(neighbour)
|
|
queue.append(neighbour)
|
|
area = sum(records[index]["area"] for index in component)
|
|
if area <= max_area:
|
|
continue
|
|
segments, corners = simplified_boundary(component, records)
|
|
max_area = area
|
|
max_straight = max(segments, default=0.0)
|
|
corner_count = len(corners)
|
|
max_corner_turn = max(corners, default=0.0)
|
|
points = [point for index in component for point in records[index]["points"]]
|
|
max_bounds = tuple((min(point[axis] for point in points),
|
|
max(point[axis] for point in points)) for axis in range(3))
|
|
max_normal = seed_record["normal"]
|
|
return {"triangles": len(records), "planar_area": max_area,
|
|
"straight": max_straight, "corners": corner_count,
|
|
"corner_turn_max": max_corner_turn,
|
|
"bounds": max_bounds, "normal": max_normal}
|
|
|
|
|
|
def synthetic_box(center, z):
|
|
x, y = center
|
|
corners = ((x - 1.5, y - 1.0, z), (x + 1.5, y - 1.0, z),
|
|
(x + 1.5, y + 1.0, z), (x - 1.5, y + 1.0, z))
|
|
return [(0, 1.0, tuple((point, (0.0, 0.0, 1.0)) for point in tri))
|
|
for tri in ((corners[0], corners[1], corners[2]),
|
|
(corners[0], corners[2], corners[3]))]
|
|
|
|
|
|
def fail(metrics, include_planarity=True):
|
|
planar_fail = (include_planarity and
|
|
(metrics["planar_area"] >= PLANE_LIMIT_M2
|
|
or metrics.get("all_face_planar_area", 0.0) >= PLANE_LIMIT_M2))
|
|
return (planar_fail
|
|
or metrics["straight"] > STRAIGHT_LIMIT_M
|
|
or metrics.get("all_face_straight", 0.0) > STRAIGHT_LIMIT_M
|
|
or metrics["corners"] > 0
|
|
or metrics.get("all_face_corners", 0) > 0)
|
|
|
|
|
|
def mouth_route_points(passage, side, scale):
|
|
controls = [
|
|
(tuple(value * scale for value in point["position_q"]),
|
|
point["radius_q"] * scale)
|
|
for point in passage["control_points"]
|
|
]
|
|
if side == "upper":
|
|
return controls[:4]
|
|
lower_mouth = tuple(value * scale for value in passage["lower_mouth_q"])
|
|
lower_index = min(range(len(controls)), key=lambda index: sum(
|
|
(controls[index][0][axis] - lower_mouth[axis]) ** 2 for axis in range(3)))
|
|
return list(reversed(controls[max(0, lower_index - 3):lower_index + 1]))
|
|
|
|
|
|
def select_corridor_triangles(triangles, route, half_width, path_length):
|
|
selected = []
|
|
for triangle in triangles:
|
|
vertices = triangle[2]
|
|
centroid = tuple(sum(vertex[0][axis] for vertex in vertices) / 3.0
|
|
for axis in range(3))
|
|
closest_distance = math.inf
|
|
closest_along = 0.0
|
|
closest_floor = 0.0
|
|
prefix = 0.0
|
|
for (a, radius_a), (b, radius_b) in zip(route, route[1:]):
|
|
dx, dy, dz = (b[axis] - a[axis] for axis in range(3))
|
|
horizontal_length_squared = dx * dx + dy * dy
|
|
horizontal_length = math.sqrt(horizontal_length_squared)
|
|
if horizontal_length_squared <= 1.0e-12:
|
|
t = 0.0
|
|
else:
|
|
t = max(0.0, min(1.0,
|
|
((centroid[0] - a[0]) * dx + (centroid[1] - a[1]) * dy)
|
|
/ horizontal_length_squared))
|
|
center_x, center_y = a[0] + t * dx, a[1] + t * dy
|
|
lateral = math.hypot(centroid[0] - center_x, centroid[1] - center_y)
|
|
segment_length = math.sqrt(horizontal_length_squared + dz * dz)
|
|
if lateral < closest_distance:
|
|
closest_distance = lateral
|
|
closest_along = prefix + t * segment_length
|
|
center_z = a[2] + t * dz
|
|
radius = radius_a + t * (radius_b - radius_a)
|
|
closest_floor = center_z - radius + PLAYER_FLOOR_OFFSET_M
|
|
prefix += segment_length
|
|
if (closest_distance <= half_width
|
|
and 0.0 <= closest_along <= path_length
|
|
and abs(centroid[2] - closest_floor) <= 1.5):
|
|
selected.append(triangle)
|
|
return selected
|
|
|
|
|
|
def route_floor_at(route, x, y):
|
|
closest = None
|
|
prefix = 0.0
|
|
for (a, radius_a), (b, radius_b) in zip(route, route[1:]):
|
|
dx, dy, dz = (b[axis] - a[axis] for axis in range(3))
|
|
horizontal_sq = dx * dx + dy * dy
|
|
t = max(0.0, min(1.0, ((x - a[0]) * dx + (y - a[1]) * dy)
|
|
/ horizontal_sq)) if horizontal_sq > 1.0e-12 else 0.0
|
|
center_x, center_y = a[0] + t * dx, a[1] + t * dy
|
|
lateral = math.hypot(x - center_x, y - center_y)
|
|
length = math.sqrt(horizontal_sq + dz * dz)
|
|
along = prefix + t * length
|
|
center_z = a[2] + t * dz
|
|
radius = radius_a + t * (radius_b - radius_a)
|
|
floor = center_z - radius + PLAYER_FLOOR_OFFSET_M
|
|
candidate = (lateral, along, floor)
|
|
if closest is None or lateral < closest[0]:
|
|
closest = candidate
|
|
prefix += length
|
|
if closest and closest[0] <= MOUTH_CORRIDOR_HALF_WIDTH_M \
|
|
and closest[1] <= MOUTH_CORRIDOR_LENGTH_M:
|
|
return closest[2]
|
|
return None
|
|
|
|
|
|
def differential_surface(candidate_triangles, baseline_triangles, route, bounds):
|
|
cell_size = 0.5
|
|
candidate_buckets = PROFILE.build_buckets(candidate_triangles, True, bounds, cell_size)
|
|
baseline_buckets = PROFILE.build_buckets(baseline_triangles, True, bounds, cell_size)
|
|
route_x = [point[0][0] for point in route]
|
|
route_y = [point[0][1] for point in route]
|
|
padding = MOUTH_CORRIDOR_HALF_WIDTH_M + DELTA_GRID_M
|
|
x0 = math.floor((min(route_x) - padding) / DELTA_GRID_M) * DELTA_GRID_M
|
|
x1 = math.ceil((max(route_x) + padding) / DELTA_GRID_M) * DELTA_GRID_M
|
|
y0 = math.floor((min(route_y) - padding) / DELTA_GRID_M) * DELTA_GRID_M
|
|
y1 = math.ceil((max(route_y) + padding) / DELTA_GRID_M) * DELTA_GRID_M
|
|
nx = round((x1 - x0) / DELTA_GRID_M)
|
|
ny = round((y1 - y0) / DELTA_GRID_M)
|
|
samples = {}
|
|
for iy in range(ny + 1):
|
|
y = y0 + iy * DELTA_GRID_M
|
|
for ix in range(nx + 1):
|
|
x = x0 + ix * DELTA_GRID_M
|
|
floor = route_floor_at(route, x, y)
|
|
if floor is None:
|
|
continue
|
|
candidate = PROFILE.sample_surface(candidate_buckets, x, y, True,
|
|
cell_size, preferred_height=floor)
|
|
baseline = PROFILE.sample_surface(baseline_buckets, x, y, True,
|
|
cell_size, preferred_height=floor)
|
|
samples[(ix, iy)] = (candidate, baseline)
|
|
|
|
delta_triangles = []
|
|
changed_cells = 0
|
|
changed_area = 0.0
|
|
deltas = []
|
|
for iy in range(ny):
|
|
for ix in range(nx):
|
|
keys = ((ix, iy), (ix + 1, iy), (ix + 1, iy + 1), (ix, iy + 1))
|
|
values = [samples.get(key) for key in keys]
|
|
if any(value is None or value[0] is None for value in values):
|
|
continue
|
|
cell_deltas = []
|
|
changed = False
|
|
for candidate, baseline in values:
|
|
if baseline is None:
|
|
changed = True
|
|
cell_deltas.append(math.inf)
|
|
else:
|
|
delta = candidate[0] - baseline[0]
|
|
cell_deltas.append(abs(delta))
|
|
deltas.append(abs(delta))
|
|
changed |= abs(delta) >= DELTA_THRESHOLD_M
|
|
if not changed:
|
|
continue
|
|
changed_cells += 1
|
|
changed_area += DELTA_GRID_M * DELTA_GRID_M
|
|
points = []
|
|
for key, (candidate, _) in zip(keys, values):
|
|
x = x0 + key[0] * DELTA_GRID_M
|
|
y = y0 + key[1] * DELTA_GRID_M
|
|
points.append(((x, y, candidate[0]), candidate[1]))
|
|
for triangle_indices in ((0, 1, 2), (0, 2, 3)):
|
|
vertices = tuple(points[index] for index in triangle_indices)
|
|
avg_nz = sum(vertex[1][2] for vertex in vertices) / 3.0
|
|
delta_triangles.append((0, avg_nz, vertices))
|
|
metrics = analyze_triangles(delta_triangles, (0.0, 0.0), 0.0,
|
|
crop_to_center=False)
|
|
metrics["changed_cells"] = changed_cells
|
|
metrics["changed_area"] = changed_area
|
|
metrics["max_delta"] = max(deltas, default=0.0)
|
|
metrics["mean_abs_delta"] = sum(deltas) / len(deltas) if deltas else 0.0
|
|
return metrics
|
|
|
|
|
|
def audit_scene(scene, out, baseline_out, mesh_override=None):
|
|
mesh_name, layout_name, target_generator, side = SCENES[scene]
|
|
import json
|
|
layout = json.loads((out / layout_name).read_text(encoding="utf-8"))
|
|
scale = layout["core_voxel_size_um"] / (256.0 * 1_000_000.0)
|
|
strata = {int(value["strate_index"]): value for value in layout["strates"]}
|
|
choices = []
|
|
for passage in layout["passages"]:
|
|
side_index = int(passage[side + "_strate_index"])
|
|
if strata[side_index]["generator_kind"] == target_generator:
|
|
choices.append(passage)
|
|
if not choices:
|
|
raise RuntimeError(f"no {target_generator} {side} mouth in {scene}")
|
|
passage = min(choices, key=lambda value: value["stable_id"])
|
|
mouth = tuple(value * scale for value in passage[f"{side}_mouth_q"])
|
|
bounds = (mouth[0] - MOUTH_CORRIDOR_LENGTH_M - MOUTH_CORRIDOR_HALF_WIDTH_M - 1.0,
|
|
mouth[0] + MOUTH_CORRIDOR_LENGTH_M + MOUTH_CORRIDOR_HALF_WIDTH_M + 1.0,
|
|
mouth[1] - MOUTH_CORRIDOR_LENGTH_M - MOUTH_CORRIDOR_HALF_WIDTH_M - 1.0,
|
|
mouth[1] + MOUTH_CORRIDOR_LENGTH_M + MOUTH_CORRIDOR_HALF_WIDTH_M + 1.0)
|
|
voxel_um, triangles, tiles = PROFILE.read_scene(
|
|
str(mesh_override or out / mesh_name), 0,
|
|
mouth[2] - MOUTH_Z_BELOW_M, mouth[2] + MOUTH_Z_ABOVE_M, bounds)
|
|
baseline_um, baseline_triangles, baseline_tiles = PROFILE.read_scene(
|
|
str(baseline_out / mesh_name), 0, mouth[2] - MOUTH_Z_BELOW_M,
|
|
mouth[2] + MOUTH_Z_ABOVE_M, bounds)
|
|
if voxel_um != baseline_um:
|
|
raise RuntimeError(f"{scene} candidate/control voxel size differs")
|
|
route = mouth_route_points(passage, side, scale)
|
|
corridor = select_corridor_triangles(triangles, route,
|
|
MOUTH_CORRIDOR_HALF_WIDTH_M, MOUTH_CORRIDOR_LENGTH_M)
|
|
baseline_corridor = select_corridor_triangles(baseline_triangles, route,
|
|
MOUTH_CORRIDOR_HALF_WIDTH_M, MOUTH_CORRIDOR_LENGTH_M)
|
|
differential = differential_surface(corridor, baseline_corridor, route, bounds)
|
|
metrics = analyze_triangles(corridor, mouth[:2], mouth[2], crop_to_center=False)
|
|
baseline_metrics = analyze_triangles(baseline_corridor, mouth[:2], mouth[2],
|
|
crop_to_center=False)
|
|
full_surface = analyze_triangles(corridor, mouth[:2], mouth[2],
|
|
crop_to_center=False, include_sides=True)
|
|
proxy_alert = fail(differential, include_planarity=False)
|
|
corner_gate = (metrics["corners"] == 0 and full_surface["corners"] == 0
|
|
and differential["corners"] == 0)
|
|
metrics["all_face_planar_area"] = full_surface["planar_area"]
|
|
metrics["all_face_straight"] = full_surface["straight"]
|
|
metrics["all_face_corners"] = full_surface["corners"]
|
|
metrics["all_face_corner_turn_max"] = full_surface["corner_turn_max"]
|
|
print(f"[MouthBuiltShape] scene={scene} mesh={mesh_name} mouth_m=({mouth[0]:.3f},"
|
|
f"{mouth[1]:.3f},{mouth[2]:.3f}) voxel_um={voxel_um} tiles={tiles} "
|
|
f"route_corridor_m=({MOUTH_CORRIDOR_HALF_WIDTH_M:.2f}m_halfwidth,"
|
|
f"{MOUTH_CORRIDOR_LENGTH_M:.1f}m_length) corridor_triangles={len(corridor)} "
|
|
f"surface_triangles={metrics['triangles']} 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"all_face_planar_patch_m2={full_surface['planar_area']:.4f} "
|
|
f"all_face_straight_run_m={full_surface['straight']:.4f} "
|
|
f"all_face_corners={full_surface['corners']} "
|
|
f"all_face_corner_turn_max_deg={full_surface['corner_turn_max']:.2f} "
|
|
f"matched_no_support_planar_patch_m2={baseline_metrics['planar_area']:.4f} "
|
|
f"matched_no_support_straight_run_m={baseline_metrics['straight']:.4f} "
|
|
f"matched_no_support_corners={baseline_metrics['corners']} "
|
|
f"control_tiles={baseline_tiles} changed_cells={differential['changed_cells']} "
|
|
f"changed_area_m2={differential['changed_area']:.4f} "
|
|
f"max_abs_delta_m={differential['max_delta']:.4f} "
|
|
f"mean_abs_delta_m={differential['mean_abs_delta']:.4f} "
|
|
f"added_planar_patch_m2={differential['planar_area']:.4f} "
|
|
f"added_straight_run_m={differential['straight']:.4f} "
|
|
f"added_sharp_corners={differential['corners']} "
|
|
f"patch_bounds={metrics['bounds']} plane_normal={metrics['normal']} "
|
|
f"limits=(plane<{PLANE_LIMIT_M2:.2f}m2 informational,line<={STRAIGHT_LIMIT_M:.2f}m,"
|
|
f"corner_turn<{CORNER_TURN_LIMIT_DEG:.0f}deg) "
|
|
f"straight_or_planar_proxy={'ALERT' if proxy_alert else 'PASS'} "
|
|
f"delta_threshold_m={DELTA_THRESHOLD_M:.2f} status="
|
|
f"{'PASS' if corner_gate else 'FAIL'}")
|
|
metrics["differential"] = differential
|
|
metrics["matched_no_support"] = baseline_metrics
|
|
metrics["corner_gate"] = corner_gate
|
|
metrics["proxy_alert"] = proxy_alert
|
|
return metrics
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--out", type=Path, default=ROOT / "Saved/Linux/l37")
|
|
parser.add_argument("--baseline-out", type=Path,
|
|
default=BASELINES / "mouth/l37-no-tread")
|
|
parser.add_argument("--control-out", type=Path, default=BASELINES / "mouth/l36")
|
|
parser.add_argument("--scene", choices=tuple(SCENES) + ("all",), default="all")
|
|
args = parser.parse_args()
|
|
out = args.out if args.out.is_absolute() else ROOT / args.out
|
|
baseline_out = (args.baseline_out if args.baseline_out.is_absolute()
|
|
else ROOT / args.baseline_out)
|
|
control_out = (args.control_out if args.control_out.is_absolute()
|
|
else ROOT / args.control_out)
|
|
scenes = tuple(SCENES) if args.scene == "all" else (args.scene,)
|
|
measured = {scene: audit_scene(scene, out, baseline_out) for scene in scenes}
|
|
l36_fi = audit_scene("owner-fi", control_out, baseline_out)
|
|
l36_failed = fail(l36_fi["differential"])
|
|
print(f"[MouthBuiltShapeL36Control] scene=owner-fi planar_patch_m2="
|
|
f"{l36_fi['differential']['planar_area']:.4f} "
|
|
f"straight_run_m={l36_fi['differential']['straight']:.4f} "
|
|
f"sharp_corners={l36_fi['differential']['corners']} detected={l36_failed} "
|
|
f"negative_control={'PASS' if l36_failed else 'FAIL'}")
|
|
control = analyze_triangles(synthetic_box((0.0, 0.0), 0.0), (0.0, 0.0), 0.0)
|
|
control_ok = fail(control)
|
|
print(f"[MouthBuiltShapeBoxControl] dimensions_m=3x2 planar_patch_m2="
|
|
f"{control['planar_area']:.4f} straight_run_m={control['straight']:.4f} "
|
|
f"sharp_corners={control['corners']} detected={control_ok} "
|
|
f"negative_control={'PASS' if control_ok else 'FAIL'}")
|
|
candidates_pass = all(metrics["corner_gate"] for metrics in measured.values())
|
|
passed = control_ok and l36_failed and candidates_pass
|
|
print(f"[MouthBuiltShapeSummary] scenes={len(measured)} candidates_pass="
|
|
f"{sum(metrics['corner_gate'] for metrics in measured.values())}/{len(measured)} "
|
|
f"L36_control_fail={'PASS' if l36_failed else 'FAIL'} "
|
|
f"box_control_fail={'PASS' if control_ok else 'FAIL'} "
|
|
f"corner_gate={'PASS' if candidates_pass else 'FAIL'} "
|
|
f"straight_or_planar_proxy_alerts={sum(metrics['proxy_alert'] for metrics in measured.values())} "
|
|
f"planar_gate=informational_owner_accepted "
|
|
f"status={'PASS' if passed else 'FAIL'}")
|
|
raise SystemExit(0 if passed else 1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|