feat(core): native lattice generator, slice 1 -- helix-ramp spine + small maze (L58); research R27
Core.Lattice.PrimitiveField v1, opt-in per strate (legacy Maze/VerticalShaft stay legacy): resolves the real arrival and departure mouths first, plans a mandatory spine, then maze cells and two dead ends (9 rooms, 9 edges, stable IDs). The spine descends 20 m through four helix turns (6 m radius, 3 m clear width, 5 m pitch, 2.6 m clearance; max grade 10.0 deg); each feature is one filleted void (0.57 m measured). Final-field reachability passes at 883 stations; an orphaned shaft control is rejected; exact intervals (0 misses) and a whole-surface audit with 0 flags. Whole view 1.69 s (above the 1 s target). fast/medium/full PASS. Research R27 (self-checked): static stream-to-pool water design. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
No files matched your search
@@ -0,0 +1,249 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run L54's triangle audit over L58's explicit lattice polylines and controls."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import importlib.util
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
AUDIT54 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux54/AuditWholeSurfaceL54.py"
|
||||
spec = importlib.util.spec_from_file_location("whole_surface_l54", AUDIT54)
|
||||
audit54 = importlib.util.module_from_spec(spec)
|
||||
assert spec and spec.loader
|
||||
spec.loader.exec_module(audit54)
|
||||
original_make_route = audit54.make_route
|
||||
|
||||
|
||||
def build_segment_bvh(segments: list[tuple]) -> dict:
|
||||
def build(indices: list[int]) -> dict:
|
||||
minimum = tuple(min(min(segments[index][0][axis], segments[index][1][axis])
|
||||
for index in indices) for axis in range(3))
|
||||
maximum = tuple(max(max(segments[index][0][axis], segments[index][1][axis])
|
||||
for index in indices) for axis in range(3))
|
||||
if len(indices) <= 8:
|
||||
return {"minimum": minimum, "maximum": maximum,
|
||||
"segments": tuple(sorted(indices)), "left": None, "right": None}
|
||||
axis = max(range(3), key=lambda value: maximum[value] - minimum[value])
|
||||
indices.sort(key=lambda index: (segments[index][0][axis]
|
||||
+ segments[index][1][axis]) * 0.5)
|
||||
middle = len(indices) // 2
|
||||
return {"minimum": minimum, "maximum": maximum, "segments": None,
|
||||
"left": build(indices[:middle]), "right": build(indices[middle:])}
|
||||
|
||||
if not segments:
|
||||
raise ValueError("lattice audit route has no measurable segments")
|
||||
return build(list(range(len(segments))))
|
||||
|
||||
|
||||
def make_lattice_route(layout: dict, phase_m: float = 0.0) -> dict:
|
||||
edges = sorted((edge for edge in layout.get("cavern_edges", [])
|
||||
if edge["stable_id"] in ("lattice/spine/00-arrival-portal-v1",
|
||||
"lattice/spine/01-helix-ramp-v1", "lattice/spine/99-departure-portal-v1")
|
||||
or edge["stable_id"].startswith("lattice/spine/maze-edge-")),
|
||||
key=lambda edge: (0 if edge["stable_id"].startswith("lattice/spine/00-") else
|
||||
1 if edge["stable_id"].startswith("lattice/spine/01-") else
|
||||
2 if edge["stable_id"].startswith("lattice/spine/maze-edge-") else 3,
|
||||
edge["stable_id"]))
|
||||
if not edges:
|
||||
raise ValueError("lattice audit needs its resolved mandatory spine edges")
|
||||
segments = []
|
||||
routes = []
|
||||
for edge_index, edge in enumerate(edges):
|
||||
points = [[value / 1_000_000.0 for value in row]
|
||||
for row in edge.get("polyline_um", [])]
|
||||
if len(points) < 2:
|
||||
raise ValueError(f"{edge['stable_id']}: explicit path is missing")
|
||||
radius = edge["bore_radius_um"] / 1_000_000.0
|
||||
height = edge["bore_height_um"] / 1_000_000.0
|
||||
horizontal_s = [0.0]
|
||||
for a, b in zip(points, points[1:]):
|
||||
horizontal_s.append(horizontal_s[-1] + ((b[0] - a[0]) ** 2
|
||||
+ (b[1] - a[1]) ** 2) ** 0.5)
|
||||
first = points[1]
|
||||
first_dx, first_dy = first[0] - points[0][0], first[1] - points[0][1]
|
||||
first_length = max(1.0e-9, (first_dx * first_dx + first_dy * first_dy) ** 0.5)
|
||||
extension = (points[0][0] - first_dx / first_length * audit54.OPEN_GROUND_EXTENSION_M,
|
||||
points[0][1] - first_dy / first_length * audit54.OPEN_GROUND_EXTENSION_M,
|
||||
points[0][2] - (first[2] - points[0][2]) / first_length
|
||||
* audit54.OPEN_GROUND_EXTENSION_M)
|
||||
route_points = [extension, *points]
|
||||
route_s = [-audit54.OPEN_GROUND_EXTENSION_M, *horizontal_s]
|
||||
for index, (a, b) in enumerate(zip(route_points, route_points[1:])):
|
||||
segments.append((a, b, route_s[index], route_s[index + 1],
|
||||
radius, height, edge_index))
|
||||
routes.append({"edge": edge, "points": points, "radius": radius,
|
||||
"height": height, "length": horizontal_s[-1], "index": edge_index})
|
||||
rooms = [{"room": room,
|
||||
"center": tuple(value / 1_000_000.0 for value in room["center_um"]),
|
||||
"half": tuple(value / 1_000_000.0 for value in room["half_extents_um"]),
|
||||
"floor": room["floor_um"] / 1_000_000.0}
|
||||
for room in sorted(layout.get("cavern_rooms", []), key=lambda row: row["stable_id"])]
|
||||
return {"segments": segments, "edges": routes, "rooms": rooms, "phase_m": phase_m,
|
||||
"segment_bvh": build_segment_bvh(segments)}
|
||||
|
||||
|
||||
def make_route(layout: dict, phase_m: float = 0.0) -> dict:
|
||||
if any(edge.get("stable_id", "").startswith("lattice/")
|
||||
for edge in layout.get("cavern_edges", [])):
|
||||
return make_lattice_route(layout, phase_m)
|
||||
return original_make_route(layout, phase_m)
|
||||
|
||||
|
||||
def lattice_route_nearest(point: tuple[float, float, float], route: dict):
|
||||
"""Resolve stacked helix turns in 3D, then measure corridor radius in XY."""
|
||||
best = None
|
||||
def consider(segment_index: int) -> None:
|
||||
nonlocal best
|
||||
a, b, s0, s1, radius, height, edge_index = route["segments"][segment_index]
|
||||
dx, dy, dz = (b[axis] - a[axis] for axis in range(3))
|
||||
length_squared = dx * dx + dy * dy + dz * dz
|
||||
t = 0.0 if length_squared <= 1.0e-12 else max(0.0, min(1.0,
|
||||
((point[0] - a[0]) * dx + (point[1] - a[1]) * dy
|
||||
+ (point[2] - a[2]) * dz) / length_squared))
|
||||
x, y, z = (a[axis] + (b[axis] - a[axis]) * t for axis in range(3))
|
||||
distance3d_squared = ((point[0] - x) ** 2 + (point[1] - y) ** 2
|
||||
+ (point[2] - z) ** 2)
|
||||
if (best is None or distance3d_squared < best[0]
|
||||
or (distance3d_squared == best[0] and segment_index < best[1])):
|
||||
horizontal_length = math.hypot(dx, dy)
|
||||
lateral = ((dx * (point[1] - y) - dy * (point[0] - x))
|
||||
/ horizontal_length if horizontal_length > 1.0e-9 else 0.0)
|
||||
best = (distance3d_squared, segment_index,
|
||||
math.hypot(point[0] - x, point[1] - y),
|
||||
s0 + (s1 - s0) * t, z, lateral, radius, height, edge_index,
|
||||
route["edges"][edge_index]["length"])
|
||||
|
||||
def lower_bound_squared(node: dict) -> float:
|
||||
distance_squared = 0.0
|
||||
for axis in range(3):
|
||||
gap = (node["minimum"][axis] - point[axis]
|
||||
if point[axis] < node["minimum"][axis]
|
||||
else point[axis] - node["maximum"][axis]
|
||||
if point[axis] > node["maximum"][axis] else 0.0)
|
||||
distance_squared += gap * gap
|
||||
return distance_squared
|
||||
|
||||
def visit(node: dict) -> None:
|
||||
if best is not None and lower_bound_squared(node) > best[0]:
|
||||
return
|
||||
if node["segments"] is not None:
|
||||
for segment_index in node["segments"]:
|
||||
consider(segment_index)
|
||||
return
|
||||
left, right = node["left"], node["right"]
|
||||
left_bound, right_bound = lower_bound_squared(left), lower_bound_squared(right)
|
||||
if left_bound <= right_bound:
|
||||
visit(left)
|
||||
visit(right)
|
||||
else:
|
||||
visit(right)
|
||||
visit(left)
|
||||
|
||||
visit(route["segment_bvh"])
|
||||
if best is None:
|
||||
raise ValueError("lattice audit route has no measurable segments")
|
||||
return best[2:]
|
||||
|
||||
|
||||
def mark_lattice_region(triangles: list, route: dict) -> None:
|
||||
margin = audit54.CORRIDOR_MARGIN_M
|
||||
extension = audit54.OPEN_GROUND_EXTENSION_M
|
||||
bounds = []
|
||||
for edge in route["edges"]:
|
||||
points = edge["points"]
|
||||
radius, height = edge["radius"], edge["height"]
|
||||
bounds.append((
|
||||
min(point[0] for point in points) - radius - margin,
|
||||
max(point[0] for point in points) + radius + margin,
|
||||
min(point[1] for point in points) - radius - margin,
|
||||
max(point[1] for point in points) + radius + margin,
|
||||
min(point[2] for point in points) - margin,
|
||||
max(point[2] for point in points) + height + margin))
|
||||
for room in route["rooms"]:
|
||||
center, half, floor = room["center"], room["half"], room["floor"]
|
||||
bounds.append((center[0] - half[0] - margin, center[0] + half[0] + margin,
|
||||
center[1] - half[1] - margin, center[1] + half[1] + margin,
|
||||
floor - margin, floor + 2.0 * half[2] + margin))
|
||||
|
||||
for triangle in triangles:
|
||||
x, y, z = triangle.center
|
||||
if not any(low_x <= x <= high_x and low_y <= y <= high_y
|
||||
and low_z <= z <= high_z
|
||||
for low_x, high_x, low_y, high_y, low_z, high_z in bounds):
|
||||
triangle.in_region = False
|
||||
triangle.reference = False
|
||||
continue
|
||||
nearest = lattice_route_nearest(triangle.center, route)
|
||||
distance, distance_s, floor_z, lateral, radius, height, edge_index, edge_length = nearest
|
||||
corridor = (-extension <= distance_s <= edge_length
|
||||
and distance <= radius + margin
|
||||
and floor_z - margin <= z <= floor_z + height + margin)
|
||||
room_box = any(abs(x - room["center"][0]) <= room["half"][0] + margin
|
||||
and abs(y - room["center"][1]) <= room["half"][1] + margin
|
||||
and room["floor"] - margin <= z
|
||||
<= room["floor"] + 2.0 * room["half"][2] + margin
|
||||
for room in route["rooms"])
|
||||
triangle.in_region = corridor or room_box
|
||||
inside_room = any(abs(x - room["center"][0]) <= room["half"][0] + 0.45
|
||||
and abs(y - room["center"][1]) <= room["half"][1] + 0.45
|
||||
for room in route["rooms"])
|
||||
outside_bore = distance > radius + 0.45 and not inside_room
|
||||
triangle.reference = (triangle.in_region and outside_bore
|
||||
and triangle.smooth[2] >= 0.45)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
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)
|
||||
parser.add_argument("--phase", type=float, choices=(-0.5, 0.0, 0.5), default=0.0)
|
||||
args = parser.parse_args()
|
||||
audit54.make_route = make_route
|
||||
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")
|
||||
native_lattice = layout != "-" and any(
|
||||
edge.get("stable_id", "").startswith("lattice/")
|
||||
for edge in json.loads(Path(layout).read_text(encoding="utf-8")).get(
|
||||
"cavern_edges", []))
|
||||
original_margin = audit54.CORRIDOR_MARGIN_M
|
||||
original_extension = audit54.OPEN_GROUND_EXTENSION_M
|
||||
original_nearest = audit54.route_nearest
|
||||
original_mark_region = audit54.mark_region
|
||||
if native_lattice:
|
||||
# Keep the whole-surface test on native records: one core voxel of
|
||||
# tolerance captures their meshed walls while excluding the separate
|
||||
# legacy transition above the resolved mouth.
|
||||
audit54.CORRIDOR_MARGIN_M = 0.25
|
||||
audit54.OPEN_GROUND_EXTENSION_M = 0.0
|
||||
audit54.route_nearest = lattice_route_nearest
|
||||
audit54.mark_region = mark_lattice_region
|
||||
try:
|
||||
results.append(audit54.audit_case(label, Path(mesh), Path(layout), expect,
|
||||
focus, args.svg_dir, args.phase))
|
||||
finally:
|
||||
audit54.CORRIDOR_MARGIN_M = original_margin
|
||||
audit54.OPEN_GROUND_EXTENSION_M = original_extension
|
||||
audit54.route_nearest = original_nearest
|
||||
audit54.mark_region = original_mark_region
|
||||
if native_lattice:
|
||||
print("[L58LatticeAuditScope] margin_m=0.250 outside_mouth_extension_m=0.000 "
|
||||
"nearest_centerline=3D radius_measure=horizontal status=PASS")
|
||||
args.json.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.json.write_text(json.dumps({"audit": "whole-surface-lattice-l58-v1",
|
||||
"cases": results}, indent=2) + "\n", encoding="utf-8")
|
||||
failures = [result for result in results if result["status"] != "PASS"]
|
||||
print(f"[L58WholeSurfaceAuditSummary] 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())
|
||||
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Split the owner surface strate around a small, opt-in native maze strate."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for block in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--owner", type=Path, required=True)
|
||||
parser.add_argument("--out", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
|
||||
root = json.loads(args.owner.read_text(encoding="utf-8"))
|
||||
strates = root.get("strates")
|
||||
if not isinstance(strates, list):
|
||||
raise ValueError("owner input has no strates array")
|
||||
by_index = {int(row.get("index", -1)): row for row in strates if isinstance(row, dict)}
|
||||
if set(by_index) != {0, 1} or len(strates) != 2:
|
||||
raise ValueError("fixture requires the reviewed two-strate owner input")
|
||||
upper, surface = by_index[0], by_index[1]
|
||||
if (int(upper.get("asset", {}).get("GeneratorType", -1)) != 0
|
||||
or int(surface.get("asset", {}).get("GeneratorType", -1)) != 4):
|
||||
raise ValueError("fixture requires TunnelNetwork over SurfaceWorld")
|
||||
if int(upper.get("passage_config", {}).get("Connections", -1)) != 1:
|
||||
raise ValueError("owner arrival passage count differs from the reviewed input")
|
||||
|
||||
# Keep both resolved mouths in the small synthetic Maze layer. The authored
|
||||
# owner reach ranges are intentionally broad and can place a synthetic mouth
|
||||
# hundreds of metres beyond a four-chunk prototype layer.
|
||||
upper["passage_config"].update({"ReachMin": 32, "ReachMax": 32})
|
||||
upper["asset"]["PassageConfig"].update({"ReachMin": 32, "ReachMax": 32})
|
||||
|
||||
maze = json.loads(json.dumps(surface))
|
||||
maze.update({"index": 1, "top_chunk_z": -16, "bottom_chunk_z": -19,
|
||||
"height_chunks": 4})
|
||||
maze["asset"].update({"StrateName": "L58 Native Lattice Maze",
|
||||
"GeneratorType": 3, "StrateHeightInChunks": 4})
|
||||
maze["asset"].setdefault("MazeParams", {})["BoundarySealThickness"] = 8
|
||||
maze["passage_config"]["Connections"] = 1
|
||||
maze["passage_config"].update({"ReachMin": 32, "ReachMax": 32})
|
||||
maze["asset"]["PassageConfig"].update({"Connections": 1, "ReachMin": 32, "ReachMax": 32})
|
||||
|
||||
lower = json.loads(json.dumps(surface))
|
||||
lower.update({"index": 2, "top_chunk_z": -20, "bottom_chunk_z": -23,
|
||||
"height_chunks": 4})
|
||||
lower["asset"].update({"StrateName": "L58 Lower Surface", "GeneratorType": 4,
|
||||
"StrateHeightInChunks": 4})
|
||||
lower["passage_config"]["Connections"] = 1
|
||||
lower["asset"]["PassageConfig"]["Connections"] = 1
|
||||
|
||||
root["total_strates"] = 3
|
||||
root["strates"] = [upper, maze, lower]
|
||||
# Keep the legacy passage portals proportional to this compact native
|
||||
# prototype; the owner world remains at its authored scale.
|
||||
root.update({"owner_voxel_size_cm": 6.25,
|
||||
"owner_voxel_size_microns": 62500,
|
||||
"core_voxel_size_cm": 25,
|
||||
"core_voxel_size_microns": 250000,
|
||||
"owner_to_core_length_scale": 0.25})
|
||||
args.out.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.out.write_text(json.dumps(root, indent=2, sort_keys=True) + "\n",
|
||||
encoding="utf-8")
|
||||
print(f"[LatticeFixtureBuild] source={args.owner} source_sha256={sha256(args.owner)} "
|
||||
f"fixture={args.out} fixture_sha256={sha256(args.out)} "
|
||||
"strates=0:TunnelNetwork,1:Maze[4 chunks],2:SurfaceWorld[60 chunks] "
|
||||
"owner_input_untouched=true status=PASS")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,164 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Export the L58 native lattice view and render four reviewed viewpoints."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
CAPTURE54 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux54/CaptureNativeCavesL54.py"
|
||||
spec = importlib.util.spec_from_file_location("capture_native_caves_l54", CAPTURE54)
|
||||
capture54 = importlib.util.module_from_spec(spec)
|
||||
assert spec and spec.loader
|
||||
spec.loader.exec_module(capture54)
|
||||
run_step = capture54.run_step
|
||||
camera_angles = capture54.camera_angles
|
||||
filter_mesh_y_leq = capture54.filter_mesh_y_leq
|
||||
fmt = capture54.fmt
|
||||
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for block in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def midpoint(points: list[list[float]]) -> tuple[float, float, float]:
|
||||
return tuple(sum(point[axis] for point in points) / len(points) for axis in range(3))
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--out", type=Path, required=True)
|
||||
parser.add_argument("--fixture", type=Path, required=True)
|
||||
parser.add_argument("--layout", type=Path, required=True)
|
||||
parser.add_argument("--headless", type=Path, required=True)
|
||||
parser.add_argument("--renderer", type=Path, required=True)
|
||||
parser.add_argument("--artifact-dir", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
|
||||
out = args.out.resolve()
|
||||
artifact_dir = args.artifact_dir.resolve()
|
||||
capture_dir = out / "lattice-capture-png"
|
||||
mesh_dir = out / "lattice-meshes"
|
||||
for directory in (capture_dir, mesh_dir, artifact_dir):
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
layout = json.loads(args.layout.read_text(encoding="utf-8"))
|
||||
edges = sorted(layout.get("cavern_edges", []), key=lambda row: row["stable_id"])
|
||||
helix = next((edge for edge in edges
|
||||
if edge["stable_id"] == "lattice/spine/01-helix-ramp-v1"), None)
|
||||
maze_edges = [edge for edge in edges
|
||||
if edge["stable_id"].startswith("lattice/spine/maze-edge-")]
|
||||
if helix is None or len(maze_edges) < 4:
|
||||
raise ValueError("layout is missing the resolved helix or small maze spine")
|
||||
helix_points = [[value / 1_000_000.0 for value in point]
|
||||
for point in helix["polyline_um"]]
|
||||
maze_points = [[value / 1_000_000.0 for value in point]
|
||||
for point in maze_edges[0]["polyline_um"]]
|
||||
shaft_center = midpoint(helix_points)
|
||||
top = helix_points[0]
|
||||
bottom = helix_points[-1]
|
||||
whole_mesh = mesh_dir / "lattice-whole-view.vfmesh"
|
||||
export_layout = out / "layouts" / "lattice-capture-layout.json"
|
||||
env = os.environ.copy()
|
||||
env["LP_NUM_THREADS"] = "12"
|
||||
env["XDG_CACHE_HOME"] = str(out / "xdg-cache")
|
||||
export_camera = (top[0], top[1], top[2] + 1.5, 0.0, -8.0)
|
||||
run_step("lattice-whole-view-export", [args.headless, args.fixture,
|
||||
"--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=lattice", "--lattice-strate=1",
|
||||
f"--camera={fmt(export_camera)}", f"--resolved-layout-output={export_layout}",
|
||||
f"--mesh-export={whole_mesh}"], out / "capture-lattice-export.log",
|
||||
env=env, stamped=True)
|
||||
if not whole_mesh.is_file() or whole_mesh.stat().st_size == 0:
|
||||
raise RuntimeError("whole-lattice export produced no mesh")
|
||||
|
||||
cutaway_mesh = mesh_dir / "lattice-cutaway-y-center.vfmesh"
|
||||
cutaway_triangles = filter_mesh_y_leq(whole_mesh, cutaway_mesh, shaft_center[1])
|
||||
print(f"[L58LatticeCutaway] plane_y_m={shaft_center[1]:.3f} "
|
||||
f"retained_triangles={cutaway_triangles} mesh={cutaway_mesh}", flush=True)
|
||||
|
||||
# Stand over the clear central opening so the top tread does not occlude
|
||||
# the view down the shaft.
|
||||
top_camera = (shaft_center[0], shaft_center[1] - 0.8, top[2] + 2.0)
|
||||
top_target = (shaft_center[0] + 0.2, shaft_center[1], bottom[2] + 1.3)
|
||||
eye_index = len(helix_points) // 2
|
||||
eye_floor = helix_points[eye_index]
|
||||
eye_tangent = tuple(helix_points[eye_index + 1][axis]
|
||||
- helix_points[eye_index - 1][axis] for axis in range(3))
|
||||
eye_len = max(1.0e-9, math.hypot(eye_tangent[0], eye_tangent[1]))
|
||||
eye_camera = (eye_floor[0], eye_floor[1], eye_floor[2] + 1.45)
|
||||
# Look across the shaft center from the ramp so the adjacent higher turn
|
||||
# reads in the same frame. The view clips the near half of the shaft wall.
|
||||
eye_target = (shaft_center[0], shaft_center[1], eye_camera[2] + 4.2)
|
||||
maze_a, maze_b = maze_points[0], maze_points[-1]
|
||||
maze_camera = ((maze_a[0] + maze_b[0]) * 0.5,
|
||||
(maze_a[1] + maze_b[1]) * 0.5,
|
||||
(maze_a[2] + maze_b[2]) * 0.5 + 1.45)
|
||||
maze_target = (maze_camera[0] + (maze_b[0] - maze_a[0]) * 0.65,
|
||||
maze_camera[1] + (maze_b[1] - maze_a[1]) * 0.65,
|
||||
maze_camera[2] + (maze_b[2] - maze_a[2]) * 0.65)
|
||||
overview_camera = (shaft_center[0], shaft_center[1] - 28.0,
|
||||
(top[2] + bottom[2]) * 0.5)
|
||||
overview_target = (shaft_center[0], shaft_center[1], (top[2] + bottom[2]) * 0.5)
|
||||
views = (
|
||||
("helix-top-down", whole_mesh, top_camera, top_target, 22.0, 70.0, None),
|
||||
("ramp-eye-next-turn", whole_mesh, eye_camera, eye_target, 12.0, 76.0,
|
||||
(0.0, 1.0, 0.0, -shaft_center[1])),
|
||||
("maze-eye-level", whole_mesh, maze_camera, maze_target, 14.0, 82.0, None),
|
||||
("cutaway-overview", cutaway_mesh, overview_camera, overview_target, 40.0, 80.0, None),
|
||||
)
|
||||
quality = ROOT / "Standalone/Tools/verify/capture_quality.py"
|
||||
for name, render_mesh, camera, target, focus_radius, fov, clip_plane in views:
|
||||
yaw, pitch = camera_angles(camera, target)
|
||||
if name == "ramp-eye-next-turn":
|
||||
yaw, pitch = 45.0, 10.0
|
||||
png = capture_dir / f"lattice-{name}.png"
|
||||
image = artifact_dir / f"lattice-{name}.jpg"
|
||||
render_args = [args.renderer, render_mesh, png,
|
||||
"--size=1280x720", f"--fov-deg={fov:.1f}",
|
||||
f"--camera-m={fmt(camera)}", f"--yaw-deg={yaw:.6f}",
|
||||
f"--pitch-deg={pitch:.6f}", "--tile-skirts=off", "--shade=readable",
|
||||
"--headlamp=on", "--height-tint=on",
|
||||
f"--height-center-m={(top[2] + bottom[2]) * 0.5:.6f}",
|
||||
"--height-range-m=24", "--tile-edges=off",
|
||||
f"--focus-region={fmt((*target, focus_radius))}"]
|
||||
if clip_plane is not None:
|
||||
render_args.append(f"--clip-plane={fmt(clip_plane)}")
|
||||
run_step(f"{name}-render", render_args,
|
||||
out / f"capture-{name}-render.log", env=env, stamped=True)
|
||||
run_step(f"{name}-jpeg", ["ffmpeg", "-nostdin", "-hide_banner", "-loglevel",
|
||||
"error", "-y", "-i", png, "-frames:v", "1", "-q:v", "5", image],
|
||||
out / f"capture-{name}-jpeg.log", env=env)
|
||||
quality_output = run_step(f"{name}-quality", [sys.executable, quality, image],
|
||||
out / f"capture-{name}-quality.log", env=env)
|
||||
quality_lines = [line for line in quality_output.splitlines()
|
||||
if line.startswith("[CaptureQuality]")]
|
||||
if not quality_lines or not quality_lines[-1].endswith("status=PASS"):
|
||||
raise RuntimeError(f"capture quality gate failed for {name}")
|
||||
size = image.stat().st_size
|
||||
if size > 300_000:
|
||||
raise RuntimeError(f"capture exceeds 300 KB: {image} ({size} bytes)")
|
||||
print(f"[L58LatticeCapture] view={name} file={image} bytes={size} "
|
||||
f"sha256={sha256(image)} status=PASS", flush=True)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except Exception as error:
|
||||
print(f"[L58LatticeCapture] status=FAIL error={error}", file=sys.stderr, flush=True)
|
||||
raise SystemExit(1)
|
||||
@@ -0,0 +1,51 @@
|
||||
# Linux58 — Native lattice, slice 1
|
||||
|
||||
## Records and geometry
|
||||
|
||||
The fixture uses seed `0`, strate `1`, and recipe `Core.Lattice.PrimitiveField` version `1`. Room and edge IDs are stable and keyed by `(seed, strate, recipe_version, stable_id)`. The resolved arrival is `passage/0-1/0` lower at `(9.142, -2.073, -122.500) m`; departure is `passage/1-2/0` upper at `(12.271, -3.565, -149.500) m`. The plan resolves 9 rooms and 9 edges before branches: arrival/departure rooms, maze cells `00`–`04`, two dead-end rooms; arrival portal, helix ramp, maze edges `00`–`03`, departure portal, and two dead-end edges. The canonical record digest is `639d8b975024c9824c944f8244fae88d796350da5f4168b85d5ec7b501c5a842`.
|
||||
|
||||
The mandatory spine descends 20 m through four helix turns: 6 m centreline radius, 3 m clear width, 5 m pitch, and 2.6 m finished floor-to-soffit. Measured grades are 7.555° at the centreline and 10.028° at the inner edge. The cubic smooth-max fillet measures `0.570778 m` (required `>=0.500 m`). The prototype’s accepted outer shaft envelope is 21 m. The maze has five cells and two dead ends. This recipe is opt-in; legacy Maze and VerticalShaft assets remain legacy.
|
||||
|
||||
## Reachability, intervals, and surface
|
||||
|
||||
On the final field, arrival-to-departure reachability passed all 883 stations: 44,150 capsule samples, 4,415 support samples, and 4,415 headroom samples had zero failures. Maximum grade was 10.028° and minimum headroom was 2.200 m. The deliberately disconnected shaft control was rejected (`reachable=no`; zero connected route stations).
|
||||
|
||||
The interval oracle checked 4 boxes against 1,372 brute-force samples with zero misses. It rejected both false all-air and false all-solid proofs; `isTileShapeFree` blocked the occupied wall-foot tile. The whole-surface audit covered 89,577 LOD0 triangles across the helix edges and maze wall feet: zero flagged triangles and zero clusters. The L45 ramp, L46 entrance, and L47 wall-foot controls failed as expected; the plain-native control passed.
|
||||
|
||||
## Peers and timing
|
||||
|
||||
Clang and GCC builds used FP contraction off. W1/W8/W12 matched for four randomized cameras, including one inside the helix, with matching tile hashes and the canonical record digest above.
|
||||
|
||||
Clang measured first tile `0.201 s`, near ring `0.345 s`, whole view `1.694 s`, and moving p50/p95 `485.536/977.128 ms`. The whole view is above the 0.7–1.0 s target; moving p95 is just under 1 s.
|
||||
|
||||
## Captures and hashes
|
||||
|
||||
All four JPEGs passed the capture gate, were visually reviewed, and are below 300 KB.
|
||||
|
||||
| View | Bytes | SHA-256 |
|
||||
|---|---:|---|
|
||||
| `captures/lattice-helix-top-down.jpg` | 19,616 | `0ab87ee1e799ce96752dc3bf9d6803928e4d6aaad8a87400d899ff3f3be06149` |
|
||||
| `captures/lattice-ramp-eye-next-turn.jpg` | 22,540 | `d16b0ad577f5ec0e676e18ba62a5de64c71385cd93b53b225ac25f109b184265` |
|
||||
| `captures/lattice-maze-eye-level.jpg` | 12,361 | `463ee13e962ecc36de1778abcd09f34f5bd375db5537395e7b1e0e0df3d72331` |
|
||||
| `captures/lattice-cutaway-overview.jpg` | 28,673 | `10204ac2b120ad360e31ac589351c31d0c0e9603feb22841860bd4e145613194` |
|
||||
|
||||
Source stamp: `7d181a5b606879ae7277db296e43df11f3fdef41eefa127f5c86d8a431fc4a10`. The owner input remains unchanged at SHA-256 `7b095f9bcc6b90133d5a463f99f0b571133b2c549ce1ecf2d64fe9175e41e183`.
|
||||
|
||||
## Tiers, trials, and failed runs
|
||||
|
||||
`verify.py fast` passed in 176.4 s; `medium` passed in 1,170.0 s. The single full run on the clean build reached 1,700 s and timed out during the later GCC motif matrix. Before timeout, its L58 GCC field, orphan control, W1/W8/W12 peers, inside-helix camera, and Clang comparisons had passed. No second full run was made.
|
||||
|
||||
The run ledger records 10 verification-tier invocations against the 15-trial limit: 3 fast, 6 medium, and 1 full. Earlier failures were a stale style assertion; capture framing/quality; the initial broad transition ROI and boundary seam; and a floor-profile ripple (`0.0887 m`, adjacent step `0.1791 m`) before blend tuning. One direct surface audit initially had a missing `math` import, and one diagnostic parse had a syntax error; both were corrected. A post-tuning fast assertion also used stale blend values. The final fast and medium tiers passed. The only final-tier failure was the full-run timeout above.
|
||||
|
||||
## Orchestrator review (Claude)
|
||||
|
||||
Accepted. The stamp matches the final Clang and GCC builds (`7d181a5b…`). The full tier had timed out at the builder's
|
||||
1,740 s process wrapper (the tier itself now runs about 38 min). I re-ran it with the tier's own 7,200 s limit:
|
||||
**PASS, 2,307 s.** Reachability passes at all 883 stations, the orphan control is rejected, and the surface audit is
|
||||
clean. Two items for the next lattice slice:
|
||||
- **Whole view 1.69 s** is above the about-1 s target.
|
||||
- **It reads as a corkscrew tunnel bored through rock** (separate stacked turns with rock between them). It doesn't
|
||||
read as an open vertical shaft with a ramp around its wall, which is what "vertical shaft world" implies. Open the
|
||||
shaft core in slice 2.
|
||||
|
||||
Removed the duplicate control SVGs (already in Linux48).
|
||||
|
After Width: | Height: | Size: 3.1 MiB |
|
After Width: | Height: | Size: 2.7 MiB |
|
After Width: | Height: | Size: 3.1 MiB |
|
After Width: | Height: | Size: 4.8 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 22 KiB |