Mouth edges matched passage position, radius, tangent (dot 0.99999) and floor slope, but the cubic departure edge still curls ~1 m back alongside the passage within 5 m; shell floor violations remain; composed sweeps 0/16. Sixth mouth-join round (L144-L149): stop and take the design question to the owner. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
170 lines
7.9 KiB
Python
170 lines
7.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Structural mouth continuity and forward-extension checks for L149 layouts."""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import math
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
def _q_to_um(value: int, core_voxel_size_um: int) -> int:
|
|
return int(value * core_voxel_size_um / 256)
|
|
|
|
|
|
def _mouth(layout: dict[str, Any], edge: dict[str, Any]) -> tuple[dict[str, Any], int,
|
|
list[int], int] | None:
|
|
anchor = edge.get("start_anchor_stable_id", "")
|
|
if not anchor.endswith(("/lower-mouth", "/upper-mouth")):
|
|
return None
|
|
side = anchor.rsplit("/", 1)[1]
|
|
passage_id = anchor.rsplit("/", 1)[0]
|
|
passage = next((row for row in layout.get("passages", [])
|
|
if row.get("stable_id") == passage_id), None)
|
|
if passage is None:
|
|
return None
|
|
key = "lower_mouth_q" if side == "lower-mouth" else "upper_mouth_q"
|
|
mouth_q = passage.get(key)
|
|
controls = passage.get("control_points", [])
|
|
if not mouth_q or not controls:
|
|
return None
|
|
if side == "upper-mouth":
|
|
index = 0
|
|
else:
|
|
center_height_q = round(1.13 * 256_000_000 / layout["core_voxel_size_um"])
|
|
matches = [i for i, point in enumerate(controls)
|
|
if point["position_q"][0] == mouth_q[0]
|
|
and point["position_q"][1] == mouth_q[1]
|
|
and point["position_q"][2] - point["radius_q"] + center_height_q
|
|
== mouth_q[2]]
|
|
if not matches:
|
|
return None
|
|
index = matches[-1]
|
|
return passage, index, mouth_q, _q_to_um(controls[index]["radius_q"],
|
|
layout["core_voxel_size_um"])
|
|
|
|
|
|
def _dot_direction(a: list[float], b: list[float]) -> float:
|
|
al, bl = math.sqrt(sum(x * x for x in a)), math.sqrt(sum(x * x for x in b))
|
|
return sum(x * y for x, y in zip(a, b)) / (al * bl) if al > 0 and bl > 0 else -1.0
|
|
|
|
|
|
def _bezier(points: list[list[int]], t: float) -> list[float]:
|
|
u = 1.0 - t
|
|
return [u**3 * points[0][axis] + 3*u*u*t * points[1][axis]
|
|
+ 3*u*t*t * points[2][axis] + t**3 * points[3][axis]
|
|
for axis in range(3)]
|
|
|
|
|
|
def _metrics(layout: dict[str, Any], edge: dict[str, Any]) -> dict[str, Any]:
|
|
found = _mouth(layout, edge)
|
|
if found is None:
|
|
return {"bound": False, "tangent_dot": -1.0, "radius_match": False,
|
|
"position_match": False, "floor_slope_delta": math.inf,
|
|
"forward_extension": False}
|
|
passage, index, _, expected_radius = found
|
|
controls = passage["control_points"]
|
|
route = edge.get("control_points_um", [])
|
|
if len(route) != 4 or len(controls) < 2:
|
|
return {"bound": False, "tangent_dot": -1.0, "radius_match": False,
|
|
"position_match": False, "floor_slope_delta": math.inf,
|
|
"forward_extension": False}
|
|
side = edge["start_anchor_stable_id"].rsplit("/", 1)[1]
|
|
neighbor = 1 if side == "upper-mouth" else index - 1
|
|
if neighbor < 0 or neighbor >= len(controls):
|
|
return {"bound": False, "tangent_dot": -1.0, "radius_match": False,
|
|
"position_match": False, "floor_slope_delta": math.inf,
|
|
"forward_extension": False}
|
|
mouth_point = controls[index]
|
|
neighbor_point = controls[neighbor]
|
|
core = layout["core_voxel_size_um"]
|
|
expected = [_q_to_um(v, core) for v in mouth_point["position_q"]]
|
|
expected[2] -= expected_radius
|
|
expected_dir = [_q_to_um(mouth_point["position_q"][axis]
|
|
- neighbor_point["position_q"][axis], core)
|
|
for axis in range(2)]
|
|
edge_dir = [route[1][0] - route[0][0], route[1][1] - route[0][1]]
|
|
position_match = all(abs(route[0][axis] - expected[axis]) <= 2 for axis in range(3))
|
|
radius_match = edge.get("bore_radius_um") == expected_radius
|
|
tangent_dot = _dot_direction([*edge_dir, 0.0], [*expected_dir, 0.0])
|
|
|
|
floor_mouth = mouth_point["position_q"][2] - mouth_point["radius_q"]
|
|
floor_neighbor = neighbor_point["position_q"][2] - neighbor_point["radius_q"]
|
|
expected_slope = _q_to_um(floor_mouth - floor_neighbor, core) / max(
|
|
1.0, math.hypot(*expected_dir))
|
|
if side == "upper-mouth":
|
|
expected_slope = -expected_slope
|
|
route_slope = (route[1][2] - route[0][2]) / max(1.0, math.hypot(*edge_dir))
|
|
floor_slope_delta = abs(route_slope - expected_slope)
|
|
|
|
tangent_length = math.hypot(*expected_dir)
|
|
tangent = [v / tangent_length for v in expected_dir] if tangent_length else [0.0, 0.0]
|
|
points = [list(p) for p in route]
|
|
previous = _bezier(points, 0.0)
|
|
distance = 0.0
|
|
prior_progress = 0.0
|
|
forward = True
|
|
for step in range(1, 1025):
|
|
current = _bezier(points, step / 1024.0)
|
|
dx, dy = current[0] - previous[0], current[1] - previous[1]
|
|
segment = math.hypot(dx, dy)
|
|
next_distance = distance + segment
|
|
if distance < 12_000_000:
|
|
progress = ((current[0] - route[0][0]) * tangent[0]
|
|
+ (current[1] - route[0][1]) * tangent[1])
|
|
if progress < -100_000 or progress < prior_progress - 100_000:
|
|
forward = False
|
|
prior_progress = progress
|
|
previous, distance = current, next_distance
|
|
if distance >= 12_000_000:
|
|
break
|
|
return {"bound": True, "tangent_dot": tangent_dot,
|
|
"radius_match": radius_match, "position_match": position_match,
|
|
"floor_slope_delta": floor_slope_delta, "forward_extension": forward}
|
|
|
|
|
|
def audit_layout(layout_path: Path) -> dict[str, Any]:
|
|
layout = json.loads(layout_path.read_text(encoding="utf-8"))
|
|
mouth_edges = [edge for edge in layout.get("cavern_edges", [])
|
|
if edge.get("strate_index") in (2, 3)
|
|
and edge.get("stable_id", "").endswith(("route-edge-0", "departure-edge"))
|
|
and edge.get("start_anchor_stable_id", "").endswith(
|
|
("/lower-mouth", "/upper-mouth"))]
|
|
rows = [_metrics(layout, edge) for edge in mouth_edges]
|
|
negative_controls = 0
|
|
for edge in mouth_edges:
|
|
found = _mouth(layout, edge)
|
|
if found is None:
|
|
continue
|
|
passage, index, _, _ = found
|
|
side = edge["start_anchor_stable_id"].rsplit("/", 1)[1]
|
|
neighbor = 1 if side == "upper-mouth" else index - 1
|
|
mouth_point = passage["control_points"][index]
|
|
neighbor_point = passage["control_points"][neighbor]
|
|
core = layout["core_voxel_size_um"]
|
|
expected_dir = [_q_to_um(mouth_point["position_q"][axis]
|
|
- neighbor_point["position_q"][axis], core)
|
|
for axis in range(2)]
|
|
altered = dict(edge)
|
|
points = [list(p) for p in edge["control_points_um"]]
|
|
length = max(1.0, math.hypot(*expected_dir))
|
|
points[1][0] = points[0][0] - round(expected_dir[0] / length * 1_000_000)
|
|
points[1][1] = points[0][1] - round(expected_dir[1] / length * 1_000_000)
|
|
altered["control_points_um"] = points
|
|
control = _metrics(layout, altered)
|
|
if control["tangent_dot"] < 0.9:
|
|
negative_controls += 1
|
|
continuity = bool(rows) and all(
|
|
row["bound"] and row["position_match"] and row["radius_match"]
|
|
and row["tangent_dot"] >= 0.9 and row["floor_slope_delta"] <= 0.15
|
|
for row in rows)
|
|
no_backtrack = bool(rows) and all(row["forward_extension"] for row in rows)
|
|
controls_ok = bool(mouth_edges) and negative_controls == len(mouth_edges)
|
|
return {"edges": len(mouth_edges), "continuity": continuity,
|
|
"no_backtrack_12m": no_backtrack, "negative_controls": negative_controls,
|
|
"negative_controls_expected": len(mouth_edges), "negative_controls_pass": controls_ok,
|
|
"minimum_tangent_dot": min((row["tangent_dot"] for row in rows), default=-1.0),
|
|
"maximum_floor_slope_delta": max(
|
|
(row["floor_slope_delta"] for row in rows), default=math.inf),
|
|
"rows": rows}
|