From b8382eed8467e7bdad02c28cad71edffc3df26b4 Mon Sep 17 00:00:00 2001 From: Fr0zka Date: Sun, 1 Nov 2026 15:24:49 +0100 Subject: [PATCH] research: one continuous tube through the mouth (L149, 0/16; kept as patch) 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 Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1 --- .../WorldAssemblerM3/Linux149/MouthAudit.py | 169 +++++ .../WorldAssemblerM3/Linux149/REPORT.md | 32 + .../Linux149/RunL149Determinism.py | 93 +++ .../Linux149/RunL149MouthCaptures.py | 185 ++++++ .../Linux149/RunL149SeedSweep.py | 144 ++++ .../__pycache__/MouthAudit.cpython-313.pyc | Bin 0 -> 11956 bytes ...er-seed0-tunnel-network-departure-edge.jpg | Bin 0 -> 19137 bytes ...fter-seed0-tunnel-network-route-edge-0.jpg | Bin 0 -> 18837 bytes ...ter-seed2-vertical-shafts-route-edge-0.jpg | Bin 0 -> 20515 bytes ...re-seed0-tunnel-network-departure-edge.jpg | Bin 0 -> 18526 bytes ...fore-seed0-tunnel-network-route-edge-0.jpg | Bin 0 -> 18133 bytes ...ore-seed2-vertical-shafts-route-edge-0.jpg | Bin 0 -> 21112 bytes .../Linux149/captures/manifest.json | 122 ++++ .../Linux149/l149-one-tube-mouth.patch | 629 ++++++++++++++++++ 14 files changed, 1374 insertions(+) create mode 100644 Standalone/Research/WorldAssemblerM3/Linux149/MouthAudit.py create mode 100644 Standalone/Research/WorldAssemblerM3/Linux149/REPORT.md create mode 100644 Standalone/Research/WorldAssemblerM3/Linux149/RunL149Determinism.py create mode 100644 Standalone/Research/WorldAssemblerM3/Linux149/RunL149MouthCaptures.py create mode 100644 Standalone/Research/WorldAssemblerM3/Linux149/RunL149SeedSweep.py create mode 100644 Standalone/Research/WorldAssemblerM3/Linux149/__pycache__/MouthAudit.cpython-313.pyc create mode 100644 Standalone/Research/WorldAssemblerM3/Linux149/captures/after-seed0-tunnel-network-departure-edge.jpg create mode 100644 Standalone/Research/WorldAssemblerM3/Linux149/captures/after-seed0-tunnel-network-route-edge-0.jpg create mode 100644 Standalone/Research/WorldAssemblerM3/Linux149/captures/after-seed2-vertical-shafts-route-edge-0.jpg create mode 100644 Standalone/Research/WorldAssemblerM3/Linux149/captures/before-seed0-tunnel-network-departure-edge.jpg create mode 100644 Standalone/Research/WorldAssemblerM3/Linux149/captures/before-seed0-tunnel-network-route-edge-0.jpg create mode 100644 Standalone/Research/WorldAssemblerM3/Linux149/captures/before-seed2-vertical-shafts-route-edge-0.jpg create mode 100644 Standalone/Research/WorldAssemblerM3/Linux149/captures/manifest.json create mode 100644 Standalone/Research/WorldAssemblerM3/Linux149/l149-one-tube-mouth.patch diff --git a/Standalone/Research/WorldAssemblerM3/Linux149/MouthAudit.py b/Standalone/Research/WorldAssemblerM3/Linux149/MouthAudit.py new file mode 100644 index 0000000..346a6b1 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux149/MouthAudit.py @@ -0,0 +1,169 @@ +#!/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} diff --git a/Standalone/Research/WorldAssemblerM3/Linux149/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux149/REPORT.md new file mode 100644 index 0000000..1645cf5 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux149/REPORT.md @@ -0,0 +1,32 @@ +# L149 — one tunnel through the mouth + +**L149 does not meet the bar.** The mouth anchors now match in position, tangent, radius and floor slope, but the departure spline still turns back alongside the passage, and the composed shell audit still finds floor violations. I stopped after the third geometry attempt. + +## Changes and result + +- Planner cave and shaft edges now start from the passage mouth floor, use the mouth radius and tangent, and carry the mouth floor slope. Their runtime SDF uses the matching flat-bottom tube profile. +- Owner route samples now follow the actual mouth-edge curve 2 m from the joint. The portal lead was shortened from 6 m to 1 m, and the passage-edge clearance exclusion was reduced from 10 m to 2 m. +- Evaluator version is v238. Added Clang W1/W12 generated owner-hash baselines for surface-world style 1 and floating-islands style 2. +- These were the three geometry attempts: match the mouth tube; move route samples onto the curve; shorten the lead and clearance exclusion. The final seed-2 shell gate still failed, so there was no fourth attempt. + +The measured premise was: matching the mouth geometry and shortening the lead would make the first/last route edge continue the passage as one forward tube. It is false for the current cubic edge. The seed-2 departure edge reaches about **1.00 m behind the mouth by 5.07 m of travel**, despite an initial tangent dot of 1.0. The shell audit independently finds three floor violations at passage 2–3. + +## Measurements and checks + +- **Focused seed 2, W8, clip radius 3, max level 0, final source stamp `19dbe26a…`:** route audits for strata 2 and 3 pass. Shell audit: 85,162 floor samples, 3 violations; 5,589 wall samples, 0 violations; 8,430 interior samples, 0 violations. The interior capsule audit passes with 281,043 samples and 3/3 negative controls. Ripple, naturalness and mouth-profile summaries pass. The audit exits 10 after the shell failure, before emitting the tile-seam audit; seam status is therefore **not measured** in these failing runs. +- **Builds:** final Clang and GCC headless binaries and the Clang offscreen renderer built successfully with source stamp `19dbe26ab76a629baadc54bb0ab93eaa22d6f878e6946df2b71509c12c9524e4`. +- **Mouth geometry audit:** all three seed-2 mouth edges match their anchor position and radius; minimum tangent dot is 0.9999999999886, maximum floor-slope delta is 0.000475, and all 3/3 reversed-tangent negative controls are detected. The first-12-m forward-extension check fails on the departure edge. Across the 16-seed sets it passes 1/16. +- **Composed sweeps, W8, seeds 0–15, clip radius 3, max clip level 8, max level 0:** Tunnel Network **0/16**, Vertical Shafts **0/16**. Both sets have 0/16 passing shell and passage summaries. The interior-air capsule summary passes 6/16 in each set. Naturalness, mouth profile, ripple and the interior, ripple-gap and blocked-route negative controls pass 16/16. The route audit passes 2/16 Tunnel Network cases and 8/16 Vertical Shafts cases. The seam audit is not reached because the shell audit exits first. +- **Clang/GCC worker matrix, seed 2, W1/W8/W12:** all six runs emitted the same 12-line pipeline hash signature and resolved layout identity `resolved-layout-v3-fnv64:d980465950135a9d`. This focused matrix used `--render-distance-chunks=0` and max level 0; it checks the seed-2 view and layout identity, not a full multi-LOD world. The fast tier also passed its owner W1/W12 hash baselines and random-camera W1/W8/W12 checks. +- **Fast tier:** **FAIL**, 356.1 s. Existing owner passage checks, v238 hash baselines, worker checks and negative controls passed. The new four-strate owner-plan stage failed its tunnel-shell and passage-summary checks, arrival/departure chain, and strata-2/3 arrival/departure walkability checks. +- **Captures:** three matched-camera before/after comparisons, rendered at 1280×720 from mouth height. All six images pass capture quality. Before images use the L147/v237 executable baseline; L148's saved exclusion-gate build rejected seed 0 before producing a layout. Its accepted seed-2 layout identity matches L147. After images use the final v238 source stamp. + +| Joint | Before | After | +| --- | --- | --- | +| Seed 0, arrival | [Before](captures/before-seed0-tunnel-network-route-edge-0.jpg) | [After](captures/after-seed0-tunnel-network-route-edge-0.jpg) | +| Seed 0, departure | [Before](captures/before-seed0-tunnel-network-departure-edge.jpg) | [After](captures/after-seed0-tunnel-network-departure-edge.jpg) | +| Seed 2, shaft arrival | [Before](captures/before-seed2-vertical-shafts-route-edge-0.jpg) | [After](captures/after-seed2-vertical-shafts-route-edge-0.jpg) | + +## Remaining risks + +The v238 geometry still fails the requested composed shell and route gates, so it is not a passing L149 result. The captures are ready for the owner’s visual review. No Unreal module `Source/` files changed; `Standalone/Source/WorldAssemblyPrototype.h` did change. diff --git a/Standalone/Research/WorldAssemblerM3/Linux149/RunL149Determinism.py b/Standalone/Research/WorldAssemblerM3/Linux149/RunL149Determinism.py new file mode 100644 index 0000000..2f2631e --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux149/RunL149Determinism.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +"""Compare L149 hashes and layout identity at Clang/GCC W1, W8, and W12.""" +from __future__ import annotations + +import json +import re +import subprocess +import time +from pathlib import Path + +from MouthAudit import audit_layout + + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l149/determinism" +DATA = ROOT / "Standalone/data/owner_world.json" +BINARIES = { + "clang": ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-l149-clang", + "gcc": ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-l149-gcc", +} +STAMP = "19dbe26ab76a629baadc54bb0ab93eaa22d6f878e6946df2b71509c12c9524e4" +CAMERA = "-20.062500,-5.068359,-900.353516,0,0" +HASH_RE = re.compile( + r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", + re.M) +STAMP_RE = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]+)$", re.M) + + +def main() -> int: + if any(not path.is_file() for path in (DATA, *BINARIES.values())): + raise SystemExit("missing L149 Clang/GCC binary or composed owner fixture") + OUT.mkdir(parents=True, exist_ok=True) + rows = [] + partial = OUT / "runs-partial.json" + for compiler, binary in BINARIES.items(): + for workers in (1, 8, 12): + name = f"determinism-{compiler}-w{workers}" + log, layout = OUT / f"{name}.log", OUT / f"{name}.json" + command = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(binary), str(DATA), "--world-assembler-prototype", + "--stream-benchmark", "--stream-pipeline", f"--workers={workers}", + "--clip-radius=3", "--max-clip-level=8", "--max-level=0", + f"--camera={CAMERA}", "--stream-hash", "--force-generate", "--seed=2", + "--recipe=surface-world", "--render-distance-chunks=0", + "--density-dispatch-report", f"--resolved-layout-output={layout}"] + started = time.monotonic() + with log.open("w", encoding="utf-8") as output: + proc = subprocess.run(command, cwd=ROOT, stdout=output, + stderr=subprocess.STDOUT, check=False, timeout=1740) + content = log.read_text(encoding="utf-8", errors="replace") + stamp_match = STAMP_RE.search(content) + stamp = stamp_match.group(1) if stamp_match else None + hashes = tuple(HASH_RE.findall(content)) + identity = (json.loads(layout.read_text(encoding="utf-8")).get("stable_identity") + if layout.is_file() else None) + mouth_audit = audit_layout(layout) if layout.is_file() else None + row = {"compiler": compiler, "workers": workers, "exit": proc.returncode, + "stamp": stamp, "source_stamp_matches": stamp == STAMP, + "hash_lines": len(hashes), "hashes": hashes, + "layout_identity": identity, + "mouth_audit": mouth_audit, + "elapsed_s": round(time.monotonic() - started, 3), + "log": str(log.relative_to(ROOT)), "layout": str(layout.relative_to(ROOT))} + rows.append(row) + partial.write_text(json.dumps(rows, indent=2) + "\n", encoding="utf-8") + print("[L149DeterminismRun] " + json.dumps(row, sort_keys=True), flush=True) + valid = [row for row in rows if row["source_stamp_matches"] + and row["hash_lines"] == 12 and row["layout_identity"]] + reference_hashes = valid[0]["hashes"] if valid else [] + reference_identity = valid[0]["layout_identity"] if valid else None + deterministic = len(valid) == 6 and all( + row["hashes"] == reference_hashes and row["layout_identity"] == reference_identity + for row in valid) + mouth_geometry = all(row["mouth_audit"] and row["mouth_audit"]["continuity"] + and row["mouth_audit"]["negative_controls_pass"] + for row in rows) + no_backtrack = all(row["mouth_audit"] and row["mouth_audit"]["no_backtrack_12m"] + for row in rows) + summary = {"status": "PASS" if deterministic else "FAIL", + "deterministic": deterministic, "mouth_geometry_pass": mouth_geometry, + "no_backtrack_12m": no_backtrack, + "source_stamp": STAMP, "layout_identity": reference_identity, "runs": rows} + (OUT / "summary.json").write_text(json.dumps(summary, indent=2) + "\n", + encoding="utf-8") + print(f"[L149DeterminismSummary] runs={len(rows)} deterministic=" + f"{'PASS' if deterministic else 'FAIL'} mouth_geometry=" + f"{'PASS' if mouth_geometry else 'FAIL'} no_backtrack_12m=" + f"{'PASS' if no_backtrack else 'FAIL'} status={summary['status']}", flush=True) + return 0 if summary["status"] == "PASS" else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux149/RunL149MouthCaptures.py b/Standalone/Research/WorldAssemblerM3/Linux149/RunL149MouthCaptures.py new file mode 100644 index 0000000..c3eb5f5 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux149/RunL149MouthCaptures.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python3 +"""Capture the seed 0/2 cave-mouth joints before and after L149.""" +from __future__ import annotations + +import json +import math +import os +from pathlib import Path +import re +import subprocess +import sys + + +ROOT = Path(__file__).resolve().parents[4] +DATA = ROOT / "Standalone/data/owner_world.json" +OUT = ROOT / "Saved/Linux/l149/captures" +ARTIFACTS = ROOT / "Standalone/Research/WorldAssemblerM3/Linux149/captures" +LIMIT = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--"] +VERSIONS = { + "before-l147": { + "headless": ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-l147-clang", + "renderer": ROOT / "Standalone/bin/linux/VoxelForgeOffscreenCapture-l147-clang", + "stamp": "c6b8a100355e271ddc40eb5973569d2ff4e2ba8d9c628c72f8e0af2493f0609f", + }, + "after-l149": { + "headless": ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-l149-clang", + "renderer": ROOT / "Standalone/bin/linux/VoxelForgeOffscreenCapture-l149-clang", + "stamp": "19dbe26ab76a629baadc54bb0ab93eaa22d6f878e6946df2b71509c12c9524e4", + }, +} +JOINTS = ( + (0, "tunnel-network", "tunnel-network/strate-2/route-edge-0"), + (0, "tunnel-network", "tunnel-network/strate-2/departure-edge"), + (2, "vertical-shafts", "vertical-shafts/strate-3/route-edge-0"), +) +STAMP_RE = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]{64})$", re.M) + + +def fmt(values: tuple[float, ...]) -> str: + return ",".join(f"{value:.6f}" for value in values) + + +def angles(camera: tuple[float, ...], target: tuple[float, ...]) -> tuple[float, float]: + dx, dy, dz = (target[i] - camera[i] for i in range(3)) + return math.degrees(math.atan2(dx, dy)), math.degrees( + math.atan2(dz, math.hypot(dx, dy))) + + +def run(name: str, command: list[object], log: Path, expected_stamp: str, + env: dict[str, str]) -> str: + binary = Path(str(command[0])) + probe = subprocess.run([*LIMIT, "timeout", "15s", str(binary), "--print-source-stamp"], + cwd=ROOT, env=env, text=True, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, check=False, timeout=20) + match = STAMP_RE.search(probe.stdout or "") + if probe.returncode != 0 or not match or match.group(1) != expected_stamp: + raise RuntimeError(f"{name}: executable stamp mismatch for {binary}") + log.parent.mkdir(parents=True, exist_ok=True) + with log.open("w", encoding="utf-8") as stream: + result = subprocess.run([*LIMIT, "timeout", "1740s", *map(str, command)], + cwd=ROOT, env=env, stdout=stream, stderr=subprocess.STDOUT, + timeout=1745, check=False) + output = log.read_text(encoding="utf-8", errors="replace") + emitted = STAMP_RE.search(output) + if result.returncode != 0 or not emitted or emitted.group(1) != expected_stamp: + raise RuntimeError(f"{name}: process failed or source changed; see {log}") + return output + + +def q_to_metres(q: int, core_voxel_um: int) -> float: + return q * core_voxel_um / (256.0 * 1_000_000.0) + + +def camera_for(layout: dict, edge: dict) -> tuple[tuple[float, ...], tuple[float, ...]]: + anchor = edge["start_anchor_stable_id"] + if anchor.endswith("/lower-mouth"): + side = "lower" + mouth_q = next(p["lower_mouth_q"] for p in layout["passages"] + if p["stable_id"] == anchor.removesuffix("/lower-mouth")) + elif anchor.endswith("/upper-mouth"): + side = "upper" + mouth_q = next(p["upper_mouth_q"] for p in layout["passages"] + if p["stable_id"] == anchor.removesuffix("/upper-mouth")) + else: + raise ValueError(f"edge has no passage mouth anchor: {anchor}") + passage = next(p for p in layout["passages"] + if p["stable_id"] == anchor.rsplit("/", 1)[0]) + controls = passage["control_points"] + mouth_index = len(controls) - 1 if side == "lower" else 0 + neighbor_index = mouth_index - 1 if side == "lower" else 1 + a, b = controls[neighbor_index]["position_q"], controls[mouth_index]["position_q"] + sign = 1.0 if side == "lower" else -1.0 + tx, ty = sign * (b[0] - a[0]), sign * (b[1] - a[1]) + tangent_length = math.hypot(tx, ty) + if tangent_length <= 1.0e-9: + raise ValueError(f"passage has a degenerate {side} mouth tangent: {anchor}") + tx, ty = tx / tangent_length, ty / tangent_length + scale = layout["core_voxel_size_um"] + mx, my, mz = (q_to_metres(value, scale) for value in mouth_q) + camera = (mx - tx * 2.0, my - ty * 2.0, mz + 0.47) + target = (mx + tx * 10.0, my + ty * 10.0, mz + 0.47) + return camera, target + + +def main() -> int: + if not DATA.is_file(): + raise SystemExit("missing composed owner fixture") + OUT.mkdir(parents=True, exist_ok=True) + ARTIFACTS.mkdir(parents=True, exist_ok=True) + env = os.environ.copy() + env["LP_NUM_THREADS"] = "12" + env["XDG_CACHE_HOME"] = str(OUT / "xdg-cache") + quality = ROOT / "Standalone/Tools/verify/capture_quality.py" + manifest = [] + failures = [] + for version, config in VERSIONS.items(): + phase = "before" if version.startswith("before") else "after" + for seed, kind, edge_id in JOINTS: + name = f"seed{seed}-{kind}-{edge_id.rsplit('/', 1)[-1]}" + reference_path = ROOT / f"Saved/Linux/l147/seeds/{kind}/seed{seed}.json" + reference_layout = json.loads(reference_path.read_text(encoding="utf-8")) + reference_edge = next(row for row in reference_layout["cavern_edges"] + if row["stable_id"] == edge_id) + camera, target = camera_for(reference_layout, reference_edge) + yaw, pitch = angles(camera, target) + mesh = OUT / "meshes" / phase / f"{name}.vfmesh" + layout_path = OUT / "meshes" / phase / f"{name}-layout.json" + export_log = OUT / f"{phase}-{name}-export.log" + export = [config["headless"], DATA, "--world-assembler-prototype", + "--stream-benchmark", "--stream-pipeline", "--workers=8", + "--clip-radius=6", "--max-clip-level=0", "--max-level=0", + "--force-generate", f"--seed={seed}", "--recipe=surface-world", + "--render-distance-chunks=0", + f"--camera={fmt((*camera, yaw, pitch))}", + f"--resolved-layout-output={layout_path}", + f"--mesh-export={mesh}"] + run(f"{phase}-{name}-export", export, export_log, config["stamp"], env) + if not mesh.is_file() or mesh.stat().st_size == 0: + raise RuntimeError(f"{phase}-{name}: empty mesh export") + layout = json.loads(layout_path.read_text(encoding="utf-8")) + png = OUT / f"{phase}-{name}.png" + jpg = ARTIFACTS / f"{phase}-{name}.jpg" + render = [config["renderer"], mesh, png, "--size=1280x720", + "--fov-deg=78.0", 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", "--height-center-m=-950", "--height-range-m=60", + "--tile-edges=off", f"--focus-region={fmt((*camera[:3], 12.0))}"] + run(f"{phase}-{name}-render", render, + OUT / f"{phase}-{name}-render.log", config["stamp"], env) + subprocess.run([*LIMIT, "timeout", "120s", "ffmpeg", "-nostdin", + "-hide_banner", "-loglevel", "error", "-y", "-i", png, + "-frames:v", "1", "-q:v", "4", jpg], cwd=ROOT, env=env, + check=True, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT, + timeout=125) + check = subprocess.run([*LIMIT, "timeout", "120s", sys.executable, + quality, jpg], cwd=ROOT, env=env, text=True, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, check=False, timeout=125) + quality_ok = check.returncode == 0 and "status=PASS" in check.stdout + quality_result = (check.stdout.strip().splitlines()[-1] + if check.stdout.strip() else f"quality command exit={check.returncode}") + if not quality_ok: + failures.append(name) + manifest.append({"phase": phase, "view": name, + "file": str(jpg.relative_to(ROOT)), "bytes": jpg.stat().st_size, + "camera_m": camera, "target_m": target, "yaw_deg": yaw, + "pitch_deg": pitch, "quality_status": "PASS" if quality_ok else "FAIL", + "quality": quality_result}) + print(f"[L149MouthCapture] phase={phase} view={name} " + f"file={jpg.relative_to(ROOT)} quality={'PASS' if quality_ok else 'FAIL'}", + flush=True) + (ARTIFACTS / "manifest.json").write_text(json.dumps(manifest, indent=2) + "\n", + encoding="utf-8") + print(f"[L149MouthCaptureSummary] views={len(JOINTS)} comparisons={len(manifest)//2} " + f"quality_failures={len(failures)} status={'PASS' if not failures else 'FAIL'}", + flush=True) + return 0 if not failures else 1 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except Exception as error: + print(f"[L149MouthCaptureFailure] {error}", file=sys.stderr, flush=True) + raise SystemExit(1) diff --git a/Standalone/Research/WorldAssemblerM3/Linux149/RunL149SeedSweep.py b/Standalone/Research/WorldAssemblerM3/Linux149/RunL149SeedSweep.py new file mode 100644 index 0000000..aacfd6e --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux149/RunL149SeedSweep.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +"""Run the L149 composed passage gates across the 16 owner seeds.""" +from __future__ import annotations + +import argparse +import json +import re +import subprocess +import time +from pathlib import Path + +from MouthAudit import audit_layout + + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l149/seeds" +BIN = ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-l149-clang" +DATA = ROOT / "Standalone/data/owner_world.json" +STAMP = "19dbe26ab76a629baadc54bb0ab93eaa22d6f878e6946df2b71509c12c9524e4" +STAMP_RE = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]+)$", re.M) +BASELINES = { + "tunnel-network": ROOT / "Saved/Linux/l135/optimized-seed-sweep.json", + "vertical-shafts": ROOT / "Saved/Linux/l139/seeds/summary.json", +} +DISPATCH = { + "tunnel-network": re.compile( + r"^\[DensityDispatch\] strate=2 generator=TunnelNetwork " + r"recipe=Core\.Caves\.PrimitiveField version=1 " + r"bound_path=Caves-native adapter=FCavesFieldAdapter$", re.M), + "vertical-shafts": re.compile( + r"^\[DensityDispatch\] strate=3 generator=VerticalShafts " + r"recipe=Core\.Caves\.PrimitiveField version=1 " + r"bound_path=Caves-native adapter=FCavesFieldAdapter$", re.M), +} +ROUTES = { + "tunnel-network": re.compile( + r"^\[ResolvedStrateRouteAudit\] strate=2 .*unsupported=0 " + r"grade_failures=0 .*status=PASS$", re.M), + "vertical-shafts": re.compile( + r"^\[ResolvedStrateRouteAudit\] strate=3 .*unsupported=0 " + r"grade_failures=0 .*status=PASS$", re.M), +} +CONTROLS = { + "tunnel-network": re.compile( + r"^\[TunnelNetworkBlockedRouteControl\].*expected=FAIL status=PASS$", re.M), + "vertical-shafts": re.compile( + r"^\[VerticalShaftsBlockedRouteControl\].*expected=FAIL status=PASS$", re.M), +} +REQUIRED = { + "passages": re.compile(r"^\[ResolvedPassageAuditSummary\].*status=PASS$", re.M), + "seams": re.compile(r"^\[PassageTileSeamAudit\].*status=PASS$", re.M), + "shell": re.compile(r"^\[PassageShellAuditSummary\].*status=PASS$", re.M), + "naturalness": re.compile(r"^\[PassageNaturalnessSummary\].*status=PASS$", re.M), + "mouth_profile": re.compile(r"^\[PassageMouthProfileSummary\].*status=PASS$", re.M), + "ripple": re.compile(r"^\[PassageRippleAuditSummary\].*status=PASS$", re.M), + "passage_interior": re.compile( + r"^\[PassageInteriorCapsuleAuditSummary\].*status=PASS$", re.M), +} + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--kind", choices=tuple(BASELINES), required=True) + args = parser.parse_args() + if not BIN.is_file() or not DATA.is_file(): + raise SystemExit("missing L149 Clang binary or composed owner fixture") + baseline_path = BASELINES[args.kind] + baseline = json.loads(baseline_path.read_text(encoding="utf-8")) + baseline_results = {int(row["seed"]): row for row in baseline["results"]} + mode_out = OUT / args.kind + mode_out.mkdir(parents=True, exist_ok=True) + results = [] + partial = mode_out / "runs-partial.json" + for seed in range(16): + log = mode_out / f"seed{seed}.log" + layout = mode_out / f"seed{seed}.json" + command = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(BIN), str(DATA), "--world-assembler-prototype", + "--stream-benchmark", "--stream-pipeline", "--workers=8", "--clip-radius=3", + "--max-clip-level=8", "--max-level=0", "--force-generate", f"--seed={seed}", + "--recipe=surface-world", "--density-dispatch-report", + "--audit-resolved-layout-passages", "--audit-passage-tile-seams", + f"--resolved-layout-output={layout}"] + started = time.monotonic() + with log.open("w", encoding="utf-8") as output: + proc = subprocess.run(command, cwd=ROOT, stdout=output, + stderr=subprocess.STDOUT, check=False, timeout=1740) + content = log.read_text(encoding="utf-8", errors="replace") + stamp = STAMP_RE.search(content) + checks = {name: bool(pattern.search(content)) for name, pattern in REQUIRED.items()} + checks["process_exit_zero"] = proc.returncode == 0 + checks["source_stamp"] = stamp is not None and stamp.group(1) == STAMP + checks["dispatch"] = bool(DISPATCH[args.kind].search(content)) + checks["route"] = bool(ROUTES[args.kind].search(content)) + checks["blocked_route_control"] = bool(CONTROLS[args.kind].search(content)) + checks["three_passage_interiors"] = len(re.findall( + r"^\[ResolvedPassageAudit\].*interior_air=PASS", content, re.M)) == 3 + checks["three_interior_negative_controls"] = len(re.findall( + r"^\[ResolvedPassageInteriorNegativeControl\].*detected=true status=PASS$", + content, re.M)) == 3 + checks["three_ripple_gap_controls"] = len(re.findall( + r"^\[PassageRippleSamplingControl\].*status=PASS$", content, re.M)) == 3 + checks["three_blocked_passage_controls"] = len(re.findall( + r"^\[ResolvedPassageBlockedControl\].*detected=true$", content, re.M)) == 3 + checks["passage_count"] = len(re.findall( + r"^\[ResolvedPassageAudit\].*walkability=PASS$", content, re.M)) == 3 + checks["layout_output"] = layout.is_file() + mouth_audit = audit_layout(layout) if layout.is_file() else None + checks["mouth_continuity"] = bool(mouth_audit and mouth_audit["continuity"]) + checks["no_backtrack_12m"] = bool(mouth_audit and mouth_audit["no_backtrack_12m"]) + checks["mouth_audit_negative_controls"] = bool( + mouth_audit and mouth_audit["negative_controls_pass"]) + passed = all(checks.values()) + prior = baseline_results.get(seed, {}) + prior_passed = bool(prior.get("passed", False)) + change = ("newly failing" if prior_passed and not passed else + "newly passing" if not prior_passed and passed else + "still passing" if prior_passed else "still failing") + failed_checks = [name for name, ok in checks.items() if not ok] + reason = ", ".join(failed_checks) if failed_checks else "all requested gates pass" + row = {"kind": args.kind, "seed": seed, + "prior_verdict": "PASS" if prior_passed else "FAIL", + "verdict": "PASS" if passed else "FAIL", "change": change, + "reason": reason, "exit": proc.returncode, "checks": checks, + "mouth_audit": mouth_audit, + "elapsed_s": round(time.monotonic() - started, 3), + "log": str(log.relative_to(ROOT)), "layout": str(layout.relative_to(ROOT))} + results.append(row) + print("[L149SeedSweep] " + json.dumps(row, sort_keys=True), flush=True) + partial.write_text(json.dumps(results, indent=2) + "\n", encoding="utf-8") + passed_count = sum(row["verdict"] == "PASS" for row in results) + summary = {"kind": args.kind, "source_stamp": STAMP, + "baseline": str(baseline_path.relative_to(ROOT)), "seeds": list(range(16)), + "passed": passed_count, "failed": 16 - passed_count, "results": results} + (mode_out / "summary.json").write_text(json.dumps(summary, indent=2) + "\n", + encoding="utf-8") + print(f"[L149SeedSweepSummary] kind={args.kind} passed={passed_count}/16 " + f"failed={16-passed_count} status={'PASS' if passed_count == 16 else 'FAIL'}", + flush=True) + return 0 if passed_count == 16 else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux149/__pycache__/MouthAudit.cpython-313.pyc b/Standalone/Research/WorldAssemblerM3/Linux149/__pycache__/MouthAudit.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1021ea888949e35e4074013db659a43ab69bba79 GIT binary patch literal 11956 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 literal 0 HcmV?d00001 diff --git a/Standalone/Research/WorldAssemblerM3/Linux149/captures/after-seed0-tunnel-network-route-edge-0.jpg b/Standalone/Research/WorldAssemblerM3/Linux149/captures/after-seed0-tunnel-network-route-edge-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1e5e37e40b713d70135cd67efa3140d773c468fd GIT binary patch literal 18837 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 literal 0 HcmV?d00001 diff --git a/Standalone/Research/WorldAssemblerM3/Linux149/captures/before-seed2-vertical-shafts-route-edge-0.jpg b/Standalone/Research/WorldAssemblerM3/Linux149/captures/before-seed2-vertical-shafts-route-edge-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9ba5ec405c0f4d19142b82c6abd02ca903e6b168 GIT binary patch literal 21112 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 BF1`Q& literal 0 HcmV?d00001 diff --git a/Standalone/Research/WorldAssemblerM3/Linux149/captures/manifest.json b/Standalone/Research/WorldAssemblerM3/Linux149/captures/manifest.json new file mode 100644 index 0000000..0c21e83 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux149/captures/manifest.json @@ -0,0 +1,122 @@ +[ + { + "phase": "before", + "view": "seed0-tunnel-network-route-edge-0", + "file": "Standalone/Research/WorldAssemblerM3/Linux149/captures/before-seed0-tunnel-network-route-edge-0.jpg", + "bytes": 18133, + "camera_m": [ + 17.237675178355673, + -14.73076399027466, + -752.9040234375 + ], + "target_m": [ + 28.807717858221643, + -11.547351923626703, + -752.9040234375 + ], + "yaw_deg": 74.61615105967402, + "pitch_deg": 0.0, + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux149/captures/before-seed0-tunnel-network-route-edge-0.jpg pixels=921600 mean_luma=129.283 stddev_luma=14.699 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "phase": "before", + "view": "seed0-tunnel-network-departure-edge", + "file": "Standalone/Research/WorldAssemblerM3/Linux149/captures/before-seed0-tunnel-network-departure-edge.jpg", + "bytes": 18526, + "camera_m": [ + -21.497510185619362, + -3.67525285819352, + -811.9255078125 + ], + "target_m": [ + -12.887449071903188, + -12.033891959032399, + -811.9255078125 + ], + "yaw_deg": 134.15112141809087, + "pitch_deg": 0.0, + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux149/captures/before-seed0-tunnel-network-departure-edge.jpg pixels=921600 mean_luma=110.748 stddev_luma=4.561 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "phase": "before", + "view": "seed2-vertical-shafts-route-edge-0", + "file": "Standalone/Research/WorldAssemblerM3/Linux149/captures/before-seed2-vertical-shafts-route-edge-0.jpg", + "bytes": 21112, + "camera_m": [ + -17.783733421365206, + -4.912768125994757, + -888.10421875 + ], + "target_m": [ + -15.817661018173972, + -16.750612495026218, + -888.10421875 + ], + "yaw_deg": 170.57018242389748, + "pitch_deg": 0.0, + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux149/captures/before-seed2-vertical-shafts-route-edge-0.jpg pixels=921600 mean_luma=129.471 stddev_luma=14.369 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "phase": "after", + "view": "seed0-tunnel-network-route-edge-0", + "file": "Standalone/Research/WorldAssemblerM3/Linux149/captures/after-seed0-tunnel-network-route-edge-0.jpg", + "bytes": 18837, + "camera_m": [ + 17.237675178355673, + -14.73076399027466, + -752.9040234375 + ], + "target_m": [ + 28.807717858221643, + -11.547351923626703, + -752.9040234375 + ], + "yaw_deg": 74.61615105967402, + "pitch_deg": 0.0, + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux149/captures/after-seed0-tunnel-network-route-edge-0.jpg pixels=921600 mean_luma=126.893 stddev_luma=13.032 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "phase": "after", + "view": "seed0-tunnel-network-departure-edge", + "file": "Standalone/Research/WorldAssemblerM3/Linux149/captures/after-seed0-tunnel-network-departure-edge.jpg", + "bytes": 19137, + "camera_m": [ + -21.497510185619362, + -3.67525285819352, + -811.9255078125 + ], + "target_m": [ + -12.887449071903188, + -12.033891959032399, + -811.9255078125 + ], + "yaw_deg": 134.15112141809087, + "pitch_deg": 0.0, + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux149/captures/after-seed0-tunnel-network-departure-edge.jpg pixels=921600 mean_luma=112.098 stddev_luma=7.992 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "phase": "after", + "view": "seed2-vertical-shafts-route-edge-0", + "file": "Standalone/Research/WorldAssemblerM3/Linux149/captures/after-seed2-vertical-shafts-route-edge-0.jpg", + "bytes": 20515, + "camera_m": [ + -17.783733421365206, + -4.912768125994757, + -888.10421875 + ], + "target_m": [ + -15.817661018173972, + -16.750612495026218, + -888.10421875 + ], + "yaw_deg": 170.57018242389748, + "pitch_deg": 0.0, + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux149/captures/after-seed2-vertical-shafts-route-edge-0.jpg pixels=921600 mean_luma=110.186 stddev_luma=6.663 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + } +] diff --git a/Standalone/Research/WorldAssemblerM3/Linux149/l149-one-tube-mouth.patch b/Standalone/Research/WorldAssemblerM3/Linux149/l149-one-tube-mouth.patch new file mode 100644 index 0000000..8f442da --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux149/l149-one-tube-mouth.patch @@ -0,0 +1,629 @@ +diff --git a/Standalone/Core/include/VoxelForgeCore/PassageSpline.h b/Standalone/Core/include/VoxelForgeCore/PassageSpline.h +index 24c68ab..c8610eb 100644 +--- a/Standalone/Core/include/VoxelForgeCore/PassageSpline.h ++++ b/Standalone/Core/include/VoxelForgeCore/PassageSpline.h +@@ -13,6 +13,7 @@ namespace VoxelForgeCore::PassageSpline + using FPoint = std::array; + inline constexpr std::size_t ArcTableSteps = 32; + inline constexpr double UpperMouthFloorBlendRadiusMetres = 3.5; ++ inline constexpr double FlatFloorCornerBlendMetres = 0.5; + + struct FSegment + { +@@ -60,6 +61,36 @@ namespace VoxelForgeCore::PassageSpline + return result; + } + ++ inline FPoint EvaluateRawDerivative(std::span controls, ++ std::size_t span,double t,double unitsPerMetre) ++ { ++ const auto& p1=controls[span]; ++ const auto& p2=controls[span+1]; ++ const auto& p0=controls[span==0?0:span-1]; ++ const auto& p3=controls[std::min(span+2,controls.size()-1)]; ++ const double dt0=KnotSpan(p0,p1,unitsPerMetre); ++ const double dt1=KnotSpan(p1,p2,unitsPerMetre); ++ const double dt2=KnotSpan(p2,p3,unitsPerMetre); ++ const double t2=t*t; ++ const double h00=6.0*t2-6.0*t; ++ const double h01=-h00; ++ const double h10=3.0*t2-4.0*t+1.0; ++ const double h11=3.0*t2-2.0*t; ++ FPoint result{}; ++ for(const std::size_t axis:{0u,1u,3u}) ++ { ++ const double m1=dt1*((p1[axis]-p0[axis])/dt0 ++ -(p2[axis]-p0[axis])/(dt0+dt1) ++ +(p2[axis]-p1[axis])/dt1); ++ const double m2=dt1*((p2[axis]-p1[axis])/dt1 ++ -(p3[axis]-p1[axis])/(dt1+dt2) ++ +(p3[axis]-p2[axis])/dt2); ++ result[axis]=h00*p1[axis]+h01*p2[axis]+h10*m1+h11*m2; ++ } ++ result[2]=p2[2]-p1[2]; ++ return result; ++ } ++ + inline bool ClosestRouteHeightAtXY(std::span> route, + double x,double y,double& height,double& distanceSquared) + { +diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +index 8c44461..e712747 100644 +--- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h ++++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +@@ -511,7 +511,8 @@ namespace VoxelForgeCore + // the planned graph route, preserves its walk corridor through shell overlaps, and keeps + // boundary-blended host rock from widening the authored passage wall. + // v237 adds the shared Core cave-graph shaft primitive and basal terminal route. +- inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 237; ++ // v238 joins cave edges to passage mouth tubes with matched geometry and clearance. ++ inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 238; + + enum class FieldChannel : std::uint32_t + { +diff --git a/Standalone/Core/src/ResolvedLayoutInternal.h b/Standalone/Core/src/ResolvedLayoutInternal.h +index 7b7520a..095418b 100644 +--- a/Standalone/Core/src/ResolvedLayoutInternal.h ++++ b/Standalone/Core/src/ResolvedLayoutInternal.h +@@ -2367,8 +2367,7 @@ bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout, + constexpr std::int64_t MinimumBoreRadius=1750000; + constexpr std::int64_t MinimumBoreHeight=2200000; + constexpr std::int64_t PlannedBoreHeight=4500000; +- constexpr std::int64_t MouthOverlap=2000000; +- constexpr std::int64_t PortalLeadLength=6000000; ++ constexpr std::int64_t PortalLeadLength=1000000; + constexpr std::int64_t MaximumBoreRadius=10000000; + constexpr std::int64_t MaximumWalkGradeDenominator=5; // 1:5 = 11.31 degrees. + const auto integerSqrt=[](std::uint64_t value) +@@ -2394,171 +2393,77 @@ bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout, + const auto qToMicrometres=[&](std::int64_t q) + {return q*static_cast(layout.coreVoxelSizeMicrometres) + /WorldAssemblySubvoxelsPerVoxel;}; +- const std::int64_t mouthBoreRadius=std::clamp( +- std::max(MinimumBoreRadius,settings.minimumTunnelRadiusMicrometres), +- MinimumBoreRadius,MaximumBoreRadius); +- const auto chooseMouthRouteTangent=[&](const ResolvedLayoutPassageV1& passage, +- std::size_t mouthIndex,bool lowerSide, +- const std::array& authoredRouteTangentQ16, +- std::int64_t authoredFloorSlopeQ16,const FPoint& mouthFloorPoint) ++ std::int64_t mouthBoreRadius=std::max(MinimumBoreRadius, ++ settings.minimumTunnelRadiusMicrometres); ++ const auto includePassageMouthRadius=[&]( ++ const ResolvedLayoutPassageV1* passage,bool lowerSide) + { +- constexpr std::array,16> DirectionsQ16={{ +- {{65536,0}},{{60547,25080}},{{46341,46341}},{{25080,60547}}, +- {{0,65536}},{{-25080,60547}},{{-46341,46341}},{{-60547,25080}}, +- {{-65536,0}},{{-60547,-25080}},{{-46341,-46341}},{{-25080,-60547}}, +- {{0,-65536}},{{25080,-60547}},{{46341,-46341}},{{60547,-25080}}}}; +- const auto scoreDirection=[&](const std::array& outwardQ16) +- { +- const std::size_t segmentCount=passage.controlPoints.size()>1 +- ?passage.controlPoints.size()-1:0; +- std::int64_t worst=std::numeric_limits::max(); +- bool sampled=false; +- const std::int64_t projectedSlopeQ16=(authoredFloorSlopeQ16 +- *(outwardQ16[0]*authoredRouteTangentQ16[0] +- +outwardQ16[1]*authoredRouteTangentQ16[1]) +- /(65536ll*65536ll)); +- constexpr std::int64_t MouthClearanceProbeExtent=48000000; +- for(std::int64_t distance=2000000; +- distance<=MouthClearanceProbeExtent;distance+=2000000) +- { +- const std::int64_t x=mouthFloorPoint[0] +- +outwardQ16[0]*distance/65536; +- const std::int64_t y=mouthFloorPoint[1] +- +outwardQ16[1]*distance/65536; +- const std::int64_t caveFloor=mouthFloorPoint[2] +- +projectedSlopeQ16*distance/65536; +- const std::int64_t caveCeiling=caveFloor+PlannedBoreHeight; +- for(std::size_t segmentIndex=0;segmentIndex=mouthIndex)continue; +- if(!lowerSide&&segmentIndex<2)continue; +- const auto& a=passage.controlPoints[segmentIndex]; +- const auto& b=passage.controlPoints[segmentIndex+1]; +- const std::int64_t ax=qToMicrometres(a.positionSubvoxels[0]); +- const std::int64_t ay=qToMicrometres(a.positionSubvoxels[1]); +- const std::int64_t az=qToMicrometres(a.positionSubvoxels[2]); +- const std::int64_t bx=qToMicrometres(b.positionSubvoxels[0]); +- const std::int64_t by=qToMicrometres(b.positionSubvoxels[1]); +- const std::int64_t bz=qToMicrometres(b.positionSubvoxels[2]); +- const std::int64_t dx=bx-ax,dy=by-ay; +- const double lengthSquared=static_cast(dx)*dx +- +static_cast(dy)*dy; +- const double projection=lengthSquared>0.0 +- ?std::clamp((static_cast(x-ax)*dx +- +static_cast(y-ay)*dy)/lengthSquared,0.0,1.0):0.0; +- const std::int64_t tQ16=static_cast(std::llround( +- projection*65536.0)); +- const std::int64_t closestX=ax+dx*tQ16/65536; +- const std::int64_t closestY=ay+dy*tQ16/65536; +- const std::int64_t horizontalDistance=static_cast( +- integerSqrt(static_cast((x-closestX)*(x-closestX) +- +(y-closestY)*(y-closestY)))); +- const std::int64_t passageCenterZ=qToMicrometres( +- a.positionSubvoxels[2]) +- +(qToMicrometres(b.positionSubvoxels[2]) +- -qToMicrometres(a.positionSubvoxels[2]))*tQ16/65536; +- const std::int64_t passageRadius=qToMicrometres(a.radiusSubvoxels) +- +(qToMicrometres(b.radiusSubvoxels) +- -qToMicrometres(a.radiusSubvoxels))*tQ16/65536; +- const std::int64_t horizontalGap=horizontalDistance +- -passageRadius-mouthBoreRadius; +- const std::int64_t passageFloor=passageCenterZ-passageRadius; +- const std::int64_t passageCeiling=passageCenterZ+passageRadius; +- const std::int64_t verticalGap=caveFloor>passageCeiling +- ?caveFloor-passageCeiling +- :(passageFloor>caveCeiling?passageFloor-caveCeiling +- :-std::min(caveCeiling-passageFloor, +- passageCeiling-caveFloor)); +- const std::int64_t separation=horizontalGap>=0&&verticalGap>=0 +- ?static_cast(integerSqrt(static_cast( +- horizontalGap*horizontalGap+verticalGap*verticalGap))) +- :std::max(horizontalGap,verticalGap); +- worst=std::min(worst,separation);sampled=true; +- } +- } +- return sampled?worst:std::numeric_limits::max(); +- }; +- +- // Keep the authored tangent whenever it clears the other passage spans. +- // If the passage folds back over the mouth, choose the outward direction +- // with the greatest 3D clearance while preserving deterministic tie order. +- const std::array authoredOutwardQ16={ +- lowerSide?authoredRouteTangentQ16[0]:-authoredRouteTangentQ16[0], +- lowerSide?authoredRouteTangentQ16[1]:-authoredRouteTangentQ16[1]}; +- const std::int64_t authoredScore=scoreDirection(authoredOutwardQ16); +- if(authoredScore>=0)return authoredRouteTangentQ16; +- std::array bestOutwardQ16=authoredOutwardQ16; +- std::int64_t bestScore=authoredScore; +- std::int64_t bestAlignment=authoredOutwardQ16[0]*authoredOutwardQ16[0] +- +authoredOutwardQ16[1]*authoredOutwardQ16[1]; +- for(const auto& candidate:DirectionsQ16) +- { +- const std::int64_t score=scoreDirection(candidate); +- const std::int64_t alignment=candidate[0]*authoredOutwardQ16[0] +- +candidate[1]*authoredOutwardQ16[1]; +- if(score>bestScore||(score==bestScore&&alignment>bestAlignment)) +- {bestScore=score;bestAlignment=alignment;bestOutwardQ16=candidate;} +- } +- std::printf("[CavernMouthClearancePlan] passage=%s side=%s authored_clearance_m=%.3f selected_clearance_m=%.3f outward_q16=(%lld,%lld)\n", +- passage.stableId.c_str(),lowerSide?"lower":"upper", +- authoredScore/1.0e6,bestScore/1.0e6, +- static_cast(bestOutwardQ16[0]), +- static_cast(bestOutwardQ16[1])); +- return std::array{ +- lowerSide?bestOutwardQ16[0]:-bestOutwardQ16[0], +- lowerSide?bestOutwardQ16[1]:-bestOutwardQ16[1]}; ++ if(!passage||passage->controlPoints.empty())return; ++ const std::size_t index=lowerSide ++ ?std::min(passage->lowerMouthControlIndex,passage->controlPoints.size()-1):0; ++ mouthBoreRadius=std::max(mouthBoreRadius, ++ qToMicrometres(passage->controlPoints[index].radiusSubvoxels)); + }; ++ includePassageMouthRadius(arrivalPassage,true); ++ includePassageMouthRadius(departurePassage,false); ++ mouthBoreRadius=std::clamp(mouthBoreRadius,MinimumBoreRadius,MaximumBoreRadius); + const auto mouthInnerPoint=[&](const ResolvedLayoutPassageV1& passage, + bool lowerSide,FPoint& point,std::array& routeTangentQ16, + std::int64_t& floorSlopeQ16) + { +- for(std::size_t axis=0;axis<3;++axis) +- point[axis]=qToMicrometres(lowerSide?passage.lowerMouthSubvoxels[axis] +- :passage.upperMouthSubvoxels[axis]); + if(passage.controlPoints.size()<2)return false; + const std::size_t mouthIndex=lowerSide + ?std::min(passage.lowerMouthControlIndex,passage.controlPoints.size()-1):0; +- const std::size_t tangentIndex=lowerSide +- ?(mouthIndex>0?mouthIndex-1:1):1; +- // The lower arrival continues from the previous passage point through its +- // mouth. The upper departure approaches its mouth from the cave and then +- // continues toward control point 1, so the two route tangents have opposite +- // signs even though both portal offsets lie on the cave side. ++ if(lowerSide&&mouthIndex==0)return false; ++ const std::size_t span=lowerSide?mouthIndex-1:0; ++ const double endpointT=lowerSide?1.0:0.0; + const std::int64_t direction=lowerSide?1:-1; +- const std::int64_t tx=direction*(passage.controlPoints[mouthIndex].positionSubvoxels[0] +- -passage.controlPoints[tangentIndex].positionSubvoxels[0]); +- const std::int64_t ty=direction*(passage.controlPoints[mouthIndex].positionSubvoxels[1] +- -passage.controlPoints[tangentIndex].positionSubvoxels[1]); +- const auto tangentLength=static_cast(integerSqrt( +- static_cast(tx*tx+ty*ty))); +- if(tangentLength<=0)return false; +- routeTangentQ16={tx*65536/tangentLength,ty*65536/tangentLength}; ++ std::vector splineControls; ++ std::vector floorProfile; ++ splineControls.reserve(passage.controlPoints.size()); ++ floorProfile.reserve(passage.controlPoints.size()); ++ for(const auto& control:passage.controlPoints) ++ { ++ const std::int64_t x=qToMicrometres(control.positionSubvoxels[0]); ++ const std::int64_t y=qToMicrometres(control.positionSubvoxels[1]); ++ const std::int64_t z=qToMicrometres(control.positionSubvoxels[2]); ++ const std::int64_t radius=qToMicrometres(control.radiusSubvoxels); ++ splineControls.push_back({static_cast(x),static_cast(y), ++ static_cast(z),static_cast(radius)}); ++ floorProfile.push_back(static_cast(z-radius)); ++ } ++ auto derivative=VoxelForgeCore::PassageSpline::EvaluateRawDerivative( ++ splineControls,span,endpointT,MicrometresPerMetre); ++ if(!lowerSide) ++ { ++ derivative[0]=-derivative[0]; ++ derivative[1]=-derivative[1]; ++ } ++ const double tangentLength=std::hypot(derivative[0],derivative[1]); ++ if(!(tangentLength>1.0e-9)||!std::isfinite(tangentLength))return false; ++ routeTangentQ16={ ++ static_cast(std::llround(derivative[0]*65536.0/tangentLength)), ++ static_cast(std::llround(derivative[1]*65536.0/tangentLength))}; ++ const auto passageSegments=VoxelForgeCore::PassageSpline::BuildSegments( ++ splineControls,splineControls.size()-1,2.0,MicrometresPerMetre, ++ floorProfile); ++ double spanHorizontalLengthMicrometres=0.0; ++ for(const auto& segment:passageSegments) ++ if(segment.controlSpan==span) ++ spanHorizontalLengthMicrometres+=std::hypot( ++ segment.b[0]-segment.a[0],segment.b[1]-segment.a[1]); ++ if(!(spanHorizontalLengthMicrometres>1.0e-9))return false; + const auto& mouthControl=passage.controlPoints[mouthIndex]; +- const auto& tangentControl=passage.controlPoints[tangentIndex]; + const std::int64_t mouthFloor=mouthControl.positionSubvoxels[2] + -mouthControl.radiusSubvoxels; +- const std::int64_t tangentFloor=tangentControl.positionSubvoxels[2] +- -tangentControl.radiusSubvoxels; +- // Resolved passage mouths already store the player center at floor + 1.13 m. +- // Cave edge control points store the floor itself, so start from the passage +- // control's floor rather than reusing the player-center mouth height. ++ const std::size_t tangentIndex=lowerSide?mouthIndex-1:1; ++ const double passageFloorDelta=direction*(floorProfile[mouthIndex] ++ -floorProfile[tangentIndex]); ++ const double slope=passageFloorDelta/spanHorizontalLengthMicrometres; ++ floorSlopeQ16=static_cast(std::llround(slope*65536.0)); ++ point[0]=qToMicrometres(mouthControl.positionSubvoxels[0]); ++ point[1]=qToMicrometres(mouthControl.positionSubvoxels[1]); + point[2]=qToMicrometres(mouthFloor); +- const std::int64_t passageFloorDelta=direction*(mouthFloor-tangentFloor); +- floorSlopeQ16=passageFloorDelta*65536/tangentLength; +- const auto authoredRouteTangentQ16=routeTangentQ16; +- routeTangentQ16=chooseMouthRouteTangent(passage,mouthIndex,lowerSide, +- authoredRouteTangentQ16,floorSlopeQ16,point); +- const std::int64_t tangentAlignment=routeTangentQ16[0] +- *authoredRouteTangentQ16[0]+routeTangentQ16[1] +- *authoredRouteTangentQ16[1]; +- floorSlopeQ16=floorSlopeQ16*tangentAlignment/(65536ll*65536ll); +- const std::int64_t offsetDirection=lowerSide?1:-1; +- point[0]+=offsetDirection*routeTangentQ16[0]*MouthOverlap/65536; +- point[1]+=offsetDirection*routeTangentQ16[1]*MouthOverlap/65536; +- point[2]+=offsetDirection*floorSlopeQ16*MouthOverlap/65536; + return true; + }; + FPoint routeStart{},routeEnd{}; +@@ -2738,7 +2643,7 @@ bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout, + controls.size()-1,2.0,1000000.0); + const double totalLength=segments.empty()?0.0: + segments.back().alongMetres+segments.back().lengthMetres; +- constexpr double PortalExclusionMetres=10.0; ++ constexpr double PortalExclusionMetres=2.0; + for(const auto& segment:segments) + { + const double midpoint=segment.alongMetres+segment.lengthMetres*0.5; +@@ -2815,7 +2720,7 @@ bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout, + if(routePassageClearanceSpans.empty()||points.size()<2) + return std::numeric_limits::infinity(); + const double totalLength=routeLength(points)/1.0e6; +- constexpr double PortalExclusionMetres=10.0; ++ constexpr double PortalExclusionMetres=2.0; + double minimumClearance=std::numeric_limits::infinity(); + double along=0.0; + for(std::size_t index=1;index& tangentQ16,std::int64_t floorSlopeQ16, ++ std::int64_t profileStartDistance,std::int64_t profileEndDistance) ++ { ++ const std::size_t mouthIndex=lowerSide ++ ?std::min(passage.lowerMouthControlIndex,passage.controlPoints.size()-1):0; ++ const auto& mouth=passage.controlPoints[mouthIndex]; ++ const std::int64_t mouthRadius=qToMicrometres(mouth.radiusSubvoxels); ++ auto edge=makeEdge(std::move(id),std::move(startAnchor),start,endNode, ++ mouthRadius,profileStartDistance,profileEndDistance); ++ edge.boreRadiusMicrometres=mouthRadius; ++ edge.boreHeightMicrometres=2*mouthRadius; ++ edge.smoothWidthMicrometres=static_cast(std::llround( ++ VoxelForgeCore::PassageSpline::FlatFloorCornerBlendMetres ++ *MicrometresPerMetre)); ++ edge.unionWidthMicrometres=smooth; ++ const FPoint end={endNode.point[0],endNode.point[1],endNode.point[2]}; ++ const std::int64_t horizontalRun=distance2d(start,end); ++ const std::int64_t controlHandle=std::max(1, ++ std::min(PortalLeadLength,std::max(1,horizontalRun))/3); ++ edge.controlPointsMicrometres[1][0]=start[0] ++ +tangentQ16[0]*controlHandle/65536; ++ edge.controlPointsMicrometres[1][1]=start[1] ++ +tangentQ16[1]*controlHandle/65536; ++ edge.controlPointsMicrometres[1][2]=start[2] ++ +floorSlopeQ16*controlHandle/65536; ++ return edge; ++ }; + struct FPlannedShaftLanding + { + FRoomNode node; +@@ -3396,9 +3331,13 @@ bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout, + { + const auto& node=nodes[nodeIndex]; + const std::string edgeId=routePrefix+"route-edge-"+std::to_string(nodeIndex); +- auto edge=makeEdge(edgeId,previousAnchor,previousPoint,node, +- selectedBoreRadius(edgeId), +- previousProfileDistance,node.floorProfileDistanceMicrometres); ++ auto edge=nodeIndex==0 ++ ?makePassageMouthEdge(edgeId,previousAnchor,previousPoint,node, ++ *arrivalPassage,true,arrivalTangentQ16,arrivalFloorSlopeQ16, ++ previousProfileDistance,node.floorProfileDistanceMicrometres) ++ :makeEdge(edgeId,previousAnchor,previousPoint,node, ++ selectedBoreRadius(edgeId),previousProfileDistance, ++ node.floorProfileDistanceMicrometres); + edges.push_back(std::move(edge)); + previousPoint={node.point[0],node.point[1],node.point[2]}; + previousProfileDistance=node.floorProfileDistanceMicrometres; +@@ -3407,8 +3346,9 @@ bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout, + if(!nodes.empty()&&departurePassage) + { + const std::string departureEdgeId=routePrefix+"departure-edge"; +- auto edge=makeEdge(departureEdgeId,departurePassage->stableId+"/upper-mouth", +- routeEnd,nodes.back(),selectedBoreRadius(departureEdgeId), ++ auto edge=makePassageMouthEdge(departureEdgeId, ++ departurePassage->stableId+"/upper-mouth",routeEnd,nodes.back(), ++ *departurePassage,false,departureTangentQ16,departureFloorSlopeQ16, + totalRouteLength,nodes.back().floorProfileDistanceMicrometres); + edges.push_back(std::move(edge)); + } +diff --git a/Standalone/Core/src/ShapeWorldInternal.h b/Standalone/Core/src/ShapeWorldInternal.h +index 1a57f85..d17aa02 100644 +--- a/Standalone/Core/src/ShapeWorldInternal.h ++++ b/Standalone/Core/src/ShapeWorldInternal.h +@@ -4443,11 +4443,12 @@ private: + buildSegmentNode(buildSegmentNode,0,runtime.segmentOrder.size()); + const double radius=edge.boreRadiusMicrometres/1000000.0; + const double height=edge.boreHeightMicrometres/1000000.0; ++ const bool passageMouthTube=IsPlannerPassageMouthContinuationEdge(edge); + if(radius>0.0&&height>0.0) + for(auto& node:runtime.segmentNodes) + for(std::size_t axis=0;axis<3;++axis) + { +- const double scale=axis==2?height:radius; ++ const double scale=axis==2&&!passageMouthTube?height:radius; + node.minimumNormalized[axis]=node.minimum[axis]/scale; + node.maximumNormalized[axis]=node.maximum[axis]/scale; + } +@@ -5728,6 +5729,8 @@ public: + } + else if(feature.edge&&!feature.segments.empty()) + { ++ if(IsPlannerPassageMouthContinuationEdge(*feature.edge)) ++ return {0.0,0.0,false}; + const double radius=feature.edge->boreRadiusMicrometres/1000000.0; + const double height=feature.edge->boreHeightMicrometres/1000000.0; + if(!(radius>0.0&&height>0.0))return {0.0,0.0,false}; +@@ -7832,6 +7835,14 @@ private: + return composed; + } + ++ static bool IsPlannerPassageMouthContinuationEdge( ++ const VoxelForgeCore::ResolvedCavernEdgeV1& edge) ++ { ++ const bool sharedPlannerEdge=edge.stableId.starts_with("tunnel-network/") ++ ||edge.stableId.starts_with("vertical-shafts/"); ++ return sharedPlannerEdge&&edge.startAnchorStableId.ends_with("-mouth"); ++ } ++ + static double SmoothMinMetres(double a,double b,double width) + { + if(!(width>0.0))return std::min(a,b); +@@ -7997,9 +8008,11 @@ private: + double nearestFloor=0.0,nearestGrade=0.0,nearestRadial=0.0; + const double radius=feature.edge->boreRadiusMicrometres/1000000.0; + const double height=feature.edge->boreHeightMicrometres/1000000.0; ++ const bool passageMouthTube= ++ IsPlannerPassageMouthContinuationEdge(*feature.edge); + constexpr double SegmentDistanceTieToleranceMetres=1.0e-12; + if(!(radius>0.0&&height>0.0))continue; +- const double minScale=std::min(radius,height); ++ const double minScale=passageMouthTube?radius:std::min(radius,height); + std::uint32_t nearestSegment=std::numeric_limits::max(); + const auto evaluateSegment=[&](std::uint32_t segmentIndex) + { +@@ -8010,7 +8023,9 @@ private: + { + const double delta=segment.end[axis]-segment.start[axis]; + lengthSquared+=delta*delta; +- dot+=(point[axis]-segment.start[axis])*delta; ++ const double query=axis==2&&passageMouthTube ++ ?point[axis]-radius:point[axis]; ++ dot+=(query-segment.start[axis])*delta; + } + const double t=lengthSquared>0.0 + ?std::clamp(dot/lengthSquared,0.0,1.0):0.0; +@@ -8020,11 +8035,12 @@ private: + const double delta=point[axis]-(segment.start[axis] + +t*(segment.end[axis]-segment.start[axis])); + if(axis<2)radialSquared+=delta*delta; +- else vertical=delta; ++ else vertical=delta-(passageMouthTube?radius:0.0); + } ++ const double verticalScale=passageMouthTube?radius:height; + const double normalized=std::sqrt(radialSquared/(radius*radius) +- +vertical*vertical/(height*height)); +- double distance=(normalized-1.0)*std::min(radius,height); ++ +vertical*vertical/(verticalScale*verticalScale)); ++ double distance=(normalized-1.0)*minScale; + if(feature.irregularityAmplitudeVoxels>0.0f) + { + const float floorVoxels=static_cast(nearestFloor/metresPerVoxel); +@@ -8049,8 +8065,10 @@ private: + ?(segment.end[2]-segment.start[2])/horizontal:0.0; + } + }; ++ const double verticalScale=passageMouthTube?radius:height; + const std::array normalizedPoint={ +- point[0]/radius,point[1]/radius,point[2]/height}; ++ point[0]/radius,point[1]/radius, ++ (point[2]-(passageMouthTube?radius:0.0))/verticalScale}; + const auto nodeLowerBound=[&](const FCavernRuntimeFeature::FSegmentNode& node) + { + double gapSquared=0.0; +diff --git a/Standalone/Source/WorldAssemblyPrototype.h b/Standalone/Source/WorldAssemblyPrototype.h +index 4e20068..0c3cbb7 100644 +--- a/Standalone/Source/WorldAssemblyPrototype.h ++++ b/Standalone/Source/WorldAssemblyPrototype.h +@@ -1732,6 +1732,44 @@ namespace StandaloneWorldAssemblyPrototype + if(std::abs(a[axis]-b[axis])>2)return false; + return true; + }; ++ const auto pointAlongEdge=[&](const ResolvedCavernEdgeV1& edge, ++ double distanceFromStart) ++ { ++ const auto evaluate=[&](double t) ++ { ++ const double one=1.0-t; ++ const std::array weights={one*one*one, ++ 3.0*one*one*t,3.0*one*t*t,t*t*t}; ++ std::array point{}; ++ for(std::size_t control=0;control<4;++control) ++ for(std::size_t axis=0;axis<3;++axis) ++ point[axis]+=weights[control] ++ *edge.controlPointsMicrometres[control][axis]; ++ return point; ++ }; ++ auto previous=evaluate(0.0); ++ double travelled=0.0; ++ constexpr int Samples=256; ++ for(int sample=1;sample<=Samples;++sample) ++ { ++ const auto next=evaluate(static_cast(sample)/Samples); ++ const double segmentLength=std::hypot(next[0]-previous[0], ++ next[1]-previous[1])/1000000.0; ++ if(travelled+segmentLength>=distanceFromStart ++ &&segmentLength>1.0e-9) ++ { ++ const double t=std::clamp((distanceFromStart-travelled) ++ /segmentLength,0.0,1.0); ++ std::array point{}; ++ for(std::size_t axis=0;axis<3;++axis) ++ point[axis]=std::lerp(previous[axis],next[axis],t); ++ return point; ++ } ++ travelled+=segmentLength; ++ previous=next; ++ } ++ return evaluate(1.0); ++ }; + auto start=arrival->lowerMouthSubvoxels; + appendRoutePoint("000-arrival",start); + for(std::size_t edgeIndex=0;edgeIndex<256;++edgeIndex) +@@ -1755,15 +1793,14 @@ namespace StandaloneWorldAssemblyPrototype + } + if(edgeIndex==0) + { +- auto inner=edge->controlPointsMicrometres.front(); +- // Keep the exact authored mouth anchor and use the planner +- // edge's floor height for the first cave-side walking point. +- // Passage composition uses these same two points to blend the +- // floor profile into the native cave route. +- inner[2]+=PlayerCenterAboveFloorMicrometres; ++ const auto edgeStart=pointAlongEdge(*edge,2.0); + const std::array point={ +- toCoreSubvoxels(inner[0]),toCoreSubvoxels(inner[1]), +- toCoreSubvoxels(inner[2])}; ++ toCoreSubvoxels(static_cast(std::llround(edgeStart[0]))), ++ toCoreSubvoxels(static_cast(std::llround(edgeStart[1]))), ++ toCoreSubvoxels(static_cast(std::llround(edgeStart[2] ++ +PlayerCenterAboveFloorMicrometres)))}; ++ // Keep a walking point on the actual edge curve so audits and ++ // passage floor blending follow the same mouth continuation. + if(!sameRoutePoint(point,start)) + appendRoutePoint("000-edge-start",point); + } +@@ -1783,11 +1820,12 @@ namespace StandaloneWorldAssemblyPrototype + &&value.stableId==departureEdgeId;}); + if(departureEdge==layout.cavernEdges.end()) + {error="native cave route lost its planned departure edge";return false;} +- auto inner=departureEdge->controlPointsMicrometres.front(); +- inner[2]+=PlayerCenterAboveFloorMicrometres; ++ const auto edgeInner=pointAlongEdge(*departureEdge,2.0); + const std::array innerPoint={ +- toCoreSubvoxels(inner[0]),toCoreSubvoxels(inner[1]), +- toCoreSubvoxels(inner[2])}; ++ toCoreSubvoxels(static_cast(std::llround(edgeInner[0]))), ++ toCoreSubvoxels(static_cast(std::llround(edgeInner[1]))), ++ toCoreSubvoxels(static_cast(std::llround(edgeInner[2] ++ +PlayerCenterAboveFloorMicrometres)))}; + auto end=departure->upperMouthSubvoxels; + if(!sameRoutePoint(innerPoint,end)) + appendRoutePoint("998-departure-inner",innerPoint); +diff --git a/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-floating-islands-style2-v238.baseline b/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-floating-islands-style2-v238.baseline +new file mode 100644 +index 0000000..98d4cc8 +--- /dev/null ++++ b/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-floating-islands-style2-v238.baseline +@@ -0,0 +1,12 @@ ++[PipelineFullDetailHash] tiles=960 density=0x91931a57a067fd92 mesh=0xa818654be89790ac payload=0x370ff80451105a39 topology=0x06e507557aa70173 ++[PipelineWholeViewHash] tiles=2370 density=0x7111b4b743e03c95 mesh=0x8c7240939db21b88 payload=0x3981ff5ca7076219 topology=0x07947d2e691b2e55 ++[PipelineLODHash] lod=0 tiles=512 density=0x2fd4c78685a43cfe mesh=0x50478cff1c1f8ca0 payload=0x54e48e57e0af26de topology=0x860c011cfdb9833a ++[PipelineLODHash] lod=1 tiles=448 density=0x4b8d9a6dcd2942ad mesh=0x17e89b7087bfe711 payload=0x2389c109eb780db6 topology=0x7eccd4cf560d0630 ++[PipelineLODHash] lod=2 tiles=320 density=0x48d4c21a852e6e21 mesh=0x5516a17618d142d5 payload=0x59cda38ab9ca6b7a topology=0x15ae353bd835e6a1 ++[PipelineLODHash] lod=3 tiles=208 density=0x09b4811098a48c8d mesh=0xbac3fb6d54bca8e9 payload=0xca779555d55be411 topology=0xc63e8ee46890a03a ++[PipelineLODHash] lod=4 tiles=240 density=0xfb11591772a3d18e mesh=0xebf2baee8764591b payload=0xe491d7ac9c0061ad topology=0xd618ac33b95ce494 ++[PipelineLODHash] lod=5 tiles=144 density=0x2f8b7b7fb1e67258 mesh=0x4cd508cbe97344fd payload=0x2b6e96a6050458c9 topology=0x4ff6d0eda8988573 ++[PipelineLODHash] lod=6 tiles=96 density=0x0114a34c8b8d6113 mesh=0xb1fec4d01ae1fa44 payload=0x3e50cfd429b8e950 topology=0x08090c96d15c1618 ++[PipelineLODHash] lod=7 tiles=96 density=0x3836ed8cfce73418 mesh=0x40bc6589c5fc1aac payload=0x677349bcae16790f topology=0x36e3aa53192e6a6b ++[PipelineLODHash] lod=8 tiles=306 density=0x144f3c1fd722708b mesh=0xef77b28bf7fd9453 payload=0xe3f47b37fcd61489 topology=0x5a3c73e295494e04 ++[PipelineExactQueryHash] probes=9 density=0x7b7c50ac24ccadcc unclamped=0x7b7c50ac24ccadcc detail=0xefcba20c0c70c415 collision=0xe604823a249029bf path=direct-Core-queries +diff --git a/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-surface-world-style1-v238.baseline b/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-surface-world-style1-v238.baseline +new file mode 100644 +index 0000000..e43aaf0 +--- /dev/null ++++ b/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-surface-world-style1-v238.baseline +@@ -0,0 +1,12 @@ ++[PipelineFullDetailHash] tiles=960 density=0xace56c8f205238ad mesh=0xa9c603a1248de6b8 payload=0xea55f1f4fcc6a894 topology=0x5ea7bc78a1b678ae ++[PipelineWholeViewHash] tiles=2370 density=0x826704417f556bb8 mesh=0x3dc31324d910ad2b payload=0x32cd1847a8e05d0a topology=0xb800610aa85336d7 ++[PipelineLODHash] lod=0 tiles=512 density=0xcd6ab28cfa504aca mesh=0xf03dc80a669fca31 payload=0x397aded1c3d2d297 topology=0xb4a1dc84fabc1fe3 ++[PipelineLODHash] lod=1 tiles=448 density=0xce6e713828b2a20a mesh=0x1cf408c25d67d18c payload=0x8a2b5d20571c997e topology=0x51cd985fe8e7d100 ++[PipelineLODHash] lod=2 tiles=320 density=0xd7f003d75d73e5b1 mesh=0x4e9c56918da506ae payload=0x3867c5915b409d6d topology=0x02a2a7c0ee9ac809 ++[PipelineLODHash] lod=3 tiles=208 density=0xce02d09814028b15 mesh=0xe4c2addc4c54c439 payload=0x7e25d9eee6469c5a topology=0xe603e61c9b9bb733 ++[PipelineLODHash] lod=4 tiles=240 density=0x0be8798407724eaf mesh=0x131c303cbbe51434 payload=0x2a90ecf0a2411ca0 topology=0xe3e054a371152e08 ++[PipelineLODHash] lod=5 tiles=144 density=0xf20897ad272103a9 mesh=0x13d00b6253ae1f72 payload=0x0db3880693a962fa topology=0x462bd87a80f15e89 ++[PipelineLODHash] lod=6 tiles=96 density=0xc040fcac457d6313 mesh=0x1ed132eeb718fb62 payload=0xcd0214e35577c6e3 topology=0x2c56c69e6fdcfd64 ++[PipelineLODHash] lod=7 tiles=96 density=0xc2d419446355fdb8 mesh=0x4ea44452574fc84b payload=0xfd9779bedfc9ae0f topology=0x8948546e770c9b72 ++[PipelineLODHash] lod=8 tiles=306 density=0x62db31696f2da831 mesh=0xd373f9e9481532fe payload=0x540a60526e0e3e46 topology=0xc725cb901e6dc335 ++[PipelineExactQueryHash] probes=9 density=0xc949c4df2ce6f9d9 unclamped=0xc949c4df2ce6f9d9 detail=0x114c501e2d830d94 collision=0xe6011d3a248d4849 path=direct-Core-queries +diff --git a/Standalone/Tools/verify/verify.py b/Standalone/Tools/verify/verify.py +index e97c250..12f4405 100644 +--- a/Standalone/Tools/verify/verify.py ++++ b/Standalone/Tools/verify/verify.py +@@ -125,11 +125,11 @@ RECIPE_RE = re.compile( + + def owner_hash_baseline(recipe: str, style: int) -> tuple[Path, str]: + if recipe == "surface-world" and style == 1: +- return (BASELINES / "owner-hash/canonical/owner-hash-surface-world-style1-v237.baseline", +- "L139-v237-vertical-shafts-core-planner") ++ return (BASELINES / "owner-hash/canonical/owner-hash-surface-world-style1-v238.baseline", ++ "L149-v238-passage-mouth-tube-joins") + if recipe == "floating-islands" and style == 2: +- return (BASELINES / "owner-hash/canonical/owner-hash-floating-islands-style2-v237.baseline", +- "L139-v237-vertical-shafts-core-planner") ++ return (BASELINES / "owner-hash/canonical/owner-hash-floating-islands-style2-v238.baseline", ++ "L149-v238-passage-mouth-tube-joins") + raise VerificationFailure(f"no pinned owner hash baseline for {recipe} style {style}") + +