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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
This commit is contained in:
Fr0zkaandClaude Opus 5.5 committed 2026-11-01 15:24:49 +01:00
1 parent f21aab775f
commit b8382eed84
14 files changed
+1374

No files matched your search

@@ -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}
@@ -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.
@@ -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())
@@ -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)
@@ -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())
Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

@@ -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"
}
]
@@ -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<double,4>;
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<const FPoint> 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<const std::array<double,3>> 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<std::int64_t>(layout.coreVoxelSizeMicrometres)
/WorldAssemblySubvoxelsPerVoxel;};
- const std::int64_t mouthBoreRadius=std::clamp<std::int64_t>(
- std::max(MinimumBoreRadius,settings.minimumTunnelRadiusMicrometres),
- MinimumBoreRadius,MaximumBoreRadius);
- const auto chooseMouthRouteTangent=[&](const ResolvedLayoutPassageV1& passage,
- std::size_t mouthIndex,bool lowerSide,
- const std::array<std::int64_t,2>& 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<std::array<std::int64_t,2>,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<std::int64_t,2>& outwardQ16)
- {
- const std::size_t segmentCount=passage.controlPoints.size()>1
- ?passage.controlPoints.size()-1:0;
- std::int64_t worst=std::numeric_limits<std::int64_t>::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<segmentCount;++segmentIndex)
- {
- // The two spans nearest the mouth form the authored joint itself.
- // Test the candidate against the rest of the passage so a loop
- // crossing cannot be mistaken for a usable portal tangent.
- if(lowerSide&&segmentIndex+2>=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<double>(dx)*dx
- +static_cast<double>(dy)*dy;
- const double projection=lengthSquared>0.0
- ?std::clamp((static_cast<double>(x-ax)*dx
- +static_cast<double>(y-ay)*dy)/lengthSquared,0.0,1.0):0.0;
- const std::int64_t tQ16=static_cast<std::int64_t>(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<std::int64_t>(
- integerSqrt(static_cast<std::uint64_t>((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<std::int64_t>(integerSqrt(static_cast<std::uint64_t>(
- horizontalGap*horizontalGap+verticalGap*verticalGap)))
- :std::max(horizontalGap,verticalGap);
- worst=std::min(worst,separation);sampled=true;
- }
- }
- return sampled?worst:std::numeric_limits<std::int64_t>::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<std::int64_t,2> 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<std::int64_t,2> 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<long long>(bestOutwardQ16[0]),
- static_cast<long long>(bestOutwardQ16[1]));
- return std::array<std::int64_t,2>{
- 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<std::int64_t,2>& 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<std::int64_t>(integerSqrt(
- static_cast<std::uint64_t>(tx*tx+ty*ty)));
- if(tangentLength<=0)return false;
- routeTangentQ16={tx*65536/tangentLength,ty*65536/tangentLength};
+ std::vector<VoxelForgeCore::PassageSpline::FPoint> splineControls;
+ std::vector<double> 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<double>(x),static_cast<double>(y),
+ static_cast<double>(z),static_cast<double>(radius)});
+ floorProfile.push_back(static_cast<double>(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::int64_t>(std::llround(derivative[0]*65536.0/tangentLength)),
+ static_cast<std::int64_t>(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::int64_t>(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<double>::infinity();
const double totalLength=routeLength(points)/1.0e6;
- constexpr double PortalExclusionMetres=10.0;
+ constexpr double PortalExclusionMetres=2.0;
double minimumClearance=std::numeric_limits<double>::infinity();
double along=0.0;
for(std::size_t index=1;index<points.size();++index)
@@ -3264,6 +3169,36 @@ bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout,
}
return edge;
};
+ const auto makePassageMouthEdge=[&](std::string id,std::string startAnchor,
+ const FPoint& start,const FRoomNode& endNode,
+ const ResolvedLayoutPassageV1& passage,bool lowerSide,
+ const std::array<std::int64_t,2>& 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::int64_t>(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<std::int64_t>(1,
+ std::min(PortalLeadLength,std::max<std::int64_t>(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<std::uint32_t>::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<float>(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<double,3> 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<double,4> weights={one*one*one,
+ 3.0*one*one*t,3.0*one*t*t,t*t*t};
+ std::array<double,3> 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<double>(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<double,3> 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<std::int64_t,3> point={
- toCoreSubvoxels(inner[0]),toCoreSubvoxels(inner[1]),
- toCoreSubvoxels(inner[2])};
+ toCoreSubvoxels(static_cast<std::int64_t>(std::llround(edgeStart[0]))),
+ toCoreSubvoxels(static_cast<std::int64_t>(std::llround(edgeStart[1]))),
+ toCoreSubvoxels(static_cast<std::int64_t>(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<std::int64_t,3> innerPoint={
- toCoreSubvoxels(inner[0]),toCoreSubvoxels(inner[1]),
- toCoreSubvoxels(inner[2])};
+ toCoreSubvoxels(static_cast<std::int64_t>(std::llround(edgeInner[0]))),
+ toCoreSubvoxels(static_cast<std::int64_t>(std::llround(edgeInner[1]))),
+ toCoreSubvoxels(static_cast<std::int64_t>(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}")