The passage mouth fade multiplied the signed carve distance by zero, which turned native rock into an open surface (the reason earlier rounds stamped walk pads at mouths); it now blends between the uncarved field and the full signed carve. The FI style-0 route followed a lifted grade envelope over the crater (the "bridge"); it now runs on the rim and follows the native surface. New built-shape and crease audits with thresholds frozen before the fix (L36 and box controls fail). 24/24 passage rows, 30/30 determinism, 16/16 invariants, 18/18 interface/ring. Unreal: one recipe setter (uncompiled). Research R1 (read-only, web): ranked proposals for natural walkable mouths, floor-limited carve first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
177 lines
7.6 KiB
Python
177 lines
7.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Compare the FI fade-ring crease against L35 using the same surface samples."""
|
|
import argparse
|
|
import importlib.util
|
|
import math
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[4]
|
|
RING_PATH = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py"
|
|
SPEC = importlib.util.spec_from_file_location("ring_profile_l32", RING_PATH)
|
|
RING = importlib.util.module_from_spec(SPEC)
|
|
SPEC.loader.exec_module(RING)
|
|
|
|
CENTER = (6.298233, 1.137233)
|
|
INNER, OUTER, STEP = 5.5, 9.5, 0.125
|
|
HEIGHT_MIN, HEIGHT_MAX, PREFERRED = -80.0, -40.0, -56.0
|
|
# Chosen before the L37 mouth edit: a 20-degree normal jump over one 0.125 m
|
|
# sample interval is visible at the standard camera distance.
|
|
NORMAL_LIMIT_DEG = 20.0
|
|
COVERAGE_MIN = 0.90
|
|
FRAGMENT_MAX_TRIANGLES = 64
|
|
FRAGMENT_MIN_AREA_M2 = 0.0001
|
|
FRAGMENT_MAX_AREA_M2 = 0.05
|
|
FRAGMENT_MAX_DISTANCE_M = 12.0
|
|
|
|
|
|
def small_components(triangles):
|
|
parent = list(range(len(triangles)))
|
|
sizes = [1] * len(triangles)
|
|
owners = {}
|
|
|
|
def find(index):
|
|
while parent[index] != index:
|
|
parent[index] = parent[parent[index]]
|
|
index = parent[index]
|
|
return index
|
|
|
|
def union(a, b):
|
|
a, b = find(a), find(b)
|
|
if a == b:
|
|
return
|
|
if sizes[a] < sizes[b]:
|
|
a, b = b, a
|
|
parent[b] = a
|
|
sizes[a] += sizes[b]
|
|
|
|
for index, (_, _, vertices) in enumerate(triangles):
|
|
for point, _ in vertices:
|
|
key = tuple(round(value, 5) for value in point)
|
|
other = owners.setdefault(key, index)
|
|
union(index, other)
|
|
|
|
components = {}
|
|
for index, (_, _, vertices) in enumerate(triangles):
|
|
root = find(index)
|
|
item = components.setdefault(root, {"count": 0, "area": 0.0, "sum": [0.0] * 3})
|
|
item["count"] += 1
|
|
points = [vertex[0] for vertex in vertices]
|
|
for point in points:
|
|
for axis in range(3):
|
|
item["sum"][axis] += point[axis]
|
|
ab = tuple(points[1][axis] - points[0][axis] for axis in range(3))
|
|
ac = tuple(points[2][axis] - points[0][axis] for axis in range(3))
|
|
cross = (ab[1] * ac[2] - ab[2] * ac[1],
|
|
ab[2] * ac[0] - ab[0] * ac[2],
|
|
ab[0] * ac[1] - ab[1] * ac[0])
|
|
item["area"] += 0.5 * math.sqrt(sum(value * value for value in cross))
|
|
|
|
cx, cy = CENTER
|
|
small = []
|
|
below_floor = []
|
|
for item in components.values():
|
|
centroid = tuple(value / (3 * item["count"]) for value in item["sum"])
|
|
distance = math.hypot(centroid[0] - cx, centroid[1] - cy)
|
|
if (item["count"] <= FRAGMENT_MAX_TRIANGLES
|
|
and item["area"] <= FRAGMENT_MAX_AREA_M2
|
|
and distance <= FRAGMENT_MAX_DISTANCE_M):
|
|
record = (item["count"], item["area"], distance, centroid)
|
|
(below_floor if item["area"] < FRAGMENT_MIN_AREA_M2 else small).append(record)
|
|
return len(components), small, below_floor
|
|
|
|
|
|
def audit(path):
|
|
cx, cy = CENTER
|
|
bounds = (cx - OUTER, cx + OUTER, cy - OUTER, cy + OUTER)
|
|
voxel_um, triangles, tiles = RING.read_scene(
|
|
str(path), 0, HEIGHT_MIN, HEIGHT_MAX, bounds)
|
|
component_count, fragments, below_floor = small_components(triangles)
|
|
print(f"[MouthFragmentComponents] mesh={path.name} components={component_count} "
|
|
f"small_near_mouth={len(fragments)} subthreshold_near_mouth={len(below_floor)} "
|
|
f"limits=(triangles<={FRAGMENT_MAX_TRIANGLES},"
|
|
f"area={FRAGMENT_MIN_AREA_M2:.4f}..{FRAGMENT_MAX_AREA_M2:.3f}m2,"
|
|
f"distance<={FRAGMENT_MAX_DISTANCE_M:.1f}m) "
|
|
f"first={fragments[0] if fragments else None} "
|
|
f"subthreshold={below_floor[0] if below_floor else None} "
|
|
f"status={'PASS' if not fragments else 'FAIL'}")
|
|
cell = max(2.0, 16.0 * voxel_um / 1_000_000.0)
|
|
buckets = RING.build_buckets(triangles, True, bounds, cell)
|
|
lines = []
|
|
count = round((OUTER - INNER) / STEP)
|
|
for degrees in (135.0, 180.0, 225.0, 270.0, 315.0):
|
|
angle = math.radians(degrees)
|
|
lines.append((f"radial-{degrees:.0f}", [
|
|
(cx + radius * math.cos(angle), cy + radius * math.sin(angle))
|
|
for radius in (INNER + i * STEP for i in range(count + 1))]))
|
|
tangent_count = round(8.0 / STEP)
|
|
for degrees in (225.0, 315.0):
|
|
angle = math.radians(degrees)
|
|
normal = (math.cos(angle), math.sin(angle))
|
|
tangent = (-normal[1], normal[0])
|
|
lines.append((f"tangent-{degrees:.0f}", [
|
|
(cx + 6.5 * normal[0] + (-4.0 + i * STEP) * tangent[0],
|
|
cy + 6.5 * normal[1] + (-4.0 + i * STEP) * tangent[1])
|
|
for i in range(tangent_count + 1)]))
|
|
|
|
max_angle = 0.0
|
|
max_pair = None
|
|
min_coverage = 1.0
|
|
for name, points in lines:
|
|
samples = [RING.sample_surface(buckets, x, y, True, cell, PREFERRED)
|
|
for x, y in points]
|
|
coverage = sum(value is not None for value in samples) / len(samples)
|
|
min_coverage = min(min_coverage, coverage)
|
|
line_max = 0.0
|
|
for index in range(1, len(samples)):
|
|
prior, current = samples[index - 1], samples[index]
|
|
if prior is None or current is None:
|
|
continue
|
|
dot = max(-1.0, min(1.0, sum(a * b for a, b in zip(prior[1], current[1]))))
|
|
delta = math.degrees(math.acos(dot))
|
|
if delta > line_max:
|
|
line_max = delta
|
|
if delta > max_angle:
|
|
max_angle = delta
|
|
max_pair = (name, points[index - 1], prior[0], points[index], current[0])
|
|
print(f"[MouthFadeLine] mesh={path.name} line={name} coverage={coverage:.3f} "
|
|
f"max_normal_jump_deg={line_max:.6f}")
|
|
normal_pass = max_angle <= NORMAL_LIMIT_DEG and min_coverage >= COVERAGE_MIN
|
|
passed = normal_pass and not fragments
|
|
print(f"[MouthFadeAudit] mesh={path.name} tiles={tiles} lines={len(lines)} "
|
|
f"coverage_min={min_coverage:.3f} coverage_limit={COVERAGE_MIN:.2f} "
|
|
f"max_normal_jump_deg={max_angle:.6f} normal_limit_deg={NORMAL_LIMIT_DEG:.1f} "
|
|
f"fragments={len(fragments)} "
|
|
f"worst_pair={max_pair} status={'PASS' if passed else 'FAIL'}")
|
|
return passed, max_angle
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--control", type=Path,
|
|
default=ROOT / "Saved/Linux/l35/owner-fi-ground-mouth.vfmesh")
|
|
parser.add_argument("--prior", type=Path,
|
|
default=ROOT / "Saved/Linux/l36/owner-fi-ground-mouth.vfmesh")
|
|
parser.add_argument("--current", type=Path,
|
|
default=ROOT / "Saved/Linux/l37/owner-fi-ground-mouth.vfmesh")
|
|
args = parser.parse_args()
|
|
control = args.control if args.control.is_absolute() else ROOT / args.control
|
|
prior = args.prior if args.prior.is_absolute() else ROOT / args.prior
|
|
current = args.current if args.current.is_absolute() else ROOT / args.current
|
|
control_ok, control_max = audit(control)
|
|
prior_ok, prior_max = audit(prior)
|
|
current_ok, current_max = audit(current)
|
|
controls = (not control_ok and not prior_ok
|
|
and control_max > NORMAL_LIMIT_DEG and prior_max > NORMAL_LIMIT_DEG)
|
|
expected = controls and current_ok and current_max <= NORMAL_LIMIT_DEG
|
|
print(f"[MouthFadeNegativeControl] L35_max_deg={control_max:.6f} "
|
|
f"L35_detected={not control_ok} L36_max_deg={prior_max:.6f} "
|
|
f"L36_detected={not prior_ok} L37_max_deg={current_max:.6f} "
|
|
f"normal_limit_deg={NORMAL_LIMIT_DEG:.1f} current_pass={current_ok} "
|
|
f"status={'PASS' if expected else 'FAIL'}")
|
|
raise SystemExit(0 if expected else 1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|