diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h index cb2b2dd..204242d 100644 --- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h @@ -891,6 +891,7 @@ namespace VoxelForgeCore inline constexpr std::uint32_t ResolvedLayoutStampVocabularyVersionV1 = 1; inline constexpr std::uint32_t ResolvedLayoutStampVocabularyVersionV2 = 2; inline constexpr std::uint32_t ResolvedLayoutStampVocabularyVersionV3 = 3; + inline constexpr std::uint32_t ResolvedLayoutStampVocabularyVersionV4 = 4; inline constexpr std::uint32_t ResolvedLayoutCavernGraphVocabularyVersionV1 = 1; inline constexpr std::uint32_t ResolvedLayoutOreVeinVocabularyVersionV1 = 1; enum class ResolvedLayoutValidationModeV3 : std::uint8_t { compatibility, strict }; @@ -904,7 +905,8 @@ namespace VoxelForgeCore fillPillar = 3, fillDais = 4, fillMonolith = 5, - fillPlatform = 6 + fillPlatform = 6, + fillSweptDeckSegment = 7 }; struct ResolvedLayoutStampRequestV1 @@ -914,6 +916,8 @@ namespace VoxelForgeCore ResolvedSdfStampKindV1 kind = ResolvedSdfStampKindV1::carveEllipsoid; // A preferred center and a bounded deterministic search radius. std::array anchorPositionMicrometres{}; + // Present only for fillSweptDeckSegment; defines its directed centerline. + std::array, 2> sweepEndpointsMicrometres{}; std::int64_t placementRadiusMicrometres = 0; std::array halfExtentsMicrometres{}; std::int64_t smoothWidthMicrometres = 0; @@ -933,6 +937,7 @@ namespace VoxelForgeCore std::int32_t strateIndex = -1; ResolvedSdfStampKindV1 kind = ResolvedSdfStampKindV1::carveEllipsoid; std::array positionMicrometres{}; + std::array, 2> sweepEndpointsMicrometres{}; std::array boundsMinimumMicrometres{}; std::array boundsMaximumMicrometres{}; std::array halfExtentsMicrometres{}; diff --git a/Standalone/Core/src/ResolvedLayoutInternal.h b/Standalone/Core/src/ResolvedLayoutInternal.h index 5fcb719..5074335 100644 --- a/Standalone/Core/src/ResolvedLayoutInternal.h +++ b/Standalone/Core/src/ResolvedLayoutInternal.h @@ -176,7 +176,7 @@ namespace { const auto value=static_cast(kind); return value>=static_cast(ResolvedSdfStampKindV1::carveEllipsoid) - &&value<=static_cast(ResolvedSdfStampKindV1::fillPlatform); + &&value<=static_cast(ResolvedSdfStampKindV1::fillSweptDeckSegment); } bool LayoutStampIsMotif(ResolvedSdfStampKindV1 kind) {return kind==ResolvedSdfStampKindV1::fillMonolith @@ -598,6 +598,14 @@ namespace <<",\"irregularity_wavelength_um\":"<(stamp.yawQuarterTurns); + if(stamp.kind==ResolvedSdfStampKindV1::fillSweptDeckSegment) + out<<",\"sweep_endpoints_um\":[[" + <=ResolvedLayoutStampVocabularyVersionV3) out<<",\"rounding_radius_um\":"< stampIds; for(const auto& stamp:stamps) { @@ -1145,6 +1154,8 @@ bool ResolvedLayoutSnapshotV3::validate(ResolvedLayoutValidationModeV3 mode, else stampIds.push_back(stamp.stableId); const bool motif=LayoutStampIsMotif(stamp.kind); if(!LayoutStampKindValid(stamp.kind)||stamp.yawQuarterTurns>3 + ||(stamp.kind==ResolvedSdfStampKindV1::fillSweptDeckSegment + &&stampVocabularyVersion0 &&stamp.irregularityWavelengthMicrometres<=0)) add("stamp_parameters",stamp.stableId+" has invalid or unbounded geometry parameters"); + if(stamp.kind==ResolvedSdfStampKindV1::fillSweptDeckSegment) + { + double lengthSquared=0.0; + for(std::size_t axis=0;axis<3;++axis) + { + const auto a=stamp.sweepEndpointsMicrometres[0][axis]; + const auto b=stamp.sweepEndpointsMicrometres[1][axis]; + if(a<-1000000000000ll||a>1000000000000ll + ||b<-1000000000000ll||b>1000000000000ll) + add("stamp_sweep",stamp.stableId+" has an unbounded endpoint"); + const double delta=static_cast(stamp.sweepEndpointsMicrometres[1][axis]) + -stamp.sweepEndpointsMicrometres[0][axis]; + lengthSquared+=delta*delta; + } + if(!(lengthSquared>0.0) + ||std::sqrt(lengthSquared)>2.0*stamp.halfExtentsMicrometres[0]+1.0) + add("stamp_sweep",stamp.stableId+" has invalid endpoints or longitudinal extent"); + } if(motif) { const auto minimumExtent=*std::min_element(stamp.halfExtentsMicrometres.begin(), @@ -1186,16 +1215,39 @@ bool ResolvedLayoutSnapshotV3::validate(ResolvedLayoutValidationModeV3 mode, const std::int64_t pad=motif ?(stamp.smoothWidthMicrometres+5)/6+stamp.irregularityAmplitudeMicrometres :stamp.smoothWidthMicrometres+stamp.irregularityAmplitudeMicrometres; - const std::array extents={ - stamp.halfExtentsMicrometres[stamp.yawQuarterTurns%2?1:0]+pad, - stamp.halfExtentsMicrometres[stamp.yawQuarterTurns%2?0:1]+pad, - stamp.halfExtentsMicrometres[2]+pad}; - for(std::size_t axis=0;axis<3;++axis) - if(stamp.boundsMinimumMicrometres[axis] - !=stamp.positionMicrometres[axis]-extents[axis] - ||stamp.boundsMaximumMicrometres[axis] - !=stamp.positionMicrometres[axis]+extents[axis]) - add("stamp_bounds",stamp.stableId+" conservative bounds do not match its parameters"); + if(stamp.kind==ResolvedSdfStampKindV1::fillSweptDeckSegment) + { + double radiusSquared=0.0; + for(const auto extent:stamp.halfExtentsMicrometres) + { + const double expanded=static_cast(extent+pad); + radiusSquared+=expanded*expanded; + } + const auto radius=static_cast(std::ceil(std::sqrt(radiusSquared))); + for(std::size_t axis=0;axis<3;++axis) + { + const auto minimum=std::min(stamp.sweepEndpointsMicrometres[0][axis], + stamp.sweepEndpointsMicrometres[1][axis]); + const auto maximum=std::max(stamp.sweepEndpointsMicrometres[0][axis], + stamp.sweepEndpointsMicrometres[1][axis]); + if(stamp.boundsMinimumMicrometres[axis]!=minimum-radius + ||stamp.boundsMaximumMicrometres[axis]!=maximum+radius) + add("stamp_bounds",stamp.stableId+" conservative sweep bounds do not match its parameters"); + } + } + else + { + const std::array extents={ + stamp.halfExtentsMicrometres[stamp.yawQuarterTurns%2?1:0]+pad, + stamp.halfExtentsMicrometres[stamp.yawQuarterTurns%2?0:1]+pad, + stamp.halfExtentsMicrometres[2]+pad}; + for(std::size_t axis=0;axis<3;++axis) + if(stamp.boundsMinimumMicrometres[axis] + !=stamp.positionMicrometres[axis]-extents[axis] + ||stamp.boundsMaximumMicrometres[axis] + !=stamp.positionMicrometres[axis]+extents[axis]) + add("stamp_bounds",stamp.stableId+" conservative bounds do not match its parameters"); + } std::string safetyReason; if(!LayoutStampSafe(*this,stamp,&safetyReason)) add("stamp_placement",stamp.stableId+" is unsafe: "+safetyReason); @@ -1497,6 +1549,13 @@ std::optional ResolvedLayoutSnapshotV3::FromJson( target[axis]=values[axis].Integer(); }; readTriplet("position_um",stamp.positionMicrometres); + const auto& sweepEndpoints=v.Get("sweep_endpoints_um").AsArray(); + for(std::size_t endpoint=0;endpoint<2&&endpoint3 ||request.placementRadiusMicrometres<0 ||request.placementRadiusMicrometres>100000000 + ||(request.kind==ResolvedSdfStampKindV1::fillSweptDeckSegment + &&request.placementRadiusMicrometres!=0) ||request.smoothWidthMicrometres<0||request.smoothWidthMicrometres>10000000 ||request.irregularityAmplitudeMicrometres<0 ||request.irregularityAmplitudeMicrometres>10000000 @@ -1648,6 +1712,23 @@ bool ResolveResolvedLayoutStampsV1(ResolvedLayoutSnapshotV3& layout,std::int32_t for(const auto coordinate:request.anchorPositionMicrometres) if(coordinate<-1000000000000ll||coordinate>1000000000000ll) {error="stamp request "+request.stableId+" has an unbounded anchor";return false;} + if(request.kind==ResolvedSdfStampKindV1::fillSweptDeckSegment) + { + double lengthSquared=0.0; + for(std::size_t axis=0;axis<3;++axis) + { + const auto a=request.sweepEndpointsMicrometres[0][axis]; + const auto b=request.sweepEndpointsMicrometres[1][axis]; + if(a<-1000000000000ll||a>1000000000000ll + ||b<-1000000000000ll||b>1000000000000ll) + {error="stamp request "+request.stableId+" has an unbounded sweep endpoint";return false;} + const double delta=static_cast(b)-a; + lengthSquared+=delta*delta; + } + if(!(lengthSquared>0.0) + ||std::sqrt(lengthSquared)>2.0*request.halfExtentsMicrometres[0]+1.0) + {error="stamp request "+request.stableId+" has invalid swept-segment extents";return false;} + } // The shared JSON value type stores numbers as IEEE-754 doubles; keep the // serialized seed in its exact integer range so the canonical round trip holds. @@ -1666,6 +1747,7 @@ bool ResolveResolvedLayoutStampsV1(ResolvedLayoutSnapshotV3& layout,std::int32_t { ResolvedLayoutStampV1 stamp;stamp.stableId=request.stableId; stamp.strateIndex=request.strateIndex;stamp.kind=request.kind; + stamp.sweepEndpointsMicrometres=request.sweepEndpointsMicrometres; stamp.halfExtentsMicrometres=request.halfExtentsMicrometres; stamp.smoothWidthMicrometres=request.smoothWidthMicrometres; stamp.irregularityAmplitudeMicrometres=request.irregularityAmplitudeMicrometres; @@ -1687,14 +1769,36 @@ bool ResolveResolvedLayoutStampsV1(ResolvedLayoutSnapshotV3& layout,std::int32_t stamp.positionMicrometres[axis]=request.anchorPositionMicrometres[axis]+offset[axis]; const std::int64_t pad=stamp.smoothWidthMicrometres +stamp.irregularityAmplitudeMicrometres; - const std::array extents={ - stamp.halfExtentsMicrometres[stamp.yawQuarterTurns%2?1:0]+pad, - stamp.halfExtentsMicrometres[stamp.yawQuarterTurns%2?0:1]+pad, - stamp.halfExtentsMicrometres[2]+pad}; - for(std::size_t axis=0;axis<3;++axis) + if(stamp.kind==ResolvedSdfStampKindV1::fillSweptDeckSegment) { - stamp.boundsMinimumMicrometres[axis]=stamp.positionMicrometres[axis]-extents[axis]; - stamp.boundsMaximumMicrometres[axis]=stamp.positionMicrometres[axis]+extents[axis]; + double radiusSquared=0.0; + for(const auto extent:stamp.halfExtentsMicrometres) + { + const double expanded=static_cast(extent+pad); + radiusSquared+=expanded*expanded; + } + const std::int64_t radius=static_cast(std::ceil(std::sqrt(radiusSquared))); + for(std::size_t axis=0;axis<3;++axis) + { + const auto minimum=std::min(stamp.sweepEndpointsMicrometres[0][axis], + stamp.sweepEndpointsMicrometres[1][axis]); + const auto maximum=std::max(stamp.sweepEndpointsMicrometres[0][axis], + stamp.sweepEndpointsMicrometres[1][axis]); + stamp.boundsMinimumMicrometres[axis]=minimum-radius; + stamp.boundsMaximumMicrometres[axis]=maximum+radius; + } + } + else + { + const std::array extents={ + stamp.halfExtentsMicrometres[stamp.yawQuarterTurns%2?1:0]+pad, + stamp.halfExtentsMicrometres[stamp.yawQuarterTurns%2?0:1]+pad, + stamp.halfExtentsMicrometres[2]+pad}; + for(std::size_t axis=0;axis<3;++axis) + { + stamp.boundsMinimumMicrometres[axis]=stamp.positionMicrometres[axis]-extents[axis]; + stamp.boundsMaximumMicrometres[axis]=stamp.positionMicrometres[axis]+extents[axis]; + } } if(!LayoutStampSafe(candidate,stamp))continue; candidate.stamps.push_back(std::move(stamp));placed=true;break; diff --git a/Standalone/Core/src/ShapeWorldInternal.h b/Standalone/Core/src/ShapeWorldInternal.h index 37bad31..b9a8c8a 100644 --- a/Standalone/Core/src/ShapeWorldInternal.h +++ b/Standalone/Core/src/ShapeWorldInternal.h @@ -3535,6 +3535,7 @@ private: { const VoxelForgeCore::ResolvedLayoutStampV1* record=nullptr; std::array positionVoxels{},halfExtentsVoxels{}; + std::array,2> sweepEndpointsVoxels{}; float smoothWidthVoxels=0.0f; float irregularityAmplitudeVoxels=0.0f; float irregularityWavelengthVoxels=0.0f; @@ -3700,9 +3701,15 @@ private: StampSpatialBounds.push_back(bound); FStampRuntime runtime;runtime.record=&stamp; for(std::size_t axis=0;axis<3;++axis) + { runtime.positionVoxels[axis]=static_cast( static_cast(stamp.positionMicrometres[axis]) /Owner->CoreVoxelSizeMicrons); + for(std::size_t endpoint=0;endpoint<2;++endpoint) + runtime.sweepEndpointsVoxels[endpoint][axis]=static_cast( + static_cast(stamp.sweepEndpointsMicrometres[endpoint][axis]) + /Owner->CoreVoxelSizeMicrons); + } const std::size_t xAxis=stamp.yawQuarterTurns%2?1:0; const std::size_t yAxis=stamp.yawQuarterTurns%2?0:1; runtime.halfExtentsVoxels={ @@ -7152,6 +7159,35 @@ private: std::min(0.35f,std::min({stamp.halfExtentsVoxels[0], stamp.halfExtentsVoxels[1],stamp.halfExtentsVoxels[2]})*0.15f)); break; + case VoxelForgeCore::ResolvedSdfStampKindV1::fillSweptDeckSegment: + { + std::array tangent{},side{},up{},delta{}; + float lengthSquared=0.0f,horizontalSquared=0.0f; + for(std::size_t axis=0;axis<3;++axis) + { + delta[axis]=stamp.sweepEndpointsVoxels[1][axis] + -stamp.sweepEndpointsVoxels[0][axis]; + lengthSquared+=delta[axis]*delta[axis]; + if(axis<2)horizontalSquared+=delta[axis]*delta[axis]; + } + const float segmentLength=std::sqrt(lengthSquared); + const float horizontalLength=std::sqrt(horizontalSquared); + if(!(segmentLength>1.0e-5f&&horizontalLength>1.0e-5f)) + return std::numeric_limits::infinity(); + for(std::size_t axis=0;axis<3;++axis)tangent[axis]=delta[axis]/segmentLength; + side={-delta[1]/horizontalLength,delta[0]/horizontalLength,0.0f}; + up={-tangent[2]*side[1],tangent[2]*side[0], + tangent[0]*side[1]-tangent[1]*side[0]}; + const std::array offset={ + x-stamp.positionVoxels[0],y-stamp.positionVoxels[1],z-stamp.positionVoxels[2]}; + const auto dot=[&](const std::array& axis) + {return offset[0]*axis[0]+offset[1]*axis[1]+offset[2]*axis[2];}; + const float rounded=std::min(0.35f, + std::min(stamp.halfExtentsVoxels[1],stamp.halfExtentsVoxels[2])*0.15f); + distance=StampRoundedBoxSdf(dot(tangent),dot(side),dot(up), + stamp.halfExtentsVoxels,rounded); + break; + } default:return std::numeric_limits::infinity(); } if(stamp.irregularityAmplitudeVoxels>0.0f) diff --git a/Standalone/Research/WorldAssemblerM3/Linux142/BuildL142Fixtures.py b/Standalone/Research/WorldAssemblerM3/Linux142/BuildL142Fixtures.py new file mode 100644 index 0000000..b07c54e --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux142/BuildL142Fixtures.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +"""Build the L142 liminal pictures and walk-audit controls.""" +from __future__ import annotations + +import argparse +import json +import os +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[4] +OWNER = Path(os.environ.get("VOXELFORGE_OWNER_DATA", + ROOT / "Standalone/data/owner_world.json")) +OUT_DEFAULT = Path(os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l142")) + +FLAT_FIELD = { + "version": 1, + "kind": "flatField", + "minimum_arena_clearance_m": 0.0, +} + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--out", type=Path, default=OUT_DEFAULT / "fixtures") + args = parser.parse_args() + args.out.mkdir(parents=True, exist_ok=True) + base = json.loads(OWNER.read_text(encoding="utf-8")) + + monolith = dict(base) + monolith["surface_profile_override"] = dict(FLAT_FIELD) + monolith["motif_family_override"] = { + "version": 1, + "strate_index": 0, + "placement_policy": "seed,strate,stable_id", + "motifs": [{ + "stable_id": "liminal/oversized-familiar-monolith-v1", + "kind": "cappedCylinder", + "radius_m": 1.6, + "height_m": 26.0, + "route_side_offset_m": 42.0, + "placement_radius_m": 0.0, + "fillet_radius_m": 0.6, + "blend_width_m": 1.2, + "minimum_spacing_m": 0.0, + "yaw_quarter_turns": 0, + "attachment": "seated_floor", + }], + } + + hills = dict(base) + hills["surface_profile_override"] = { + "version": 1, + "kind": "periodicHills", + "amplitude_x_m": 2.2, + "amplitude_y_m": 2.2, + "wavelength_x_m": 80.0, + "wavelength_y_m": 80.0, + "minimum_arena_clearance_m": 0.0, + } + + road_override = { + "stable_id": "liminal/road-clouds-v2", + "strate_index": 0, + "start_xy_m": [120.0, 340.0], + "path_length_m": 360.0, + "curve_control_offset_m": 84.0, + "deck_height_above_field_m": 44.0, + "rise_control_m": 22.0, + "half_width_m": 4.0, + "half_thickness_m": 1.0, + "sample_spacing_m": 2.0, + "support_spacing_m": 24.0, + "support_radius_m": 0.55, + } + road = dict(base) + road["surface_profile_override"] = dict(FLAT_FIELD) + road["liminal_road_override"] = dict(road_override) + + short_control = dict(base) + short_control["surface_profile_override"] = dict(FLAT_FIELD) + short_override = dict(road_override) + short_override["path_length_m"] = 180.0 + short_control["liminal_road_override"] = short_override + + pillars = dict(base) + pillars["surface_profile_override"] = dict(FLAT_FIELD) + pillars["liminal_pillar_grid_override"] = { + "stable_id": "liminal/horizon-pillar-grid-v1", + "strate_index": 0, + "center_xy_m": [700.0, 800.0], + "rows": 15, + "columns": 15, + "spacing_m": 76.0, + "radius_m": 5.5, + "height_m": 40.0, + } + + fixtures = ( + ("flatField-monolith", monolith), + ("periodicHills", hills), + ("road-in-clouds", road), + ("road-short-control", short_control), + ("pillar-grid", pillars), + ) + for name, fixture in fixtures: + path = args.out / f"liminal-{name}.json" + path.write_text(json.dumps(fixture, indent=2) + "\n", encoding="utf-8") + print(f"[L142Fixture] name={name} file={path} status=PASS") + + +if __name__ == "__main__": + main() diff --git a/Standalone/Research/WorldAssemblerM3/Linux142/CaptureL142.py b/Standalone/Research/WorldAssemblerM3/Linux142/CaptureL142.py new file mode 100644 index 0000000..ced17b1 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux142/CaptureL142.py @@ -0,0 +1,433 @@ +#!/usr/bin/env python3 +"""Run L142 liminal determinism checks, walk audits and multi-LOD captures.""" +from __future__ import annotations + +import argparse +import hashlib +import json +import math +import os +from pathlib import Path +import re +import subprocess +import sys +import time + + +ROOT = Path(__file__).resolve().parents[4] +OUT_DEFAULT = Path(os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l142")) +CAPTURE_DEFAULT = Path(__file__).resolve().parent / "captures" +LIMIT = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--"] +Q_PER_METRE = 1024.0 +FOG_COLOR = "0.66,0.73,0.77" + +sys.path.insert(0, str(ROOT / "Standalone/Tools/verify")) +from runner import bounded_run, current_stamp, first_stamp + + +def fmt(values) -> str: + return ",".join(f"{value:.6f}" for value in values) + + +def sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + for block in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(block) + return digest.hexdigest() + + +def run_step(name: str, command: list[object], log: Path, *, env: dict[str, str], + stamped: bool = False, timeout_seconds: int = 1740, + expected_exit: int = 0) -> str: + expected_stamp = current_stamp() if stamped else None + if stamped: + binary = Path(str(command[0])) + probe = bounded_run([str(binary), "--print-source-stamp"], timeout=15) + if probe is None or probe.returncode != 0 or first_stamp(probe.stdout or "") != expected_stamp: + raise RuntimeError(f"{name} refused a stale executable: {binary}") + started = time.monotonic() + log.parent.mkdir(parents=True, exist_ok=True) + with log.open("w", encoding="utf-8") as stream: + result = subprocess.run([*LIMIT, f"timeout", f"{timeout_seconds}s", + *map(str, command)], cwd=ROOT, env=env, + stdout=stream, stderr=subprocess.STDOUT, + timeout=timeout_seconds + 5, check=False) + elapsed = time.monotonic() - started + output = log.read_text(encoding="utf-8", errors="replace") + print(f"[L142Run] step={name} elapsed_s={elapsed:.3f} exit={result.returncode} log={log}", + flush=True) + if stamped and (first_stamp(output) != expected_stamp or current_stamp() != expected_stamp): + raise RuntimeError(f"{name} source stamp mismatch; see {log}") + if result.returncode != expected_exit: + raise RuntimeError(f"{name} exit={result.returncode}, expected={expected_exit}; see {log}") + return output + + +def route_frame(manifest_path: Path) -> tuple[tuple[float, float, float], + tuple[float, float, float]]: + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + strata = [entry for entry in manifest["plan"]["strates"] + if entry.get("route") and len(entry["route"]) >= 2] + if not strata: + raise RuntimeError(f"no planned SurfaceWorld route in {manifest_path}") + route = strata[0]["route"] + points = [tuple(float(value) / Q_PER_METRE for value in item["position_q"]) + for item in route] + lengths = [math.dist(a, b) for a, b in zip(points, points[1:])] + total = sum(lengths) + if total <= 1.0e-6: + raise RuntimeError(f"degenerate planned route in {manifest_path}") + + def at(distance: float) -> tuple[float, float, float]: + remaining = max(0.0, min(total, distance)) + for index, (a, b, length) in enumerate(zip(points, points[1:], lengths)): + if remaining <= length or index == len(lengths) - 1: + if length <= 1.0e-9: + return b + t = remaining / length + return tuple(a[axis] + (b[axis] - a[axis]) * t for axis in range(3)) + remaining -= length + return points[-1] + + center = at(total * 0.5) + before, after = at(total * 0.5 - 1.0), at(total * 0.5 + 1.0) + dx, dy = after[0] - before[0], after[1] - before[1] + length = math.hypot(dx, dy) + if length <= 1.0e-9: + raise RuntimeError(f"degenerate route tangent in {manifest_path}") + return center, (dx / length, dy / length, 0.0) + + +def camera_angles(camera, target) -> tuple[float, float]: + dx, dy, dz = (target[index] - camera[index] for index in range(3)) + return math.degrees(math.atan2(dx, dy)), math.degrees(math.atan2(dz, math.hypot(dx, dy))) + + +def point_toward(camera, target) -> tuple[float, ...]: + yaw, pitch = camera_angles(camera, target) + return (*camera, yaw, pitch) + + +def stamp(layout: dict, stable_id: str) -> dict: + for record in layout.get("stamps", []): + if record.get("stable_id") == stable_id: + return record + raise RuntimeError(f"missing resolved stamp {stable_id}") + + +def make_views(name: str, manifest_path: Path, layout_path: Path): + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + layout = json.loads(layout_path.read_text(encoding="utf-8")) + center, tangent = route_frame(manifest_path) + tx, ty, _ = tangent + nx, ny = -ty, tx + base_height = manifest["recipe"]["surface_profile"]["h0_q"] / Q_PER_METRE + if name == "flatField-monolith": + monolith = stamp(layout, "liminal/oversized-familiar-monolith-v1") + sx, sy, _ = (value / 1.0e6 for value in monolith["position_um"]) + floor = monolith["floor_um"] / 1.0e6 + eye_camera = (sx - tx * 36.0, sy - ty * 36.0, floor + 1.6) + eye_target = (sx, sy, floor + 10.5) + wide_camera = (sx - tx * 42.0 - nx * 18.0, + sy - ty * 42.0 - ny * 18.0, floor + 42.0) + wide_target = (sx, sy, floor + 8.0) + return { + "eye": (eye_camera, eye_target, 20), + "wide": (wide_camera, wide_target, 24), + "height_center": floor, + "height_range": 38.0, + } + if name == "periodicHills": + # Route points are capsule centers: +0.72 m places the view at 1.60 m above ground. + eye_camera = (center[0], center[1], center[2] + 0.72) + eye_target = (center[0] - tx * 360.0, center[1] - ty * 360.0, + center[2] + 0.72) + wide_camera = (center[0] - tx * 36.0 - nx * 28.0, + center[1] - ty * 36.0 - ny * 28.0, center[2] + 30.0) + wide_target = (center[0] - tx * 310.0, center[1] - ty * 310.0, + center[2] + 0.72) + return { + "eye": (eye_camera, eye_target, 20), + "wide": (wide_camera, wide_target, 28), + "height_center": base_height, + "height_range": 6.0, + } + if name == "road-in-clouds": + deck = sorted((row for row in layout.get("stamps", []) + if row.get("stable_id", "").startswith("liminal/road-clouds-v2/deck/")), + key=lambda row: row["stable_id"]) + if len(deck) < 2: + raise RuntimeError("road layout has no resolved swept platform chain") + points = [tuple(value / 1.0e6 for value in row["position_um"]) + for row in deck] + tops = [point[2] + row["half_extents_um"][2] / 1.0e6 + for point, row in zip(points, deck)] + index = max(1, len(points) // 12) + camera_dx = points[index + 1][0] - points[index - 1][0] + camera_dy = points[index + 1][1] - points[index - 1][1] + camera_tangent_length = math.hypot(camera_dx, camera_dy) + camera_tx, camera_ty = (camera_dx / camera_tangent_length, + camera_dy / camera_tangent_length) + camera_nx, camera_ny = -camera_ty, camera_tx + eye_camera = (points[index][0] + camera_nx * 3.2, + points[index][1] + camera_ny * 3.2, tops[index] + 1.6) + eye_target = (points[index][0] + camera_tx * 90.0 + camera_nx * 60.0, + points[index][1] + camera_ty * 90.0 + camera_ny * 60.0, + tops[index] - 44.0) + middle = len(points) // 2 + dx = points[middle + 1][0] - points[middle - 1][0] + dy = points[middle + 1][1] - points[middle - 1][1] + tangent_length = math.hypot(dx, dy) + tx, ty = dx / tangent_length, dy / tangent_length + nx, ny = -ty, tx + wide_target_index = min(len(points) - 1, middle + 30) + wide_camera = (points[middle][0] - tx * 35.0 + nx * 30.0, + points[middle][1] - ty * 35.0 + ny * 30.0, + tops[middle] + 25.0) + wide_target = (points[wide_target_index][0], points[wide_target_index][1], + tops[wide_target_index] - 5.0) + return { + "eye": (eye_camera, eye_target, 3), + "wide": (wide_camera, wide_target, 3), + "height_center": base_height + 23.0, + "height_range": 60.0, + "road_deck": tops[middle], + "road_width": 2.0 * deck[middle]["half_extents_um"][1] / 1.0e6, + } + if name == "pillar-grid": + pillars = [row for row in layout.get("stamps", []) + if row.get("stable_id", "").startswith("liminal/horizon-pillar-grid-v1/pillar/")] + if len(pillars) < 9: + raise RuntimeError("pillar-grid layout has too few resolved supports") + xs = [row["position_um"][0] / 1.0e6 for row in pillars] + ys = [row["position_um"][1] / 1.0e6 for row in pillars] + floor = min((row["position_um"][2] - row["half_extents_um"][2]) / 1.0e6 + for row in pillars) + cx, cy = (min(xs) + max(xs)) * 0.5, (min(ys) + max(ys)) * 0.5 + eye_camera = (min(xs) - 78.0, cy, floor + 1.6) + eye_target = (max(xs) + 90.0, cy, floor + 1.6) + wide_camera = (cx - 280.0, cy - 420.0, floor + 230.0) + wide_target = (cx, cy, floor + 6.0) + return { + "eye": (eye_camera, eye_target, 3), + "wide": (wide_camera, wide_target, 3), + "height_center": floor + 12.0, + "height_range": 42.0, + } + raise RuntimeError(f"no capture framing for fixture {name}") + + +def headless_command(binary: Path, fixture: Path, camera, mesh: Path, + manifest: Path, layout: Path, workers: int, + clip_radius: int, *, hash_tiles: bool = False, + audit_road: bool = False, max_level: int = 0, + far_lods: bool = False, + max_clip_override: int | None = None) -> list[object]: + max_clip_level = (max_clip_override if max_clip_override is not None else + 8 if far_lods else max_level) + command: list[object] = [binary, fixture, f"--workers={workers}", + "--seed=0", "--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline", + "--force-generate", f"--clip-radius={clip_radius}", + f"--max-clip-level={max_clip_level}", + f"--max-level={max_level}", "--render-distance-chunks=0", + f"--camera={fmt(point_toward(camera, (camera[0], camera[1] + 1.0, camera[2])))}", + f"--mesh-export={mesh}", f"--world-assembler-manifest-output={manifest}", + f"--resolved-layout-output={layout}"] + if hash_tiles: + command.append("--stream-hash-tiles") + if audit_road: + command.append("--audit-liminal-road-walk") + return command + + +def determinism_digest(output: str, manifest_path: Path, + layout_path: Path) -> tuple[str, str, str]: + tile_hashes = sorted(line for line in output.splitlines() + if line.startswith("[PipelineTileHash] ")) + if not manifest_path.is_file() or not layout_path.is_file() or not tile_hashes: + raise RuntimeError("determinism run omitted manifest, layout or per-tile hashes") + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + layout = json.loads(layout_path.read_text(encoding="utf-8")) + manifest_digest = (manifest["content_hash"],manifest["recipe"]["content_hash"], + manifest["plan"]["content_hash"]) + return ("|".join(manifest_digest),layout["stable_identity"],"\n".join(tile_hashes)) + + +def run_determinism(name: str, fixture: Path, view, *, binaries: dict[str, Path], + out: Path, env: dict[str, str]) -> None: + camera, _, _ = view["eye"] + clip_radius = 1 + max_clip = 0 + baseline = None + cells = 0 + for compiler, workers in ((compiler, workers) + for compiler in ("clang", "gcc") + for workers in (1, 8, 12)): + tag = f"{name}-{compiler}-w{workers}" + run_dir = out / "determinism" / tag + run_dir.mkdir(parents=True, exist_ok=True) + mesh = run_dir / "feature.vfmesh" + manifest = run_dir / "manifest.json" + layout = run_dir / "layout.json" + command = headless_command(binaries[compiler], fixture,camera,mesh,manifest, + layout,workers,max_clip,hash_tiles=True) + output = run_step(tag,command,out/"logs"/f"determinism-{tag}.log", + env=env,stamped=True) + digest = determinism_digest(output,manifest,layout) + if baseline is None: + baseline = digest + elif digest != baseline: + raise RuntimeError(f"{name} diverged at {compiler} W{workers}; see determinism logs") + cells += 1 + print(f"[L142Determinism] fixture={name} compiler_worker_cells={cells} identical=yes status=PASS", + flush=True) + + +def export_and_capture(name: str, fixture: Path, views: dict, *, + headless: Path, renderer: Path, out: Path, + artifact_dir: Path, env: dict[str, str]) -> None: + for view_name in ("eye", "wide"): + camera, target, clip_radius = views[view_name] + clip_radius = min(clip_radius,3) + stem = f"{name}-{view_name}" + mesh = out / "capture-meshes" / f"{stem}.vfmesh" + manifest = out / "capture-manifests" / f"{stem}.json" + layout = out / "capture-layouts" / f"{stem}.json" + png = out / "capture-png" / f"{stem}.png" + image = artifact_dir / f"{stem}.jpg" + for path in (mesh,manifest,layout,png,image): + path.parent.mkdir(parents=True,exist_ok=True) + command = headless_command(headless,fixture,camera,mesh,manifest,layout,12, + clip_radius,max_level=-1,far_lods=True, + max_clip_override=(2 if name == "road-in-clouds" and view_name == "wide" + else 3 if name == "road-in-clouds" else None)) + output = run_step(f"capture-{stem}-export",command, + out/"logs"/f"capture-{stem}-export.log",env=env,stamped=True) + if not mesh.is_file() or mesh.stat().st_size == 0: + raise RuntimeError(f"{stem} produced an empty mesh") + yaw, pitch = camera_angles(camera,target) + height_center = views["height_center"] + height_range = views["height_range"] + if name == "road-in-clouds": + fog_start,fog_end,fog_color=(24,260,"0.88,0.93,0.95") + elif name == "pillar-grid": + fog_start,fog_end,fog_color=220,1250,"0.82,0.88,0.91" + elif name == "periodicHills": + fog_start,fog_end,fog_color=210,1000,"0.78,0.84,0.88" + else: + fog_start,fog_end,fog_color=30,105,FOG_COLOR + renderer_command = [renderer,mesh,png,"--size=1600x900","--fov-deg=72", + f"--camera-m={fmt(camera)}",f"--yaw-deg={yaw:.6f}", + f"--pitch-deg={pitch:.6f}","--tile-skirts=on","--shade=readable", + "--headlamp=off","--height-tint=on",f"--height-center-m={height_center:.6f}", + f"--height-range-m={height_range:.6f}","--light-direction=-0.42,-0.58,0.70", + "--fog=on",f"--fog-start-m={fog_start}",f"--fog-end-m={fog_end}", + f"--fog-color={fog_color}"] + run_step(f"capture-{stem}-render",renderer_command, + out/"logs"/f"capture-{stem}-render.log",env=env,stamped=True) + run_step(f"capture-{stem}-jpeg",["ffmpeg","-nostdin","-hide_banner","-loglevel", + "error","-y","-i",png,"-frames:v","1","-q:v","4",image], + out/"logs"/f"capture-{stem}-jpeg.log",env=env) + quality = run_step(f"capture-{stem}-quality", + [sys.executable,ROOT/"Standalone/Tools/verify/capture_quality.py",image], + out/"logs"/f"capture-{stem}-quality.log",env=env) + if "[CaptureQuality]" not in quality or not image.is_file() \ + or image.stat().st_size > 300_000: + raise RuntimeError(f"{stem} capture failed quality or size checks") + eye_height = "1.600m" if view_name == "eye" else "wide" + print(f"[L142Capture] fixture={name} view={view_name} eye_height={eye_height} " + f"camera_m=({fmt(camera)}) target_m=({fmt(target)}) bytes={image.stat().st_size} " + f"sha256={sha256(image)} status=PASS",flush=True) + + +def run_road_walk_audit(fixture: Path, view, *, headless: Path, + out: Path, env: dict[str, str]) -> None: + camera = view["eye"][0] + run_dir = out / "road-walk-audit" + run_dir.mkdir(parents=True,exist_ok=True) + output = run_step("road-walk-audit",headless_command(headless,fixture,camera, + run_dir/"audit.vfmesh",run_dir/"manifest.json",run_dir/"layout.json",12,1, + audit_road=True),out/"logs"/"road-walk-audit.log",env=env,stamped=True) + if not re.search(r"\[LiminalRoadWalkAudit\].*status=PASS",output): + raise RuntimeError("raised road failed the Core capsule walk audit") + + +def run_road_short_negative_control(fixture: Path, view, *, headless: Path, + out: Path, env: dict[str, str]) -> None: + camera = view["eye"][0] + run_dir = out / "road-short-negative-control" + run_dir.mkdir(parents=True,exist_ok=True) + output = run_step("road-short-negative-control", + headless_command(headless,fixture,camera,run_dir/"control.vfmesh", + run_dir/"manifest.json",run_dir/"layout.json",12,1,audit_road=True), + out/"logs"/"road-short-negative-control.log",env=env,stamped=True, + expected_exit=14) + match = re.search(r"\[LiminalRoadWalkAudit\].*path_length_m=([0-9.]+).*status=FAIL",output) + if not match or float(match.group(1)) >= 300.0: + raise RuntimeError("short-road negative control did not falsify the length gate") + print(f"[L142RoadNegativeControl] path_length_m={match.group(1)} " + "expected=FAIL status=PASS",flush=True) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--out", type=Path, default=OUT_DEFAULT) + parser.add_argument("--artifact-dir", type=Path, default=CAPTURE_DEFAULT) + parser.add_argument("--headless-clang", type=Path, required=True) + parser.add_argument("--headless-gcc", type=Path, required=True) + parser.add_argument("--renderer", type=Path, required=True) + args = parser.parse_args() + + out = args.out.resolve() + artifact_dir = args.artifact_dir.resolve() + fixture_dir = out / "fixtures" + for directory in (out,fixture_dir,artifact_dir): + directory.mkdir(parents=True,exist_ok=True) + env = os.environ.copy() + env["VOXELFORGE_VERIFY_OUT"] = str(out) + env["VOXELFORGE_OWNER_DATA"] = str(ROOT/"Standalone/data/owner_world.json") + env["LP_NUM_THREADS"] = "12" + env["XDG_CACHE_HOME"] = str(out/"xdg-cache") + run_step("build-fixtures",[sys.executable, + ROOT/"Standalone/Research/WorldAssemblerM3/Linux142/BuildL142Fixtures.py", + f"--out={fixture_dir}"],out/"logs"/"build-fixtures.log",env=env) + + binaries = {"clang":args.headless_clang.resolve(),"gcc":args.headless_gcc.resolve()} + fixtures = { + "flatField-monolith":fixture_dir/"liminal-flatField-monolith.json", + "periodicHills":fixture_dir/"liminal-periodicHills.json", + "road-in-clouds":fixture_dir/"liminal-road-in-clouds.json", + "pillar-grid":fixture_dir/"liminal-pillar-grid.json", + } + probed = {} + for name,fixture in fixtures.items(): + probe_dir=out/"plan-probes"/name + probe_dir.mkdir(parents=True,exist_ok=True) + camera=(0.0,0.0,-500.0) + output=run_step(f"plan-probe-{name}",headless_command(binaries["clang"],fixture, + camera,probe_dir/"probe.vfmesh",probe_dir/"manifest.json", + probe_dir/"layout.json",12,1),out/"logs"/f"plan-probe-{name}.log", + env=env,stamped=True) + if "[WorldAssemblerFullManifest]" not in output: + raise RuntimeError(f"{name} probe omitted its full plan manifest") + probed[name]=make_views(name,probe_dir/"manifest.json",probe_dir/"layout.json") + + for name,fixture in fixtures.items(): + run_determinism(name,fixture,probed[name],binaries=binaries,out=out,env=env) + run_road_walk_audit(fixtures["road-in-clouds"],probed["road-in-clouds"], + headless=binaries["clang"],out=out,env=env) + run_road_short_negative_control(fixture_dir/"liminal-road-short-control.json", + probed["road-in-clouds"],headless=binaries["clang"],out=out,env=env) + for name,fixture in fixtures.items(): + export_and_capture(name,fixture,probed[name],headless=binaries["clang"], + renderer=args.renderer.resolve(),out=out,artifact_dir=artifact_dir,env=env) + return 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except Exception as error: + print(f"[L142Capture] status=FAIL error={error}",file=sys.stderr,flush=True) + raise SystemExit(1) diff --git a/Standalone/Research/WorldAssemblerM3/Linux142/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux142/REPORT.md new file mode 100644 index 0000000..44de717 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux142/REPORT.md @@ -0,0 +1,46 @@ +# L142 — liminal pass 2 + +| Hills: eye height | Hills: wide | +|---|---| +| ![Periodic hills from eye height](captures/periodicHills-eye.jpg) | ![Periodic hills to the horizon](captures/periodicHills-wide.jpg) | + +| Road: eye height | Road: wide | +|---|---| +| ![Road edge over the cloud floor](captures/road-in-clouds-eye.jpg) | ![Swept road deck and supports above the cloud floor](captures/road-in-clouds-wide.jpg) | + +| Pillar grid: eye height | Pillar grid: wide | +|---|---| +| ![Pillars repeating along the horizon](captures/pillar-grid-eye.jpg) | ![Pillar grid from above](captures/pillar-grid-wide.jpg) | + +| Monolith: eye height | Monolith: wide | +|---|---| +| ![The retained monolith slice at eye height](captures/flatField-monolith-eye.jpg) | ![The retained monolith slice wide](captures/flatField-monolith-wide.jpg) | + +## Result + +The wide hill capture shows the validated 80 m repeating profile continuing across the horizon. The capture now streams LOD 0–8; the profile period and validator are unchanged. + +The road is a cubic swept path made from overlapping solid deck segments, not a chain of platform stamps. The earlier platform chain produced top steps; the swept deck passes the surface-sampled walk audit. Its wide view is capped at LOD 2 to keep the deck legible, so that frame shows a local section; the full route length is measured by the audit. + +The R47 addition is a 15×15 grid of identical pillars, spaced 76 m apart across a 1,064 m span. The monolith eye and wide views are retained as the first liminal slice. + +## Measurements + +- **Road:** 376.827 m horizontal path length, 8.000 m width, and 12.061° maximum sampled surface grade against the 15° limit. The deck rises and falls along the cubic profile. It has 17 supports at about 24 m spacing; the flat cloud floor is 44 m below the deck centerline (about 45 m below the walking surface), with pale fog in both views. +- **Walk audit:** 901 surface stations at no more than 0.5 m spacing; 56,763 capsule samples; 0 blocked samples, 0 unsupported samples across 5,871 support probes, 0 grade failures, and 0 missed surface probes. The 180 m short-road control measured 210.341 m and failed the 300 m gate as expected. +- **Capture quality:** all eight fixtures/views passed the image-quality gate at 1600×900. Hills and pillars stream through LOD 8; road eye and wide views use caps 3 and 2 respectively. +- **Determinism:** the monolith, hills, road, and pillar fixtures each matched across Clang and GCC at W1, W8, and W12 (six compiler/worker cells per fixture), on source stamp `72f239e9534faa27e3796fe29207403b190558403841aeebc87d8e75e98e34c7`. + +## Checks + +- Clang 19 and GCC builds passed with floating-point contraction off, four build jobs, and the L142 source stamp. +- `Standalone/Tools/verify/verify.py fast --out Saved/Linux/l142 --overall-timeout 1740` passed. Both current owner fixtures produced all 12 canonical hash rows matching the pinned L130-v227 baseline; W1 and W12 also matched each other. +- The fast summary retains non-gating differences against older L46/L53 diagnostic baselines; the current pinned L130 canonical checks pass. +- The L142 capture runner passed the walk audit, the short-road negative control, all four determinism comparisons, and all eight image-quality checks. Final logs are in `Saved/Linux/l142/logs/`. +- No Unreal build or performance timing matrix was run. No Unreal `Source/` files changed. The inherited single-run near-ready timing step was removed from this picture-and-geometry runner rather than treated as a timing result. + +## Remaining limits + +The road wide capture frames a local section at LOD 2 to retain a continuous deck; the 376.827 m length is established by the full-route walk audit. Captures use the standalone Linux renderer rather than an Unreal viewport. + +Changed Unreal `Source/` files: none. diff --git a/Standalone/Research/WorldAssemblerM3/Linux142/captures/flatField-monolith-eye.jpg b/Standalone/Research/WorldAssemblerM3/Linux142/captures/flatField-monolith-eye.jpg new file mode 100644 index 0000000..13eb052 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux142/captures/flatField-monolith-eye.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux142/captures/flatField-monolith-wide.jpg b/Standalone/Research/WorldAssemblerM3/Linux142/captures/flatField-monolith-wide.jpg new file mode 100644 index 0000000..3975e8a Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux142/captures/flatField-monolith-wide.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux142/captures/periodicHills-eye.jpg b/Standalone/Research/WorldAssemblerM3/Linux142/captures/periodicHills-eye.jpg new file mode 100644 index 0000000..92ba34a Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux142/captures/periodicHills-eye.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux142/captures/periodicHills-wide.jpg b/Standalone/Research/WorldAssemblerM3/Linux142/captures/periodicHills-wide.jpg new file mode 100644 index 0000000..4c066a5 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux142/captures/periodicHills-wide.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux142/captures/pillar-grid-eye.jpg b/Standalone/Research/WorldAssemblerM3/Linux142/captures/pillar-grid-eye.jpg new file mode 100644 index 0000000..54e9eea Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux142/captures/pillar-grid-eye.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux142/captures/pillar-grid-wide.jpg b/Standalone/Research/WorldAssemblerM3/Linux142/captures/pillar-grid-wide.jpg new file mode 100644 index 0000000..5b1a2cd Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux142/captures/pillar-grid-wide.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux142/captures/road-in-clouds-eye.jpg b/Standalone/Research/WorldAssemblerM3/Linux142/captures/road-in-clouds-eye.jpg new file mode 100644 index 0000000..5413752 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux142/captures/road-in-clouds-eye.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux142/captures/road-in-clouds-wide.jpg b/Standalone/Research/WorldAssemblerM3/Linux142/captures/road-in-clouds-wide.jpg new file mode 100644 index 0000000..3068688 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux142/captures/road-in-clouds-wide.jpg differ diff --git a/Standalone/Source/StreamBenchmarkHeadless.cpp b/Standalone/Source/StreamBenchmarkHeadless.cpp index bc48c9d..d8ed139 100644 --- a/Standalone/Source/StreamBenchmarkHeadless.cpp +++ b/Standalone/Source/StreamBenchmarkHeadless.cpp @@ -1882,8 +1882,11 @@ int main(int argc,char** argv) ownerJsonRoot.Get("motif_family_override").Data); const bool hasLiminalRoad=!std::holds_alternative( ownerJsonRoot.Get("liminal_road_override").Data); - if(hasMotifFamily&&hasLiminalRoad) - {std::fprintf(stderr,"motif_family_override and liminal_road_override cannot be combined\n");return 2;} + const bool hasLiminalPillarGrid=!std::holds_alternative( + ownerJsonRoot.Get("liminal_pillar_grid_override").Data); + if((hasMotifFamily&&hasLiminalRoad)||(hasMotifFamily&&hasLiminalPillarGrid) + ||(hasLiminalRoad&&hasLiminalPillarGrid)) + {std::fprintf(stderr,"liminal content overrides must be used in separate fixtures\n");return 2;} if(hasMotifFamily) { std::vector requests; @@ -1914,22 +1917,62 @@ int main(int argc,char** argv) } if(hasLiminalRoad) { - std::vector requests(1); - if(!StandaloneWorldAssemblyPrototype::BuildLiminalRoadRequest(ownerJsonRoot, - *ownerWorld,worldAssembly,resolvedLayout,requests.front(),error) + std::vector requests; + if(!StandaloneWorldAssemblyPrototype::BuildLiminalRoadRequests(ownerJsonRoot, + *ownerWorld,worldAssembly,resolvedLayout,requests,error) ||!VoxelForgeCore::ResolveResolvedLayoutStampsV1( resolvedLayout,seed,requests,error)) {std::fprintf(stderr,"Liminal road resolution failed: %s\n",error.c_str());return 2;} - const auto& road=resolvedLayout.stamps.front(); - std::printf("[LiminalRoadResolved] vocabulary=%u id=%s strate=%d shape=rounded_dais " - "center_m=(%.3f,%.3f,%.3f) top_m=%.3f span_m=%.3f width_m=%.3f " - "grade_deg=0.000 placement=free route_keepout=PASS band_clearance=PASS status=PASS\n", - resolvedLayout.stampVocabularyVersion,road.stableId.c_str(),road.strateIndex, - road.positionMicrometres[0]/1.0e6,road.positionMicrometres[1]/1.0e6, - road.positionMicrometres[2]/1.0e6, - (road.positionMicrometres[2]+road.halfExtentsMicrometres[2])/1.0e6, - 2.0*road.halfExtentsMicrometres[0]/1.0e6, - 2.0*road.halfExtentsMicrometres[1]/1.0e6); + std::vector deck; + std::size_t supportCount=0; + const std::string deckPrefix="liminal/road-clouds-v2/deck/"; + const std::string supportPrefix="liminal/road-clouds-v2/support/"; + for(const auto& stamp:resolvedLayout.stamps) + { + if(stamp.stableId.starts_with(deckPrefix))deck.push_back(&stamp); + else if(stamp.stableId.starts_with(supportPrefix))++supportCount; + } + std::sort(deck.begin(),deck.end(),[](const auto* a,const auto* b) + {return a->stableIdstableId;}); + double pathLength=0.0,maxGradeDegrees=0.0; + for(std::size_t i=0;isweepEndpointsMicrometres[1][0] + -segment->sweepEndpointsMicrometres[0][0])/1.0e6; + const double dy=(segment->sweepEndpointsMicrometres[1][1] + -segment->sweepEndpointsMicrometres[0][1])/1.0e6; + const double dz=std::abs(segment->sweepEndpointsMicrometres[1][2] + -segment->sweepEndpointsMicrometres[0][2])/1.0e6; + const double horizontal=std::hypot(dx,dy); + pathLength+=horizontal; + maxGradeDegrees=std::max(maxGradeDegrees, + std::atan2(dz,horizontal)*57.29577951308232); + } + const double width=deck.empty()?0.0: + 2.0*deck.front()->halfExtentsMicrometres[1]/1.0e6; + std::printf("[LiminalRoadResolved] vocabulary=%u id=liminal/road-clouds-v2 " + "strate=%d shape=swept_deck_segments deck_segments=%zu supports=%zu " + "path_length_m=%.3f width_m=%.3f grade_max_deg=%.3f " + "placement=free route_keepout=PASS band_clearance=PASS status=%s\n", + resolvedLayout.stampVocabularyVersion, + deck.empty()?-1:deck.front()->strateIndex,deck.size(),supportCount,pathLength, + width,maxGradeDegrees,deck.size()>1&&pathLength>=300.0&&width>=3.0 + &&maxGradeDegrees<=15.0?"PASS":"FAIL"); + if(deck.size()<2||pathLength<80.0||width<3.0||maxGradeDegrees>15.0) + {std::fprintf(stderr,"Liminal road resolved outside the walkable road bounds\n");return 2;} + } + if(hasLiminalPillarGrid) + { + std::vector requests; + if(!StandaloneWorldAssemblyPrototype::BuildLiminalPillarGridRequests(ownerJsonRoot, + *ownerWorld,worldAssembly,resolvedLayout,requests,error) + ||!VoxelForgeCore::ResolveResolvedLayoutStampsV1( + resolvedLayout,seed,requests,error)) + {std::fprintf(stderr,"Liminal pillar-grid resolution failed: %s\n",error.c_str());return 2;} + std::printf("[LiminalPillarGridResolved] vocabulary=%u pillars=%zu shape=identical_vertical_cylinders " + "placement=regular_grid route_keepout=PASS band_clearance=PASS status=PASS\n", + resolvedLayout.stampVocabularyVersion,resolvedLayout.stamps.size()); } if(options.LandmarkChamberDemo) { diff --git a/Standalone/Source/StreamBenchmarkPipeline.h b/Standalone/Source/StreamBenchmarkPipeline.h index e151fbd..dab5001 100644 --- a/Standalone/Source/StreamBenchmarkPipeline.h +++ b/Standalone/Source/StreamBenchmarkPipeline.h @@ -2966,53 +2966,122 @@ namespace StandaloneStreamBenchmark { bool found=false; bool passed=true; + std::vector deck; + constexpr std::string_view DeckPrefix="liminal/road-clouds-v2/deck/"; for(const auto& road:ResolvedLayout->stamps) { - if(road.stableId!="liminal/raised-road-v1")continue; + if(road.stableId.starts_with(DeckPrefix))deck.push_back(&road); + } + if(!deck.empty()) + { found=true; - const bool longAxisX=(road.yawQuarterTurns&1u)==0; - const double centerX=road.positionMicrometres[0]/1.0e6; - const double centerY=road.positionMicrometres[1]/1.0e6; - const double centerZ=road.positionMicrometres[2]/1.0e6; - const double halfLength=road.halfExtentsMicrometres[longAxisX?0:1]/1.0e6; - const double halfWidth=road.halfExtentsMicrometres[longAxisX?1:0]/1.0e6; - const double deckZ=centerZ+road.halfExtentsMicrometres[2]/1.0e6; - constexpr double CapsuleRadiusMetres=0.34; + std::sort(deck.begin(),deck.end(),[](const auto* a,const auto* b) + {return a->stableIdstableId;}); constexpr double CapsuleHeightMetres=1.76; constexpr double HalfCapsuleHeightMetres=CapsuleHeightMetres*0.5; - constexpr double EndInsetMetres=1.0; + constexpr double SlopeCapsuleClearanceMetres=0.12; std::vector> roadPath; - const double inset=std::min(EndInsetMetres,halfLength*0.25); - for(const double along:{-halfLength+inset,halfLength-inset}) - roadPath.push_back({(centerX+(longAxisX?along:0.0))/coreMetres, - (centerY+(longAxisX?0.0:along))/coreMetres, - (deckZ+HalfCapsuleHeightMetres)/coreMetres}); + roadPath.reserve(deck.size()*4); + std::vector> centerline; + centerline.reserve(deck.size()+1); + bool surfacePathValid=true; + for(std::size_t i=0;ikind!=VoxelForgeCore::ResolvedSdfStampKindV1::fillSweptDeckSegment) + surfacePathValid=false; + const auto point=[&](std::size_t endpoint) + {return std::array{ + segment->sweepEndpointsMicrometres[endpoint][0]/1.0e6, + segment->sweepEndpointsMicrometres[endpoint][1]/1.0e6, + segment->sweepEndpointsMicrometres[endpoint][2]/1.0e6};}; + const auto a=point(0),b=point(1); + if(i==0)centerline.push_back(a); + else if(std::abs(centerline.back()[0]-a[0])>1.0e-6 + ||std::abs(centerline.back()[1]-a[1])>1.0e-6 + ||std::abs(centerline.back()[2]-a[2])>1.0e-6) + surfacePathValid=false; + centerline.push_back(b); + } + double pathLengthMetres=0.0,maxGradeDegrees=0.0; + std::uint64_t surfaceProbes=0,surfaceMisses=0; + const auto surfaceTopAt=[&](double xMetres,double yMetres, + double nominalTop,double& surfaceTop) + { + ++surfaceProbes; + double low=nominalTop-0.25,high=nominalTop+1.0; + if(Shape.densityAt({xMetres/coreMetres,yMetres/coreMetres, + low/coreMetres})>=0.0f + ||Shape.densityAt({xMetres/coreMetres,yMetres/coreMetres, + high/coreMetres})<0.0f) + {++surfaceMisses;return false;} + for(int iteration=0;iteration<20;++iteration) + { + const double middle=(low+high)*0.5; + if(Shape.densityAt({xMetres/coreMetres,yMetres/coreMetres, + middle/coreMetres})<0.0f)low=middle; + else high=middle; + } + surfaceTop=high; + return true; + }; + const double halfThickness=deck.front()->halfExtentsMicrometres[2]/1.0e6; + for(std::size_t segment=1;segment(1, + static_cast(std::ceil(horizontal/0.5))); + for(std::uint64_t step=0;step<=intervals;++step) + { + if(segment>1&&step==0)continue; + const double t=static_cast(step)/intervals; + const double x=a[0]+dx*t,y=a[1]+dy*t; + const double nominalTop=a[2]+(b[2]-a[2])*t+halfThickness; + double actualTop=nominalTop; + if(!surfaceTopAt(x,y,nominalTop,actualTop))surfacePathValid=false; + roadPath.push_back({x/coreMetres,y/coreMetres, + (actualTop+HalfCapsuleHeightMetres + +SlopeCapsuleClearanceMetres)/coreMetres}); + } + } + for(std::size_t i=1;i=2.0*CapsuleRadiusMetres - &&gradeDegrees<=15.0&&roadAudit.passed; - std::printf("[LiminalRoadWalkAudit] id=%s stations=%llu capsule_samples=%llu " + const double widthMetres=2.0*deck.front()->halfExtentsMicrometres[1]/1.0e6; + passed=std::all_of(deck.begin(),deck.end(),[](const auto* road) + {return road->kind==VoxelForgeCore::ResolvedSdfStampKindV1::fillSweptDeckSegment;}) + &&pathLengthMetres>=300.0&&widthMetres>=3.0 + &&maxGradeDegrees<=15.0&&surfacePathValid&&roadAudit.passed; + std::printf("[LiminalRoadWalkAudit] id=liminal/road-clouds-v2 " + "deck_stamps=%zu path_length_m=%.3f stations=%llu capsule_samples=%llu " "blocked=%llu support_samples=%llu unsupported=%llu grade_failures=%llu " - "width_m=%.3f capsule_diameter_m=%.3f grade_deg=%.3f grade_limit_deg=15.000 " - "status=%s\n",road.stableId.c_str(), + "surface_probes=%llu surface_misses=%llu width_m=%.3f minimum_width_m=3.000 " + "max_grade_deg=%.3f grade_limit_deg=15.000 status=%s\n", + deck.size(),pathLengthMetres, static_cast(roadAudit.centers), static_cast(roadAudit.capsuleSamples), static_cast(roadAudit.blocked), static_cast(roadAudit.supportSamples), static_cast(roadAudit.unsupported), - static_cast(roadAudit.gradeFailures),widthMetres, - 2.0*CapsuleRadiusMetres,gradeDegrees,passed?"PASS":"FAIL"); + static_cast(roadAudit.gradeFailures), + static_cast(surfaceProbes), + static_cast(surfaceMisses),widthMetres, + maxGradeDegrees,passed?"PASS":"FAIL"); } if(!found&&required) { - std::printf("[LiminalRoadWalkAudit] status=FAIL reason=road_stamp_missing\n"); + std::printf("[LiminalRoadWalkAudit] status=FAIL reason=road_path_missing\n"); return false; } return passed; diff --git a/Standalone/Source/WorldAssemblyPrototype.h b/Standalone/Source/WorldAssemblyPrototype.h index bafe419..8897078 100644 --- a/Standalone/Source/WorldAssemblyPrototype.h +++ b/Standalone/Source/WorldAssemblyPrototype.h @@ -587,13 +587,15 @@ namespace StandaloneWorldAssemblyPrototype return true; } - inline bool BuildLiminalRoadRequest(const VFJson::Value& root, + inline bool BuildLiminalRoadRequests(const VFJson::Value& root, const FStandaloneOwnerWorld& owner, const VoxelForgeCore::WorldAssemblyManifest& assembly, const VoxelForgeCore::ResolvedLayoutSnapshotV3& layout, - VoxelForgeCore::ResolvedLayoutStampRequestV1& request,std::string& error) + std::vector& requests, + std::string& error) { using namespace VoxelForgeCore; + requests.clear(); const VFJson::Value& overrideValue=root.Get("liminal_road_override"); if(!overrideValue.IsObject()) {error="liminal_road_override must be an object";return false;} @@ -604,49 +606,229 @@ namespace StandaloneWorldAssemblyPrototype const VFJson::Value& strateValue=overrideValue.Get("strate_index"); const auto* id=std::get_if(&overrideValue.Get("stable_id").Data); - const auto& position=overrideValue.Get("position_xy_m").AsArray(); - const auto& extents=overrideValue.Get("half_extents_m").AsArray(); - const VFJson::Value& heightValue=overrideValue.Get("deck_height_above_field_m"); - const VFJson::Value& yawValue=overrideValue.Get("yaw_quarter_turns"); - if(!strateValue.IsNumber()||!id||id->empty()||position.size()!=2||extents.size()!=3 - ||!heightValue.IsNumber()||!std::isfinite(heightValue.Number()) - ||!yawValue.IsNumber()||yawValue.Integer()<0||yawValue.Integer()>3) - {error="liminal road requires stable_id, strate_index, position, extents, deck height and yaw";return false;} + const auto& position=overrideValue.Get("start_xy_m").AsArray(); + const auto readNumber=[&](const char* key,double fallback,double& result) + { + const VFJson::Value& value=overrideValue.Get(key); + if(std::holds_alternative(value.Data)) + {result=fallback;return true;} + if(!value.IsNumber()||!std::isfinite(value.Number()))return false; + result=value.Number();return true; + }; + double pathLength=0.0,curveControl=0.0,deckHeight=0.0,riseControl=0.0; + double halfWidth=0.0,halfThickness=0.0,sampleSpacing=0.0; + double supportSpacing=0.0,supportRadius=0.0; + if(!strateValue.IsNumber()||!id||id->empty()||position.size()!=2 + ||!readNumber("path_length_m",360.0,pathLength) + ||!readNumber("curve_control_offset_m",84.0,curveControl) + ||!readNumber("deck_height_above_field_m",44.0,deckHeight) + ||!readNumber("rise_control_m",22.0,riseControl) + ||!readNumber("half_width_m",3.2,halfWidth) + ||!readNumber("half_thickness_m",0.65,halfThickness) + ||!readNumber("sample_spacing_m",1.0,sampleSpacing) + ||!readNumber("support_spacing_m",24.0,supportSpacing) + ||!readNumber("support_radius_m",0.55,supportRadius)) + {error="liminal road requires a valid identity, start, and finite path parameters";return false;} for(const auto& value:position) if(!value.IsNumber()||!std::isfinite(value.Number())) - {error="liminal road position_xy_m must contain finite metre values";return false;} - for(const auto& value:extents) - if(!value.IsNumber()||!std::isfinite(value.Number()) - ||value.Number()<0.5||value.Number()>100.0) - {error="liminal road half_extents_m must contain values from 0.5 to 100 metres";return false;} - if(heightValue.Number()<1.0||heightValue.Number()>100.0) - {error="liminal road deck height must be from 1 to 100 metres above the field";return false;} + {error="liminal road start_xy_m must contain finite metre values";return false;} + if(std::abs(position[0].Number())>5000.0||std::abs(position[1].Number())>5000.0 + ||pathLength<80.0||pathLength>1000.0||curveControl<0.0||curveControl>200.0 + ||deckHeight<30.0||deckHeight>60.0||riseControl<0.0||riseControl>30.0 + ||halfWidth<1.5||halfWidth>10.0||halfThickness<0.5||halfThickness>3.0 + ||sampleSpacing<1.0||sampleSpacing>6.0 + ||supportSpacing<8.0||supportSpacing>60.0 + ||supportRadius<0.5||supportRadius>1.5) + {error="liminal road dimensions are outside the bounded walkable-road ranges";return false;} const auto strateIndex=static_cast(strateValue.Integer()); const auto strate=std::find_if(layout.strates.begin(),layout.strates.end(), [strateIndex](const auto& value){return value.strateIndex==strateIndex &&value.generatorKind=="SurfaceWorld";}); if(strate==layout.strates.end()) {error="liminal_road_override must select a resolved SurfaceWorld strate";return false;} - if(extents[0].Number()<10.0||extents[1].Number()<0.75 - ||extents[0].Number()<=extents[1].Number()*4.0) - {error="liminal road must be long and narrow, with at least 1.5 metres half-width";return false;} const auto metresToMicrometres=[](double value) {return static_cast(std::llround(value*1000000.0));}; const std::int64_t fieldFloorMicrometres=static_cast(std::llround( static_cast(assembly.recipe.surfaceProfile.h0Subvoxels) *owner.CoreVoxelSizeMicrons/WorldAssemblySubvoxelsPerVoxel)); - request={}; - request.stableId=*id; - request.strateIndex=strateIndex; - request.kind=ResolvedSdfStampKindV1::fillDais; - request.anchorPositionMicrometres={metresToMicrometres(position[0].Number()), - metresToMicrometres(position[1].Number()), - fieldFloorMicrometres+metresToMicrometres(heightValue.Number()) - -metresToMicrometres(extents[2].Number())}; - request.halfExtentsMicrometres={metresToMicrometres(extents[0].Number()), - metresToMicrometres(extents[1].Number()),metresToMicrometres(extents[2].Number())}; - request.yawQuarterTurns=static_cast(yawValue.Integer()); + const std::int64_t startX=metresToMicrometres(position[0].Number()); + const std::int64_t startY=metresToMicrometres(position[1].Number()); + const std::int64_t lengthUm=metresToMicrometres(pathLength); + const std::int64_t lateralUm=metresToMicrometres(curveControl); + const std::int64_t deckBaseUm=fieldFloorMicrometres+metresToMicrometres(deckHeight); + const std::int64_t riseUm=metresToMicrometres(riseControl); + const std::int64_t halfWidthUm=metresToMicrometres(halfWidth); + const std::int64_t halfThicknessUm=metresToMicrometres(halfThickness); + const std::uint32_t steps=static_cast(std::ceil(pathLength/sampleSpacing)); + const auto lerpInteger=[steps](std::int64_t a,std::int64_t b,std::uint32_t i) + { + const std::int64_t numerator=(b-a)*static_cast(i); + const std::int64_t rounded=numerator>=0 + ?numerator+static_cast(steps/2) + :numerator-static_cast(steps/2); + return a+rounded/static_cast(steps); + }; + const auto bezier=[&](const std::array& controls,std::uint32_t i) + { + const std::int64_t a=lerpInteger(controls[0],controls[1],i); + const std::int64_t b=lerpInteger(controls[1],controls[2],i); + const std::int64_t c=lerpInteger(controls[2],controls[3],i); + const std::int64_t d=lerpInteger(a,b,i); + const std::int64_t e=lerpInteger(b,c,i); + return lerpInteger(d,e,i); + }; + const std::array xControls={startX, + startX+lengthUm/3,startX+(2*lengthUm)/3,startX+lengthUm}; + const std::array yControls={startY,startY+lateralUm, + startY-lateralUm,startY}; + const std::array zControls={deckBaseUm,deckBaseUm+riseUm, + deckBaseUm-riseUm,deckBaseUm}; + std::vector> path; + path.reserve(static_cast(steps)+1); + const auto halfUp=[](std::int64_t numerator,std::int64_t denominator) + {return (numerator>=0?numerator+denominator/2:numerator-denominator/2)/denominator;}; + for(std::uint32_t i=0;i<=steps;++i) + { + const auto t=static_cast(i); + const auto n=static_cast(steps); + path.push_back({xControls[0]+halfUp(lengthUm*t,n), + bezier(yControls,i),bezier(zControls,i)}); + } + const auto makeStamp=[&](std::string stableId,ResolvedSdfStampKindV1 kind, + const std::array& positionUm, + const std::array& halfExtentsUm,std::int64_t smoothWidthUm) + { + ResolvedLayoutStampRequestV1 request; + request.stableId=std::move(stableId);request.strateIndex=strateIndex; + request.kind=kind;request.anchorPositionMicrometres=positionUm; + request.halfExtentsMicrometres=halfExtentsUm; + request.smoothWidthMicrometres=smoothWidthUm; + requests.push_back(std::move(request)); + }; + const std::int64_t deckSmoothUm=250000; + const std::int64_t segmentOverlapUm=350000; + for(std::uint32_t i=1;i(b[0]-a[0]); + const double dy=static_cast(b[1]-a[1]); + const double dz=static_cast(b[2]-a[2]); + const std::int64_t halfLengthUm=static_cast(std::ceil( + std::sqrt(dx*dx+dy*dy+dz*dz)*0.5))+segmentOverlapUm; + ResolvedLayoutStampRequestV1 request; + request.stableId=*id+"/deck/"+std::to_string(10000+i-1).substr(1); + request.strateIndex=strateIndex; + request.kind=ResolvedSdfStampKindV1::fillSweptDeckSegment; + request.anchorPositionMicrometres={ + (a[0]+b[0])/2,(a[1]+b[1])/2,(a[2]+b[2])/2}; + request.sweepEndpointsMicrometres={a,b}; + request.halfExtentsMicrometres={halfLengthUm,halfWidthUm,halfThicknessUm}; + request.smoothWidthMicrometres=deckSmoothUm; + requests.push_back(std::move(request)); + } + const std::int64_t supportBaseUm=fieldFloorMicrometres-2000000; + const std::int64_t supportRadiusUm=metresToMicrometres(supportRadius); + double distanceSinceSupport=supportSpacing; + std::uint32_t supportIndex=0; + for(std::uint32_t i=0;i0) + { + const auto& a=path[i-1];const auto& b=path[i]; + const double dx=static_cast(b[0]-a[0]); + const double dy=static_cast(b[1]-a[1]); + distanceSinceSupport+=std::hypot(dx,dy)/1000000.0; + } + if(i!=0&&i+1!=path.size()&&distanceSinceSupport& requests, + std::string& error) + { + using namespace VoxelForgeCore; + requests.clear(); + const VFJson::Value& overrideValue=root.Get("liminal_pillar_grid_override"); + if(!overrideValue.IsObject()) + {error="liminal_pillar_grid_override must be an object";return false;} + if(assembly.recipe.surfaceProfile.version!=1 + ||assembly.recipe.surfaceProfile.kind!=WorldAssemblySurfaceProfileKindV1::flatField + ||owner.CoreVoxelSizeMicrons==0) + {error="the liminal pillar grid requires a valid flatField SurfaceWorld recipe";return false;} + const auto* id=std::get_if(&overrideValue.Get("stable_id").Data); + const VFJson::Value& strateValue=overrideValue.Get("strate_index"); + const auto& center=overrideValue.Get("center_xy_m").AsArray(); + const auto readNumber=[&](const char* key,double fallback,double& result) + { + const VFJson::Value& value=overrideValue.Get(key); + if(std::holds_alternative(value.Data)) + {result=fallback;return true;} + if(!value.IsNumber()||!std::isfinite(value.Number()))return false; + result=value.Number();return true; + }; + double spacing=0.0,radius=0.0,height=0.0; + const VFJson::Value& rowsValue=overrideValue.Get("rows"); + const VFJson::Value& columnsValue=overrideValue.Get("columns"); + if(!id||id->empty()||!strateValue.IsNumber()||center.size()!=2 + ||!readNumber("spacing_m",76.0,spacing) + ||!readNumber("radius_m",4.0,radius) + ||!readNumber("height_m",28.0,height) + ||!rowsValue.IsNumber()||!columnsValue.IsNumber()) + {error="liminal pillar grid requires an identity, center, dimensions and counts";return false;} + for(const auto& value:center) + if(!value.IsNumber()||!std::isfinite(value.Number())) + {error="liminal pillar grid center_xy_m must contain finite metre values";return false;} + const int rows=static_cast(rowsValue.Integer()); + const int columns=static_cast(columnsValue.Integer()); + if(rows<3||rows>15||columns<3||columns>15||spacing<32.0||spacing>120.0 + ||radius<2.0||radius>12.0||height<12.0||height>60.0 + ||std::abs(center[0].Number())>5000.0||std::abs(center[1].Number())>5000) + {error="liminal pillar grid dimensions are outside their bounded ranges";return false;} + const auto strateIndex=static_cast(strateValue.Integer()); + const auto strate=std::find_if(layout.strates.begin(),layout.strates.end(), + [strateIndex](const auto& value){return value.strateIndex==strateIndex + &&value.generatorKind=="SurfaceWorld";}); + if(strate==layout.strates.end()) + {error="liminal_pillar_grid_override must select a resolved SurfaceWorld strate";return false;} + const auto toMicrometres=[](double value) + {return static_cast(std::llround(value*1000000.0));}; + const std::int64_t floorUm=static_cast(std::llround( + static_cast(assembly.recipe.surfaceProfile.h0Subvoxels) + *owner.CoreVoxelSizeMicrons/WorldAssemblySubvoxelsPerVoxel)); + const std::int64_t spacingUm=toMicrometres(spacing); + const std::int64_t radiusUm=toMicrometres(radius); + const std::int64_t halfHeightUm=toMicrometres(height*0.5); + const int rowStart=-(rows/2),columnStart=-(columns/2); + for(int row=0;row