From e016f6a20fcfac785c9870b3d1133fd5df47a6d4 Mon Sep 17 00:00:00 2001 From: Fr0zka Date: Sat, 24 Oct 2026 09:53:06 +0200 Subject: [PATCH] feat(core): cavern graph v1, one continuous floor-capped tunnel into the landmark chamber (L46); R13 The flagged SW demo's ramp (57 carves + 107 support pillars, visibly ribbed) becomes one cubic-Bezier sweep: closest point by 17-point seed plus 5 Newton steps, a cubic floor profile, a rounded 2.6 m bore, and a hard floor cap applied after unions and relief that only adds support where the field is air. Optional resolved cavern-graph records (one room, one edge) enter canonical identity only when present; their bounds extend World::isTileShapeFree. Reachability passes (L43 control fails), headroom 2.8 m, floor ripple 1.8 mm (L45 ramp control fails at 17.9 mm), shape limits met; demo off: owner hashes identical to L45. fast/medium/full PASS. Research R13 (read-only): continuous SDF sweeps along curves. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1 --- .../include/VoxelForgeCore/VoxelForgeCore.h | 47 ++ Standalone/Core/src/ResolvedLayoutInternal.h | 272 ++++++++++++ Standalone/Core/src/ShapeWorldInternal.h | 402 +++++++++++++++++- Standalone/Core/src/VoxelForgeCore.cpp | 30 ++ .../Linux46/CaptureLandmarkL46.py | 384 +++++++++++++++++ .../WorldAssemblerM3/Linux46/REPORT.md | 36 ++ .../Linux46/captures/entrance-looking-in.jpg | Bin 0 -> 45070 bytes .../Linux46/captures/ground-entrance.jpg | Bin 0 -> 39430 bytes .../Linux46/captures/inside-to-dais.jpg | Bin 0 -> 28310 bytes .../overview-entrance-and-chamber.jpg | Bin 0 -> 62735 bytes .../WorldAssemblerM3/Research13/REPORT.md | 44 ++ Standalone/Source/StreamBenchmarkHeadless.cpp | 142 ++++++- Standalone/Source/StreamBenchmarkPipeline.h | 346 +++++++++++++-- Standalone/Tools/verify/verify.py | 98 ++++- 14 files changed, 1753 insertions(+), 48 deletions(-) create mode 100644 Standalone/Research/WorldAssemblerM3/Linux46/CaptureLandmarkL46.py create mode 100644 Standalone/Research/WorldAssemblerM3/Linux46/REPORT.md create mode 100644 Standalone/Research/WorldAssemblerM3/Linux46/captures/entrance-looking-in.jpg create mode 100644 Standalone/Research/WorldAssemblerM3/Linux46/captures/ground-entrance.jpg create mode 100644 Standalone/Research/WorldAssemblerM3/Linux46/captures/inside-to-dais.jpg create mode 100644 Standalone/Research/WorldAssemblerM3/Linux46/captures/overview-entrance-and-chamber.jpg create mode 100644 Standalone/Research/WorldAssemblerM3/Research13/REPORT.md diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h index 82e5af7..ad4171b 100644 --- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h @@ -707,6 +707,7 @@ namespace VoxelForgeCore inline constexpr std::uint32_t ResolvedLayoutSchemaVersionV3 = 3; inline constexpr std::uint32_t ResolvedLayoutStampVocabularyVersionV1 = 1; inline constexpr std::uint32_t ResolvedLayoutStampVocabularyVersionV2 = 2; + inline constexpr std::uint32_t ResolvedLayoutCavernGraphVocabularyVersionV1 = 1; enum class ResolvedLayoutValidationModeV3 : std::uint8_t { compatibility, strict }; // Stable, bounded geometry edits composed into every Core field after the shared @@ -750,6 +751,42 @@ namespace VoxelForgeCore std::uint8_t yawQuarterTurns = 0; }; + // Optional room and swept-edge records for a resolved cavern graph. Geometry is stored + // in integer micrometres; bounds include smooth blending, relief, and floor support. + struct ResolvedCavernRoomV1 + { + std::string stableId; + std::int32_t strateIndex = -1; + std::array centerMicrometres{}; + std::array halfExtentsMicrometres{}; + std::int64_t floorMicrometres = 0; + std::int64_t smoothWidthMicrometres = 0; + std::int64_t irregularityAmplitudeMicrometres = 0; + std::int64_t irregularityWavelengthMicrometres = 0; + std::uint64_t irregularitySeed = 0; + std::array boundsMinimumMicrometres{}; + std::array boundsMaximumMicrometres{}; + }; + + struct ResolvedCavernEdgeV1 + { + std::string stableId; + std::string startAnchorStableId; + std::string endRoomStableId; + std::int32_t strateIndex = -1; + // A single cubic Bezier in XY. Its four Z values are the cubic floor profile. + std::array, 4> controlPointsMicrometres{}; + std::int64_t boreRadiusMicrometres = 0; + std::int64_t boreHeightMicrometres = 0; + std::int64_t floorSupportDepthMicrometres = 0; + std::int64_t smoothWidthMicrometres = 0; + std::int64_t irregularityAmplitudeMicrometres = 0; + std::int64_t irregularityWavelengthMicrometres = 0; + std::uint64_t irregularitySeed = 0; + std::array boundsMinimumMicrometres{}; + std::array boundsMaximumMicrometres{}; + }; + struct ResolvedLayoutAnchorV2 { std::string kind; @@ -918,6 +955,10 @@ namespace VoxelForgeCore // Kept absent from canonical layout identity when no feature requests stamps. std::uint32_t stampVocabularyVersion = 0; std::vector stamps; + // Kept absent from canonical identity when there are no graph records. + std::uint32_t cavernGraphVocabularyVersion = 0; + std::vector cavernRooms; + std::vector cavernEdges; std::string stableIdentity; bool finalize(std::string& error); @@ -970,6 +1011,12 @@ namespace VoxelForgeCore VFCORE_API ResolvedLayoutStampPlacementAuditV1 AuditResolvedLayoutStampPlacementV1( const ResolvedLayoutSnapshotV3& layout); + VFCORE_API bool ResolveResolvedCavernGraphV1( + ResolvedLayoutSnapshotV3& layout, std::int32_t seed, + std::span rooms, + std::span edges, + std::string& error); + struct FloatingIslandBoundsAuditResult { std::uint64_t boxesTested = 0; diff --git a/Standalone/Core/src/ResolvedLayoutInternal.h b/Standalone/Core/src/ResolvedLayoutInternal.h index 5b4feb2..bf0ead6 100644 --- a/Standalone/Core/src/ResolvedLayoutInternal.h +++ b/Standalone/Core/src/ResolvedLayoutInternal.h @@ -404,6 +404,56 @@ namespace } out<<']'; } + if(!s.cavernRooms.empty()||!s.cavernEdges.empty()) + { + out<<",\"cavern_graph_vocabulary_version\":" + <b.interiorTopMicrometres:a.order(i); @@ -870,6 +924,110 @@ bool ResolvedLayoutSnapshotV3::validate(ResolvedLayoutValidationModeV3 mode, if(!LayoutStampSafe(*this,stamp,&safetyReason)) add("stamp_placement",stamp.stableId+" is unsafe: "+safetyReason); } + if(cavernRooms.empty()&&cavernEdges.empty()) + { + if(cavernGraphVocabularyVersion!=0) + add("cavern_graph_vocabulary_version", + "cavern graph vocabulary version is set without graph records"); + } + else if(cavernGraphVocabularyVersion!=ResolvedLayoutCavernGraphVocabularyVersionV1) + add("cavern_graph_vocabulary_version", + "cavern graph records require vocabulary version 1"); + if(cavernGraphVocabularyVersion!=0) + { + if(cavernRooms.empty()||cavernEdges.empty()) + add("cavern_graph_shape","a resolved cavern graph needs at least one room and one edge"); + std::vector graphIds; + const auto graphIdValid=[&](const std::string& id) + { + if(id.empty()||std::find(graphIds.begin(),graphIds.end(),id)!=graphIds.end()) + return false; + graphIds.push_back(id);return true; + }; + for(const auto& room:cavernRooms) + { + if(!graphIdValid(room.stableId)) + add("cavern_graph_identity","room has an empty or duplicate stable identity"); + if(!findStratum(room.strateIndex)) + add("cavern_graph_strate",room.stableId+" names an unknown strate"); + bool validExtents=true; + for(const auto extent:room.halfExtentsMicrometres) + validExtents=validExtents&&extent>=500000&&extent<=100000000; + if(!validExtents||room.smoothWidthMicrometres<0 + ||room.smoothWidthMicrometres>10000000 + ||room.irregularityAmplitudeMicrometres<0 + ||room.irregularityAmplitudeMicrometres>10000000 + ||room.irregularityWavelengthMicrometres<0 + ||room.irregularityWavelengthMicrometres>100000000 + ||(room.irregularityAmplitudeMicrometres>0 + &&room.irregularityWavelengthMicrometres<=0)) + add("cavern_room_parameters",room.stableId+" has invalid or unbounded parameters"); + const std::int64_t pad=room.smoothWidthMicrometres + +room.irregularityAmplitudeMicrometres; + for(std::size_t axis=0;axis<3;++axis) + if(room.boundsMinimumMicrometres[axis] + !=room.centerMicrometres[axis]-room.halfExtentsMicrometres[axis]-pad + ||room.boundsMaximumMicrometres[axis] + !=room.centerMicrometres[axis]+room.halfExtentsMicrometres[axis]+pad) + add("cavern_room_bounds",room.stableId+" conservative bounds do not match its parameters"); + if(room.floorMicrometresroom.centerMicrometres[2]+room.halfExtentsMicrometres[2]) + add("cavern_room_floor",room.stableId+" floor lies outside its room envelope"); + } + for(const auto& edge:cavernEdges) + { + if(!graphIdValid(edge.stableId)) + add("cavern_graph_identity","edge has an empty or duplicate stable identity"); + if(edge.startAnchorStableId.empty()||edge.endRoomStableId.empty()) + add("cavern_edge_binding",edge.stableId+" has a missing endpoint identity"); + const auto room=std::find_if(cavernRooms.begin(),cavernRooms.end(), + [&](const auto& value){return value.stableId==edge.endRoomStableId;}); + if(room==cavernRooms.end()||room->strateIndex!=edge.strateIndex) + add("cavern_edge_binding",edge.stableId+" does not terminate at a room in its strate"); + if(!findStratum(edge.strateIndex)) + add("cavern_graph_strate",edge.stableId+" names an unknown strate"); + if(edge.boreRadiusMicrometres<500000||edge.boreRadiusMicrometres>10000000 + ||edge.boreHeightMicrometres<1000000||edge.boreHeightMicrometres>20000000 + ||edge.floorSupportDepthMicrometres<0 + ||edge.floorSupportDepthMicrometres>2000000 + ||edge.smoothWidthMicrometres<0||edge.smoothWidthMicrometres>10000000 + ||edge.irregularityAmplitudeMicrometres<0 + ||edge.irregularityAmplitudeMicrometres>10000000 + ||edge.irregularityWavelengthMicrometres<0 + ||edge.irregularityWavelengthMicrometres>100000000 + ||(edge.irregularityAmplitudeMicrometres>0 + &&edge.irregularityWavelengthMicrometres<=0)) + add("cavern_edge_parameters",edge.stableId+" has invalid or unbounded parameters"); + std::array low=edge.controlPointsMicrometres[0]; + std::array high=low; + for(const auto& point:edge.controlPointsMicrometres) + for(std::size_t axis=0;axis<3;++axis) + {low[axis]=std::min(low[axis],point[axis]);high[axis]=std::max(high[axis],point[axis]);} + const std::int64_t xyPad=edge.boreRadiusMicrometres + +edge.smoothWidthMicrometres+edge.irregularityAmplitudeMicrometres; + const std::int64_t zPad=edge.smoothWidthMicrometres + +edge.irregularityAmplitudeMicrometres; + for(std::size_t axis=0;axis<3;++axis) + { + const std::int64_t pad=axis==2?zPad:xyPad; + const std::int64_t lowSupport=axis==2?edge.floorSupportDepthMicrometres:0; + const std::int64_t highBore=axis==2?edge.boreHeightMicrometres:0; + if(edge.boundsMinimumMicrometres[axis]!=low[axis]-pad-lowSupport + ||edge.boundsMaximumMicrometres[axis]!=high[axis]+pad+highBore) + add("cavern_edge_bounds",edge.stableId+" conservative bounds do not match its parameters"); + } + if(room!=cavernRooms.end()) + { + const auto& endpoint=edge.controlPointsMicrometres.back(); + if(endpoint[0]centerMicrometres[0]-room->halfExtentsMicrometres[0] + ||endpoint[0]>room->centerMicrometres[0]+room->halfExtentsMicrometres[0] + ||endpoint[1]centerMicrometres[1]-room->halfExtentsMicrometres[1] + ||endpoint[1]>room->centerMicrometres[1]+room->halfExtentsMicrometres[1] + ||std::abs(endpoint[2]-room->floorMicrometres)>500000) + add("cavern_edge_portal",edge.stableId+" endpoint does not meet its room floor portal"); + } + } + } if(strict&&!issues.empty()){error="strict resolved-layout validation rejected "+issues.front().code;return false;} return true; } @@ -886,6 +1044,8 @@ std::optional ResolvedLayoutSnapshotV3::FromJson( s.strateLayoutChunkSizeMicrometres=static_cast(LayoutInt(root,"strate_layout_chunk_size_um")); s.strateLayoutChunkWidthSubvoxels=LayoutInt(root,"strate_layout_chunk_width_subvoxels"); s.stampVocabularyVersion=static_cast(LayoutInt(root,"stamp_vocabulary_version")); + s.cavernGraphVocabularyVersion=static_cast( + LayoutInt(root,"cavern_graph_vocabulary_version")); for(const auto& v:root.Get("strates").AsArray()) { ResolvedLayoutStratumV2 x;x.stableId=LayoutString(v,"stable_id"); @@ -955,6 +1115,57 @@ std::optional ResolvedLayoutSnapshotV3::FromJson( stamp.yawQuarterTurns=static_cast(LayoutInt(v,"yaw_quarter_turns")); s.stamps.push_back(std::move(stamp)); } + for(const auto& v:root.Get("cavern_rooms").AsArray()) + { + ResolvedCavernRoomV1 room;room.stableId=LayoutString(v,"stable_id"); + room.strateIndex=static_cast(LayoutInt(v,"strate_index")); + const auto readTriplet=[&](std::string_view key,std::array& target) + { + const auto& values=v.Get(key).AsArray(); + for(std::size_t axis=0;axis<3&&axis(LayoutInt(v,"irregularity_seed")); + s.cavernRooms.push_back(std::move(room)); + } + for(const auto& v:root.Get("cavern_edges").AsArray()) + { + ResolvedCavernEdgeV1 edge;edge.stableId=LayoutString(v,"stable_id"); + edge.startAnchorStableId=LayoutString(v,"start_anchor_stable_id"); + edge.endRoomStableId=LayoutString(v,"end_room_stable_id"); + edge.strateIndex=static_cast(LayoutInt(v,"strate_index")); + const auto& points=v.Get("control_points_um").AsArray(); + for(std::size_t i=0;i& target) + { + const auto& values=v.Get(key).AsArray(); + for(std::size_t axis=0;axis<3&&axis(LayoutInt(v,"irregularity_seed")); + s.cavernEdges.push_back(std::move(edge)); + } const std::string serializedIdentity=LayoutString(root,"stable_identity"); if(!s.finalize(error))return std::nullopt; if(s.stableIdentity!=serializedIdentity){error="resolved layout stable identity does not match its canonical payload";return std::nullopt;} @@ -1080,6 +1291,67 @@ bool ResolveResolvedLayoutStampsV2(ResolvedLayoutSnapshotV3& layout,std::int32_t layout=std::move(candidate);return true; } +bool ResolveResolvedCavernGraphV1(ResolvedLayoutSnapshotV3& layout,std::int32_t seed, + std::span rooms, + std::span edges,std::string& error) +{ + error.clear(); + ResolvedLayoutSnapshotV3 candidate=layout; + candidate.cavernRooms.clear();candidate.cavernEdges.clear(); + if(rooms.empty()&&edges.empty()) + { + candidate.cavernGraphVocabularyVersion=0; + if(!candidate.finalize(error))return false; + layout=std::move(candidate);return true; + } + candidate.cavernGraphVocabularyVersion=ResolvedLayoutCavernGraphVocabularyVersionV1; + candidate.cavernRooms.assign(rooms.begin(),rooms.end()); + candidate.cavernEdges.assign(edges.begin(),edges.end()); + for(auto& room:candidate.cavernRooms) + { + // The shared JSON value type stores numbers as IEEE-754 doubles. Keep the + // serialized seed in the exact integer range, as the stamp vocabulary does. + room.irregularitySeed=LayoutStampSeed(seed,room.strateIndex,room.stableId) + &0x001fffffffffffffull; + const std::int64_t pad=room.smoothWidthMicrometres + +room.irregularityAmplitudeMicrometres; + for(std::size_t axis=0;axis<3;++axis) + { + room.boundsMinimumMicrometres[axis]=room.centerMicrometres[axis] + -room.halfExtentsMicrometres[axis]-pad; + room.boundsMaximumMicrometres[axis]=room.centerMicrometres[axis] + +room.halfExtentsMicrometres[axis]+pad; + } + } + for(auto& edge:candidate.cavernEdges) + { + edge.irregularitySeed=LayoutStampSeed(seed,edge.strateIndex,edge.stableId) + &0x001fffffffffffffull; + std::array low=edge.controlPointsMicrometres[0]; + std::array high=low; + for(const auto& point:edge.controlPointsMicrometres) + for(std::size_t axis=0;axis<3;++axis) + {low[axis]=std::min(low[axis],point[axis]);high[axis]=std::max(high[axis],point[axis]);} + const std::int64_t xyPad=edge.boreRadiusMicrometres + +edge.smoothWidthMicrometres+edge.irregularityAmplitudeMicrometres; + const std::int64_t zPad=edge.smoothWidthMicrometres + +edge.irregularityAmplitudeMicrometres; + for(std::size_t axis=0;axis<3;++axis) + { + const std::int64_t pad=axis==2?zPad:xyPad; + edge.boundsMinimumMicrometres[axis]=low[axis]-pad + -(axis==2?edge.floorSupportDepthMicrometres:0); + edge.boundsMaximumMicrometres[axis]=high[axis]+pad + +(axis==2?edge.boreHeightMicrometres:0); + } + } + if(!candidate.finalize(error))return false; + std::vector issues;std::string strictError; + if(!candidate.validate(ResolvedLayoutValidationModeV3::strict,issues,strictError)) + {error="strict cavern graph validation failed: "+strictError;return false;} + layout=std::move(candidate);return true; +} + ResolvedLayoutStampPlacementAuditV1 AuditResolvedLayoutStampPlacementV1( const ResolvedLayoutSnapshotV3& layout) { diff --git a/Standalone/Core/src/ShapeWorldInternal.h b/Standalone/Core/src/ShapeWorldInternal.h index 462c990..60a4ba3 100644 --- a/Standalone/Core/src/ShapeWorldInternal.h +++ b/Standalone/Core/src/ShapeWorldInternal.h @@ -169,6 +169,8 @@ public: featureIndices_=shape.BuildPassageFeatureTileList( minimumSubvoxels_,maximumSubvoxels_); stampIndices_=shape.BuildStampTileList(minimumSubvoxels_,maximumSubvoxels_); + cavernFeatureIndices_=shape.BuildCavernFeatureTileList( + minimumSubvoxels_,maximumSubvoxels_); passageRoutes_.assign(shape.ResolvedLayout?shape.ResolvedLayout->passages.size():0,0); passageColumns_=passageRoutes_; passageMouthSupports_=passageRoutes_; @@ -219,12 +221,16 @@ public: std::size_t featureCount() const{return featureIndices_.size();} bool hasStampCandidates() const{return !stampIndices_.empty();} const std::vector& stampCandidates() const{return stampIndices_;} + bool hasCavernCandidates() const{return !cavernFeatureIndices_.empty();} + const std::vector& cavernFeatureCandidates() const + {return cavernFeatureIndices_;} private: const FStandaloneShapeWorld* shape_; std::array minimumSubvoxels_,maximumSubvoxels_; std::vector featureIndices_; std::vector stampIndices_; + std::vector cavernFeatureIndices_; std::vector passageRoutes_,passageColumns_,passageMouthSupports_; bool routeFeatureOverlap_=false,columnZOverlap_=false; bool mayContainResolvedBoundaryBand_=true; @@ -1780,9 +1786,13 @@ public: float ApplyResolvedStrateComposition(float density,float X,float Y,float Z) const { + const float graphDensity=ApplyResolvedCavernGraph(density,X,Y,Z); const float composed=ApplyResolvedStratePassageCarves( - ApplyResolvedStrateBoundaryBand(density,X,Y,Z),X,Y,Z); - return StampRuntime.empty()?composed:ApplyResolvedSdfStamps(composed,X,Y,Z); + ApplyResolvedStrateBoundaryBand(graphDensity,X,Y,Z),X,Y,Z); + const float stamped=StampRuntime.empty()?composed:ApplyResolvedSdfStamps(composed,X,Y,Z); + // Keep the explicit graph floor cap after smooth unions, relief, passages, + // and decorative stamps so nothing can reopen the tunnel floor below profile. + return ApplyResolvedCavernFloorCaps(stamped,X,Y,Z); } bool ResolvedPassageIntersectsBox(const int32 minimum[3],const int32 maximum[3]) const @@ -3198,6 +3208,21 @@ private: std::uint64_t irregularitySeed=0; std::uint8_t quarterTurns=0; }; + struct FCavernRuntimeFeature + { + const VoxelForgeCore::ResolvedCavernRoomV1* room=nullptr; + const VoxelForgeCore::ResolvedCavernEdgeV1* edge=nullptr; + std::array boundsMinimumMicrometres{},boundsMaximumMicrometres{}; + std::array centerVoxels{},halfExtentsVoxels{}; + std::array,4> controlPointsVoxels{}; + float floorVoxels=0.0f; + float boreRadiusVoxels=0.0f,boreHeightVoxels=0.0f; + float floorSupportDepthVoxels=0.0f; + float smoothWidthVoxels=0.0f; + float irregularityAmplitudeVoxels=0.0f; + float irregularityWavelengthVoxels=0.0f; + std::uint64_t irregularitySeed=0; + }; int BuildStampSpatialNode(std::size_t first,std::size_t end) { @@ -3346,6 +3371,104 @@ private: return result; } + void BuildCavernSpatialIndex() + { + CavernRuntime.clear(); + if(!ResolvedLayout||!Owner||Owner->CoreVoxelSizeMicrons==0 + ||ResolvedLayout->cavernGraphVocabularyVersion + !=VoxelForgeCore::ResolvedLayoutCavernGraphVocabularyVersionV1)return; + const float micronsPerVoxel=static_cast(Owner->CoreVoxelSizeMicrons); + for(const auto& room:ResolvedLayout->cavernRooms) + { + FCavernRuntimeFeature runtime;runtime.room=&room; + runtime.boundsMinimumMicrometres=room.boundsMinimumMicrometres; + runtime.boundsMaximumMicrometres=room.boundsMaximumMicrometres; + for(std::size_t axis=0;axis<3;++axis) + { + runtime.centerVoxels[axis]=static_cast( + static_cast(room.centerMicrometres[axis])/micronsPerVoxel); + runtime.halfExtentsVoxels[axis]=static_cast( + static_cast(room.halfExtentsMicrometres[axis])/micronsPerVoxel); + } + runtime.floorVoxels=static_cast( + static_cast(room.floorMicrometres)/micronsPerVoxel); + runtime.smoothWidthVoxels=static_cast( + static_cast(room.smoothWidthMicrometres)/micronsPerVoxel); + runtime.irregularityAmplitudeVoxels=static_cast( + static_cast(room.irregularityAmplitudeMicrometres)/micronsPerVoxel); + runtime.irregularityWavelengthVoxels=static_cast( + static_cast(room.irregularityWavelengthMicrometres)/micronsPerVoxel); + runtime.irregularitySeed=room.irregularitySeed; + CavernRuntime.push_back(runtime); + } + for(const auto& edge:ResolvedLayout->cavernEdges) + { + FCavernRuntimeFeature runtime;runtime.edge=&edge; + runtime.boundsMinimumMicrometres=edge.boundsMinimumMicrometres; + runtime.boundsMaximumMicrometres=edge.boundsMaximumMicrometres; + for(std::size_t point=0;point<4;++point) + for(std::size_t axis=0;axis<3;++axis) + runtime.controlPointsVoxels[point][axis]=static_cast( + static_cast(edge.controlPointsMicrometres[point][axis]) + /micronsPerVoxel); + runtime.boreRadiusVoxels=static_cast( + static_cast(edge.boreRadiusMicrometres)/micronsPerVoxel); + runtime.boreHeightVoxels=static_cast( + static_cast(edge.boreHeightMicrometres)/micronsPerVoxel); + runtime.floorSupportDepthVoxels=static_cast( + static_cast(edge.floorSupportDepthMicrometres)/micronsPerVoxel); + runtime.smoothWidthVoxels=static_cast( + static_cast(edge.smoothWidthMicrometres)/micronsPerVoxel); + runtime.irregularityAmplitudeVoxels=static_cast( + static_cast(edge.irregularityAmplitudeMicrometres)/micronsPerVoxel); + runtime.irregularityWavelengthVoxels=static_cast( + static_cast(edge.irregularityWavelengthMicrometres)/micronsPerVoxel); + runtime.irregularitySeed=edge.irregularitySeed; + CavernRuntime.push_back(runtime); + } + } + + std::vector BuildCavernFeatureTileList( + const std::array& minimumSubvoxels, + const std::array& maximumSubvoxels) const + { + std::vector result; + if(CavernRuntime.empty())return result; + if(!Owner||Owner->CoreVoxelSizeMicrons==0) + { + result.resize(CavernRuntime.size()); + for(std::size_t i=0;i(i); + return result; + } + constexpr long double Q=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel; + std::array minimumMicrometres{},maximumMicrometres{}; + for(std::size_t axis=0;axis<3;++axis) + { + if(minimumSubvoxels[axis]>maximumSubvoxels[axis]) + { + result.resize(CavernRuntime.size()); + for(std::size_t i=0;i(i); + return result; + } + minimumMicrometres[axis]=static_cast(minimumSubvoxels[axis]) + *Owner->CoreVoxelSizeMicrons/Q; + maximumMicrometres[axis]=static_cast(maximumSubvoxels[axis]) + *Owner->CoreVoxelSizeMicrons/Q; + } + for(std::size_t index=0;index=minimumMicrometres[axis] + &&feature.boundsMinimumMicrometres[axis] + <=maximumMicrometres[axis]; + if(overlap)result.push_back(static_cast(index)); + } + return result; + } + int BuildPassageNearestNode(std::size_t passageIndex,std::vector segments) { const auto& points=ResolvedLayout->passages[passageIndex].controlPoints; @@ -3519,6 +3642,7 @@ private: void BuildPassageSpatialIndices() { BuildStampSpatialIndex(); + BuildCavernSpatialIndex(); PassageTileFeatures.clear();PassageFeatureBounds.clear(); PassageFeatureOrder.clear();PassageFeatureNodes.clear(); PassageFeatureBoundsFailOpen=false; @@ -4459,6 +4583,19 @@ public: static_cast(BMax)*Q}; if(!BuildStampTileList(minimum,maximum).empty())return false; } + if(!CavernRuntime.empty()) + { + constexpr std::int64_t Q=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel; + const std::array minimum={ + static_cast(Origin.X-Pad)*Q, + static_cast(Origin.Y-Pad)*Q, + static_cast(BMin)*Q}; + const std::array maximum={ + static_cast(Origin.X+(Cells+1)*Step)*Q, + static_cast(Origin.Y+(Cells+1)*Step)*Q, + static_cast(BMax)*Q}; + if(!BuildCavernFeatureTileList(minimum,maximum).empty())return false; + } const float LayoutWidth=static_cast(GetResolvedLayoutChunkWidthVoxels()); const int32 FirstChunk=FMath::FloorToInt(static_cast(BMin)/LayoutWidth); const int32 LastChunk=FMath::FloorToInt(static_cast(BMax)/LayoutWidth); @@ -4563,6 +4700,20 @@ public: if(!BuildStampTileList(minimum,maximum).empty()) return Finish(EVoxelTileClass::Mixed,true); } + if(!CavernRuntime.empty()) + { + constexpr std::int64_t Q=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel; + const std::array minimum={ + static_cast(BMin[0])*Q, + static_cast(BMin[1])*Q, + static_cast(BMin[2])*Q}; + const std::array maximum={ + static_cast(BMax[0])*Q, + static_cast(BMax[1])*Q, + static_cast(BMax[2])*Q}; + if(!BuildCavernFeatureTileList(minimum,maximum).empty()) + return Finish(EVoxelTileClass::Mixed,true); + } if(ResolvedPassageIntersectsBox(BMin,BMax)) return Finish(EVoxelTileClass::Mixed,true); if(!EditBrushes.empty()&&EditBoundsMin.X<=BMax[0]&&EditBoundsMax.X>=BMin[0] @@ -5727,6 +5878,252 @@ private: apply(static_cast(index)); return composed; } + float ApplyResolvedCavernGraph(float density,float X,float Y,float Z) const + { + if(CavernRuntime.empty()||!Owner||Owner->CoreVoxelSizeMicrons==0)return density; + const auto* tileQueries=ActivePassageTileQueries; + const bool scoped=tileQueries&&tileQueries->matches(*this); + if(scoped&&!tileQueries->hasCavernCandidates())return density; + const auto* bound=FindBoundStratumForManagerZ(Z); + if(!bound)return density; + const double micronsPerVoxel=Owner->CoreVoxelSizeMicrons; + const std::array worldMicrometres={X*micronsPerVoxel, + Y*micronsPerVoxel,Z*micronsPerVoxel}; + float composed=density; + const auto apply=[&](std::uint32_t index) + { + if(index>=CavernRuntime.size())return; + const auto& feature=CavernRuntime[index]; + const std::int32_t strateIndex=feature.room?feature.room->strateIndex + :feature.edge?feature.edge->strateIndex:-1; + if(strateIndex!=bound->StrateIndex)return; + for(std::size_t axis=0;axis<3;++axis) + if(worldMicrometres[axis]feature.boundsMaximumMicrometres[axis])return; + if(feature.room) + { + const float localX=X-feature.centerVoxels[0]; + const float localY=Y-feature.centerVoxels[1]; + const float localZ=Z-feature.centerVoxels[2]; + if(Z0.0f) + { + const float fade=SmoothUnit((Z-feature.floorVoxels)/0.8f); + distance+=StampValueNoise(localX,localY,localZ, + feature.irregularityWavelengthVoxels,feature.irregularitySeed) + *feature.irregularityAmplitudeVoxels*fade; + } + composed=SmoothMaxDistance(composed,-distance,feature.smoothWidthVoxels); + const float horizontal=std::sqrt( + (localX*localX)/(feature.halfExtentsVoxels[0] + *feature.halfExtentsVoxels[0]) + +(localY*localY)/(feature.halfExtentsVoxels[1] + *feature.halfExtentsVoxels[1])); + if(horizontal<=1.0f) + composed=std::min(composed,Z-feature.floorVoxels); + return; + } + if(!feature.edge)return; + const auto bezier=[&](float t,std::size_t axis) + { + const float one=1.0f-t; + return one*one*one*feature.controlPointsVoxels[0][axis] + +3.0f*one*one*t*feature.controlPointsVoxels[1][axis] + +3.0f*one*t*t*feature.controlPointsVoxels[2][axis] + +t*t*t*feature.controlPointsVoxels[3][axis]; + }; + const auto derivative=[&](float t,std::size_t axis) + { + const float one=1.0f-t; + return 3.0f*one*one*(feature.controlPointsVoxels[1][axis] + -feature.controlPointsVoxels[0][axis]) + +6.0f*one*t*(feature.controlPointsVoxels[2][axis] + -feature.controlPointsVoxels[1][axis]) + +3.0f*t*t*(feature.controlPointsVoxels[3][axis] + -feature.controlPointsVoxels[2][axis]); + }; + const auto secondDerivative=[&](float t,std::size_t axis) + { + return 6.0f*(1.0f-t)*(feature.controlPointsVoxels[2][axis] + -2.0f*feature.controlPointsVoxels[1][axis] + +feature.controlPointsVoxels[0][axis]) + +6.0f*t*(feature.controlPointsVoxels[3][axis] + -2.0f*feature.controlPointsVoxels[2][axis] + +feature.controlPointsVoxels[1][axis]); + }; + float t=0.0f,bestDistanceSquared=std::numeric_limits::infinity(); + for(int sample=0;sample<=16;++sample) + { + const float candidate=static_cast(sample)/16.0f; + const float dx=X-bezier(candidate,0),dy=Y-bezier(candidate,1); + const float distanceSquared=dx*dx+dy*dy; + if(distanceSquared0.0f) + { + const float fade=SmoothUnit((Z-floorZ)/0.8f); + distance+=StampValueNoise(X-feature.controlPointsVoxels[0][0], + Y-feature.controlPointsVoxels[0][1],Z-floorZ, + feature.irregularityWavelengthVoxels,feature.irregularitySeed) + *feature.irregularityAmplitudeVoxels*fade; + } + if(Z0.0f&&feature.floorSupportDepthVoxels>0.0f + &&Z>=floorZ-feature.floorSupportDepthVoxels + &&radialcavernFeatureCandidates())apply(index); + else + for(std::size_t index=0;index(index)); + return composed; + } + + float ApplyResolvedCavernFloorCaps(float density,float X,float Y,float Z) const + { + if(CavernRuntime.empty()||!Owner||Owner->CoreVoxelSizeMicrons==0)return density; + const auto* tileQueries=ActivePassageTileQueries; + const bool scoped=tileQueries&&tileQueries->matches(*this); + if(scoped&&!tileQueries->hasCavernCandidates())return density; + const auto* bound=FindBoundStratumForManagerZ(Z); + if(!bound)return density; + const double micronsPerVoxel=Owner->CoreVoxelSizeMicrons; + const std::array worldMicrometres={X*micronsPerVoxel, + Y*micronsPerVoxel,Z*micronsPerVoxel}; + float capped=density; + const auto apply=[&](std::uint32_t index) + { + if(index>=CavernRuntime.size())return; + const auto& feature=CavernRuntime[index]; + const std::int32_t strateIndex=feature.room?feature.room->strateIndex + :feature.edge?feature.edge->strateIndex:-1; + if(strateIndex!=bound->StrateIndex)return; + for(std::size_t axis=0;axis<3;++axis) + if(worldMicrometres[axis]feature.boundsMaximumMicrometres[axis])return; + if(feature.room) + { + if(Z::infinity(); + for(int sample=0;sample<=16;++sample) + { + const float candidate=static_cast(sample)/16.0f; + const float dx=X-bezier(candidate,0),dy=Y-bezier(candidate,1); + const float distanceSquared=dx*dx+dy*dy; + if(distanceSquared0.0f&&feature.floorSupportDepthVoxels>0.0f + &&Z>=floorZ-feature.floorSupportDepthVoxels + &&radialcavernFeatureCandidates())apply(index); + else + for(std::size_t index=0;index(index)); + return capped; + } static float SmoothUnit(float V) { V=std::clamp(V,0.0f,1.0f); @@ -8977,6 +9374,7 @@ private: std::vector StampSpatialNodes; std::vector StampSpatialOrder; std::vector StampRuntime; + std::vector CavernRuntime; std::vector PassageFeatureBounds; std::vector PassageTileFeatures; std::vector PassageFeatureOrder; diff --git a/Standalone/Core/src/VoxelForgeCore.cpp b/Standalone/Core/src/VoxelForgeCore.cpp index 4f2fa9b..43a6af5 100644 --- a/Standalone/Core/src/VoxelForgeCore.cpp +++ b/Standalone/Core/src/VoxelForgeCore.cpp @@ -1334,6 +1334,34 @@ namespace VoxelForgeCore } return false; } + + bool ResolvedCavernGraphTouchesBox(const ResolvedLayoutSnapshotV3& layout, + const FIntVector& minimum,const FIntVector& maximum, + std::uint32_t coreVoxelSizeMicrons) + { + if((layout.cavernRooms.empty()&&layout.cavernEdges.empty()) + ||coreVoxelSizeMicrons==0)return false; + const double tileMinimum[3]={static_cast(minimum.X), + static_cast(minimum.Y),static_cast(minimum.Z)}; + const double tileMaximum[3]={static_cast(maximum.X), + static_cast(maximum.Y),static_cast(maximum.Z)}; + const auto overlaps=[&](const auto& feature) + { + bool overlap=true; + for(std::size_t axis=0;axis<3;++axis) + { + const double low=static_cast(feature.boundsMinimumMicrometres[axis]) + /coreVoxelSizeMicrons; + const double high=static_cast(feature.boundsMaximumMicrometres[axis]) + /coreVoxelSizeMicrons; + overlap=overlap&&high>=tileMinimum[axis]&&low<=tileMaximum[axis]; + } + return overlap; + }; + for(const auto& room:layout.cavernRooms)if(overlaps(room))return true; + for(const auto& edge:layout.cavernEdges)if(overlaps(edge))return true; + return false; + } } bool World::compile(const ShapeBuildRequest& request,const JobScheduler& scheduler,std::string& error) @@ -1418,6 +1446,8 @@ namespace VoxelForgeCore if(RouteTouchesBox(*selectedPlan,subMinimum,subMaximum))return false; if(ResolvedStampTouchesBox(*impl_->resolvedLayout,boundsMin,boundsMax, impl_->owner->CoreVoxelSizeMicrons))return false; + if(ResolvedCavernGraphTouchesBox(*impl_->resolvedLayout,boundsMin,boundsMax, + impl_->owner->CoreVoxelSizeMicrons))return false; if(ResolvedPassageTouchesBox(*impl_->resolvedLayout,boundsMin,boundsMax, impl_->owner->CoreVoxelSizeMicrons))return false; if(OwnerPassageTouchesBox(*impl_->owner,boundsMin,boundsMax))return false; diff --git a/Standalone/Research/WorldAssemblerM3/Linux46/CaptureLandmarkL46.py b/Standalone/Research/WorldAssemblerM3/Linux46/CaptureLandmarkL46.py new file mode 100644 index 0000000..3235e73 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux46/CaptureLandmarkL46.py @@ -0,0 +1,384 @@ +#!/usr/bin/env python3 +"""Capture the L46 cavern graph views and audit player-scale built-shape limits.""" +from __future__ import annotations + +import argparse +import importlib.util +import json +import math +import os +from pathlib import Path +import subprocess +import sys +import time + + +ROOT = Path(__file__).resolve().parents[4] +VERIFY = ROOT / "Standalone/Tools/verify" +LINUX37 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux37" +sys.path.insert(0, str(VERIFY)) +from runner import bounded_run, current_stamp, first_stamp + + +def load_module(name: str, path: Path): + spec = importlib.util.spec_from_file_location(name, path) + if spec is None or spec.loader is None: + raise RuntimeError(f"cannot import {path}") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +PROFILE = load_module("l45_ring_profile", ROOT / + "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py") +SHAPE = load_module("l45_shape_audit", LINUX37 / "AuditMouthShapeL37.py") + +PARSER = argparse.ArgumentParser(description=__doc__) +PARSER.add_argument("--layout", type=Path, required=True) +PARSER.add_argument("--artifact-dir", type=Path, required=True) +PARSER.add_argument("--capture-dir", type=Path, required=True) +PARSER.add_argument("--data", type=Path, default=ROOT / "Standalone/data/owner_world.json") +OPTIONS = PARSER.parse_args() + +ARTIFACT_DIR = OPTIONS.artifact_dir.resolve() +CAPTURE_DIR = OPTIONS.capture_dir.resolve() +LAYOUT_PATH = OPTIONS.layout.resolve() +ARTIFACT_DIR.mkdir(parents=True, exist_ok=True) +CAPTURE_DIR.mkdir(parents=True, exist_ok=True) +PNG_DIR = ARTIFACT_DIR / "capture-png" +PNG_DIR.mkdir(parents=True, exist_ok=True) +HEADLESS = Path(os.environ["VOXELFORGE_L37_HEADLESS"]) +RENDERER = Path(os.environ["VOXELFORGE_L37_RENDERER"]) +EXPECTED_STAMP = current_stamp() + + +def run_logged(name: str, command: list[str], *, capture: bool = False) -> str: + log = ARTIFACT_DIR / f"{name}.log" + started = time.monotonic() + with log.open("w", encoding="utf-8") as stream: + result = bounded_run(command, timeout=1800, stdout=stream) + elapsed = time.monotonic() - started + output = log.read_text(encoding="utf-8", errors="replace") + if result is None: + raise RuntimeError(f"{name} timed out; see {log}") + print(f"[L45CaptureRun] step={name} elapsed={elapsed:.1f}s " + f"exit={result.returncode} log={log}", flush=True) + if result.returncode != 0: + raise RuntimeError(f"{name} failed; see {log}") + if capture and (first_stamp(output) != EXPECTED_STAMP + or first_stamp(bounded_run( + [str(command[0]), "--print-source-stamp"], timeout=15).stdout or "") + != EXPECTED_STAMP): + raise RuntimeError(f"{name} executable source stamp mismatch") + return output + + +def stamp_by_id(layout: dict, stable_id: str) -> dict: + for stamp in layout.get("stamps", []): + if stamp.get("stable_id") == stable_id: + return stamp + raise RuntimeError(f"missing stamp {stable_id}") + + +def metres(stamp: dict, key: str = "position_um") -> tuple[float, float, float]: + return tuple(value / 1_000_000.0 for value in stamp[key]) + + +def camera_angles(camera, target): + 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 fmt(values) -> str: + return ",".join(f"{value:.6f}" for value in values) + + +def camera_spec(camera, target): + yaw, pitch = camera_angles(camera, target) + return (*camera, yaw, pitch) + + +def edge_point(edge: dict, t: float) -> tuple[float, float, float]: + points = edge["control_points_um"] + one = 1.0 - t + weights = (one ** 3, 3 * one ** 2 * t, 3 * one * t ** 2, t ** 3) + return tuple(sum(weights[index] * points[index][axis] + for index in range(4)) / 1_000_000.0 + for axis in range(3)) + + +def cavern_features(layout: dict): + if (layout.get("cavern_graph_vocabulary_version") != 1 + or len(layout.get("cavern_rooms", [])) != 1 + or len(layout.get("cavern_edges", [])) != 1): + raise RuntimeError("L46 capture needs one resolved cavern room and edge") + room = layout["cavern_rooms"][0] + edge = layout["cavern_edges"][0] + return room, edge + + +layout = json.loads(LAYOUT_PATH.read_text(encoding="utf-8")) +room, edge = cavern_features(layout) +portal_stamp = stamp_by_id(layout, "landmark/sw-arrival-chamber-mouth-approach-v1") +floor_stamp = stamp_by_id(layout, "landmark/sw-arrival-chamber-v1-floor") +dais_stamp = stamp_by_id(layout, "landmark/sw-arrival-chamber-v1-dais") +entrance = edge_point(edge, 0.0) +next_ramp = edge_point(edge, 0.08) +portal = metres(portal_stamp) +chamber = metres(room, "center_um") +floor = metres(floor_stamp) +floor_top = floor[2] + floor_stamp["half_extents_um"][2] / 1_000_000.0 +dais = metres(dais_stamp) +dais_top = dais[2] + dais_stamp["half_extents_um"][2] / 1_000_000.0 +ground_z = entrance[2] +direction_length = math.hypot(next_ramp[0] - entrance[0], next_ramp[1] - entrance[1]) +direction = ((next_ramp[0] - entrance[0]) / direction_length, + (next_ramp[1] - entrance[1]) / direction_length) +outward_length = math.hypot(entrance[0] - chamber[0], entrance[1] - chamber[1]) +outward = ((entrance[0] - chamber[0]) / outward_length, + (entrance[1] - chamber[1]) / outward_length) +mouth_normal = (-direction[1], direction[0]) + +mesh = ARTIFACT_DIR / "landmark-l46.vfmesh" +export_camera = ((entrance[0] + chamber[0]) * 0.5, + (entrance[1] + chamber[1]) * 0.5, + (ground_z + floor_top) * 0.5) +export_target = (chamber[0], chamber[1], floor_top) +run_logged("landmark-export", [str(HEADLESS), str(OPTIONS.data), "--workers=12", + "--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline", + "--force-generate", "--clip-radius=3", "--max-clip-level=0", "--max-level=0", + "--render-distance-chunks=0", "--recipe=surface-world", "--landmark-chamber-demo", + f"--camera={fmt(camera_spec(export_camera, export_target))}", + f"--mesh-export={mesh}", f"--resolved-layout-output={LAYOUT_PATH}"], capture=True) +layout = json.loads(LAYOUT_PATH.read_text(encoding="utf-8")) +room, edge = cavern_features(layout) +portal_stamp = stamp_by_id(layout, "landmark/sw-arrival-chamber-mouth-approach-v1") +floor_stamp = stamp_by_id(layout, "landmark/sw-arrival-chamber-v1-floor") +dais_stamp = stamp_by_id(layout, "landmark/sw-arrival-chamber-v1-dais") +entrance = edge_point(edge, 0.0) +next_ramp = edge_point(edge, 0.08) +portal = metres(portal_stamp) +chamber = metres(room, "center_um") +floor = metres(floor_stamp) +floor_top = floor[2] + floor_stamp["half_extents_um"][2] / 1_000_000.0 +dais = metres(dais_stamp) +dais_top = dais[2] + dais_stamp["half_extents_um"][2] / 1_000_000.0 +ground_z = entrance[2] +direction_length = math.hypot(next_ramp[0] - entrance[0], next_ramp[1] - entrance[1]) +direction = ((next_ramp[0] - entrance[0]) / direction_length, + (next_ramp[1] - entrance[1]) / direction_length) +outward_length = math.hypot(entrance[0] - chamber[0], entrance[1] - chamber[1]) +outward = ((entrance[0] - chamber[0]) / outward_length, + (entrance[1] - chamber[1]) / outward_length) +if not mesh.is_file() or mesh.stat().st_size == 0: + raise RuntimeError(f"missing mesh export {mesh}") + +voxel_um, triangles, tile_count = PROFILE.read_scene(str(mesh), 0, + floor_top - 8.0, entrance[2] + 9.0, + (min(entrance[0], chamber[0]) - 20.0, max(entrance[0], chamber[0]) + 20.0, + min(entrance[1], chamber[1]) - 20.0, max(entrance[1], chamber[1]) + 20.0)) +bounds = (min(entrance[0], chamber[0]) - 20.0, max(entrance[0], chamber[0]) + 20.0, + min(entrance[1], chamber[1]) - 20.0, max(entrance[1], chamber[1]) + 20.0) +buckets = PROFILE.build_buckets(triangles, True, bounds, cell_size=1.0) +ground_candidates = [] +for ix in range(-20, 21): + for iy in range(-20, 21): + x, y = portal[0] + ix * 0.5, portal[1] + iy * 0.5 + dx, dy = x - portal[0], y - portal[1] + distance = math.hypot(dx, dy) + if not 6.0 <= distance <= 10.0: + continue + hit = PROFILE.sample_surface(buckets, x, y, True, 1.0, + preferred_height=ground_z) + if hit is None or abs(hit[0] - ground_z) > 3.0 or hit[1][2] < 0.85: + continue + alignment = max(-1.0, min(1.0, + (dx * mouth_normal[0] + dy * mouth_normal[1]) / distance)) + angle = math.acos(alignment) + score = abs(distance - 7.0) + angle * 4.0 + ground_candidates.append((score, x, y, hit[0])) +if not ground_candidates: + raise RuntimeError("no walkable mesh ground lies 6 to 10 m from the cave mouth") +_, ground_x, ground_y, outside_ground = min(ground_candidates) +inside_floor = PROFILE.sample_surface(buckets, chamber[0], chamber[1], True, 1.0, + preferred_height=floor_top) +if inside_floor is None or abs(inside_floor[0] - floor_top) > 1.5: + raise RuntimeError(f"chamber floor mesh hit {inside_floor} does not match designed floor {floor_top:.3f} m") +floor_top = inside_floor[0] + +ground_camera = (ground_x, ground_y, outside_ground + 1.6) +entry_ground = PROFILE.sample_surface(buckets, entrance[0], entrance[1], True, 1.0, + preferred_height=ground_z) +if entry_ground is None: + raise RuntimeError("cave mouth has no walkable ground surface in the exported mesh") +entry_camera = (entrance[0] + mouth_normal[0] * 3.8, + entrance[1] + mouth_normal[1] * 3.8, ground_z + 1.6) +entry_target_floor = edge_point(edge, 0.30) +entry_target = (entry_target_floor[0], entry_target_floor[1], entry_target_floor[2] + 1.4) +to_dais = (dais[0] - chamber[0], dais[1] - chamber[1]) +to_dais_length = math.hypot(*to_dais) +opposite_dais = (-to_dais[0] / to_dais_length, -to_dais[1] / to_dais_length) +inside_x = chamber[0] +inside_y = chamber[1] +inside_floor = PROFILE.sample_surface(buckets, inside_x, inside_y, True, 1.0, + preferred_height=floor_top) +inside_eye_z = inside_floor[0] + 1.6 +chamber_camera = (chamber[0] + opposite_dais[0] * 5.5, + chamber[1] + opposite_dais[1] * 5.5, inside_eye_z) +chamber_target = (dais[0], dais[1], dais_top - 0.25) +overview_camera = (entrance[0] + outward[0] * 18.0, + entrance[1] + outward[1] * 18.0, outside_ground + 15.0) +overview_target = ((entrance[0] + chamber[0]) * 0.5, + (entrance[1] + chamber[1]) * 0.5, + (ground_z + floor_top) * 0.5) +inward = (-outward[0], -outward[1]) +clip_point = (entrance[0] + outward[0] * 0.5, + entrance[1] + outward[1] * 0.5) +overview_clip = (inward[0], inward[1], 0.0, + -(inward[0] * clip_point[0] + inward[1] * clip_point[1])) + +views = ( + ("ground-entrance", ground_camera, (portal[0], portal[1], portal[2]), + (portal[0], portal[1], ground_z, 20.0), -509.0), + ("entrance-looking-in", entry_camera, entry_target, (entrance[0], entrance[1], entrance[2], 16.0), -507.0), + ("inside-to-dais", chamber_camera, chamber_target, + (chamber[0], chamber[1], chamber[2], 15.0), chamber[2]), + ("overview-entrance-and-chamber", overview_camera, overview_target, + ((entrance[0] + chamber[0]) * 0.5, + (entrance[1] + chamber[1]) * 0.5, + (ground_z + floor_top) * 0.5, 34.0), -509.0), +) + +for name, camera, target, focus, height_center in views: + png = PNG_DIR / f"{name}.png" + jpg = CAPTURE_DIR / f"{name}.jpg" + yaw, pitch = camera_angles(camera, target) + fov = (80.0 if name == "overview-entrance-and-chamber" else + 70.0 if name in ("inside-to-dais", "entrance-looking-in") else 65.0 + if name == "ground-entrance" else 60.0) + if name == "ground-entrance": + fov = 55.0 + print(f"[L46CaptureCamera] view={name} camera_m=({fmt(camera)}) " + f"target_m=({fmt(target)}) yaw_deg={yaw:.3f} pitch_deg={pitch:.3f} " + f"fov_deg={fov:.1f} entrance_distance_m={math.dist(camera, portal):.3f}", flush=True) + render = [str(RENDERER), str(mesh), str(png), "--size=1600x900", f"--fov-deg={fov:.1f}", + f"--camera-m={fmt(camera)}", f"--yaw-deg={yaw:.6f}", f"--pitch-deg={pitch:.6f}", + "--shade=readable", "--headlamp=on", "--height-tint=on", + "--light-direction=0.3,-0.4,0.85", + f"--height-center-m={(floor_top if name == 'inside-to-dais' else height_center):.6f}", + f"--height-range-m={4 if name == 'inside-to-dais' else 16}", + "--tile-skirts=off", "--tile-edges=off", + f"--focus-region={fmt(focus)}"] + if name == "overview-entrance-and-chamber": + render.append(f"--clip-plane={fmt(overview_clip)}") + run_logged(f"{name}-render", render, capture=True) + for quality in (4, 6, 8): + run_logged(f"{name}-jpeg-q{quality}", ["ffmpeg", "-nostdin", "-hide_banner", + "-loglevel", "error", "-y", "-i", str(png), "-frames:v", "1", + "-q:v", str(quality), str(jpg)]) + if jpg.is_file() and 0 < jpg.stat().st_size <= 300_000: + break + if not jpg.is_file() or not 0 < jpg.stat().st_size <= 300_000: + raise RuntimeError(f"{jpg.name} exceeds the 300 KB capture limit") + quality_log = run_logged(f"{name}-quality", [sys.executable, + str(VERIFY / "capture_quality.py"), str(jpg)]) + print(f"[L46CaptureJPEG] file={jpg} bytes={jpg.stat().st_size} " + f"quality_log={quality_log.strip().replace(chr(10), ' ')} status=PASS", flush=True) + + +def region_triangles(center, radius_xy, z_min, z_max): + selected = [] + for triangle in triangles: + points = tuple(vertex[0] for vertex in triangle[2]) + centroid = tuple(sum(point[axis] for point in points) / 3.0 for axis in range(3)) + if (math.hypot(centroid[0] - center[0], centroid[1] - center[1]) <= radius_xy + and z_min <= centroid[2] <= z_max): + selected.append(triangle) + return selected + + +def tunnel_wall_triangles(edge, t, height_above_floor): + center = edge_point(edge, t) + before = edge_point(edge, max(0.0, t - 0.01)) + after = edge_point(edge, min(1.0, t + 0.01)) + tx, ty = after[0] - before[0], after[1] - before[1] + tangent_length = math.hypot(tx, ty) + tx, ty = tx / tangent_length, ty / tangent_length + expected_radius = edge["bore_radius_um"] / 1_000_000.0 * math.sqrt( + 1.0 - (height_above_floor / (edge["bore_height_um"] / 1_000_000.0)) ** 2) + selected = [] + for triangle in triangles: + points = tuple(vertex[0] for vertex in triangle[2]) + centroid = tuple(sum(point[axis] for point in points) / 3.0 for axis in range(3)) + dx, dy = centroid[0] - center[0], centroid[1] - center[1] + longitudinal = dx * tx + dy * ty + lateral = abs(-dy * tx + dx * ty) + vertical = centroid[2] - center[2] + if (abs(longitudinal) <= 1.5 + and abs(lateral - expected_radius) <= 0.7 + and abs(vertical - height_above_floor) <= 0.75): + selected.append(triangle) + print(f"[L46TunnelWallWindow] t={t:.3f} center_m=({center[0]:.3f}," + f"{center[1]:.3f},{center[2]:.3f}) expected_radius_m={expected_radius:.3f} " + f"longitudinal_half_width_m=1.500 vertical_half_width_m=0.750 " + f"triangles={len(selected)}", flush=True) + return selected + + +def audit_shape(name, region, center): + metrics = SHAPE.analyze_triangles(region, center[:2], center[2], + crop_to_center=False, include_sides=True) + passed = (metrics["triangles"] > 0 + and metrics["planar_area"] < SHAPE.PLANE_LIMIT_M2 + and metrics["straight"] <= SHAPE.STRAIGHT_LIMIT_M + and metrics["corners"] == 0) + print(f"[LandmarkBuiltShape] region={name} triangles={metrics['triangles']} " + f"planar_patch_m2={metrics['planar_area']:.4f} " + f"straight_run_m={metrics['straight']:.4f} sharp_corners={metrics['corners']} " + f"corner_turn_max_deg={metrics['corner_turn_max']:.2f} " + f"thresholds=({SHAPE.PLANE_LIMIT_M2:.2f}m2,{SHAPE.STRAIGHT_LIMIT_M:.2f}m," + f"{SHAPE.CORNER_TURN_LIMIT_DEG:.0f}deg) status={'PASS' if passed else 'FAIL'}", + flush=True) + if not passed: + raise RuntimeError(f"L37 built-shape thresholds failed in {name}") + return metrics + + +entrance_metrics = audit_shape("cave-mouth", region_triangles( + portal, 5.0, ground_z + 0.8, ground_z + 4.5), + (portal[0], portal[1], ground_z)) +tunnel_mid = edge_point(edge, 0.58) +tunnel_metrics = audit_shape("tunnel-wall-edge", tunnel_wall_triangles( + edge, 0.58, 1.2), + (tunnel_mid[0], tunnel_mid[1], tunnel_mid[2] + 1.2)) +dais_surface = PROFILE.sample_surface(buckets, dais[0], dais[1], True, 1.0, + preferred_height=dais_top - 0.4) +if dais_surface is None: + raise RuntimeError("dais stamp has no upward-facing surface in the exported mesh") +dais_rise = dais_surface[0] - floor_top +print(f"[LandmarkDaisSurface] center_m=({dais[0]:.3f},{dais[1]:.3f}) " + f"floor_z_m={floor_top:.3f} top_z_m={dais_surface[0]:.3f} " + f"rise_m={dais_rise:.3f} minimum_m=1.000 " + f"status={'PASS' if dais_rise >= 1.0 else 'FAIL'}", flush=True) +if dais_rise < 1.0: + raise RuntimeError(f"dais rises only {dais_rise:.3f} m above the chamber floor") +dais_metrics = audit_shape("dais-platform", region_triangles( + dais, 1.7, floor_top + 0.6, dais_top + 0.4), + (dais[0], dais[1], dais_surface[0])) +metrics_path = ARTIFACT_DIR / "landmark-shape-metrics.json" +metrics_path.write_text(json.dumps({ + "graph_vocabulary_version": layout["cavern_graph_vocabulary_version"], + "entry_m": entrance, "portal_m": portal, "entry_ground_m": ground_z, + "chamber_center_m": chamber, "chamber_floor_m": floor_top, + "tunnel_midpoint_m": tunnel_mid, + "dais_center_m": dais, "dais_top_m": dais_top, + "dais_surface_m": dais_surface[0], "dais_rise_m": dais_rise, + "l37_thresholds": {"planar_patch_m2_lt": SHAPE.PLANE_LIMIT_M2, + "straight_run_m_lte": SHAPE.STRAIGHT_LIMIT_M, + "corner_turn_deg_lt": SHAPE.CORNER_TURN_LIMIT_DEG}, + "entrance_arch": entrance_metrics, "tunnel_wall": tunnel_metrics, + "dais_platform": dais_metrics, + "mesh_tiles": tile_count, "mesh_voxel_um": voxel_um, +}, indent=2) + "\n", encoding="utf-8") +print(f"[L46CaptureShapeMetrics] file={metrics_path} status=PASS", flush=True) diff --git a/Standalone/Research/WorldAssemblerM3/Linux46/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux46/REPORT.md new file mode 100644 index 0000000..a37b1e8 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux46/REPORT.md @@ -0,0 +1,36 @@ +# Linux 46 — cavern graph slice 1 + +## Sweep and resolved graph + +The ramp is one cubic-Bezier sweep. Each field query finds the closest XY point with a 17-point seed and five Newton refinements; the curve's Z coordinates define the cubic floor profile. A continuous rounded bore uses a 2.6 m radius, 4.8 m height and 0.3 m smooth blend. The 20 mm relief is applied to the bore. A separate hard floor cap runs after smooth unions, relief, passages and stamps. It preserves native negative density and adds at most 0.45 m of support only where the field is air. The room and edge conservative bounds feed `World::isTileShapeFree`. + +Resolved snapshots can carry optional cavern graph vocabulary version 1 records: room `landmark/sw-arrival-chamber-v1` and edge `landmark/sw-arrival-chamber-tunnel-v1`, anchored from the surface-ground approach to that room. Graph fields are serialized and included in canonical identity only when records exist. With the demo off, the graph is absent and the L45 identities remain unchanged. + +The 57 ramp carves and 107 support pillars are gone. The room is one graph node and the tunnel one edge. Four resolved stamps remain for the two mouth carves, room floor and dais. + +## Audits + +- Reachability: pass from walkable ground to the chamber floor; 58 path samples, 5,974 capsule samples, no blocked or unsupported samples, maximum grade 14.179°. Buried L43 control: expected fail. +- Headroom: 2.800 m minimum above the capsule, against 0.300 m required. +- Ripple: tunnel floor peak 0.0018 m over 202 samples (0.015 m limit); wall peak 0.0232 m over 138 samples (0.035 m limit, 0.25–1.0 m band). L45 segmented-ramp control: expected floor-ripple fail at 0.0179 m against 0.015 m. +- Built shape (L37 limits: planar patch <0.50 m², straight run ≤0.75 m, no sharp corners): entrance 0.4902 m² / 0.5470 m / 0; tunnel wall 0.3042 m² / 0.6225 m / 0; dais 0.2816 m² / 0.7500 m / 0. +- Inside-tunnel and near-tunnel camera hashes match at W1/W8/W12. Random-camera determinism also passed. + +## Hashes, cost and tiers + +The clean Clang and GCC builds share source stamp `20d1ae654440b22c6fdf9c91dd39c809b3063b54da6f2f4f44acf78894b7cf3b`. Owner JSON SHA-256 remains `7b095f9bcc6b90133d5a463f99f0b571133b2c549ce1ecf2d64fe9175e41e183`. Demo-off FI/SW layout IDs are `b7a7d9ecc48d8f26` / `8a52e84c26ed92d2`; their 12-hash signatures match L45: FI `9820f4f38b189b3f7263810e7b3ff62aa502b21f476ffcfdbc7761a65cb07d58`, SW `a1e9268a4bbcc77d2a1ac54095df6851a70bab65c43ae50b332ed75132617834`. + +Five demo-off cost pairs against committed L45: FI median 16.315 → 16.401 s (+0.53%); SW 1.720 → 1.730 s (+0.58%). Fast passed in 202.7 s, medium in 571.9 s, and final full in 1605.2 s. Four captures are in `captures/`: `ground-entrance.jpg` (39,430 B), `entrance-looking-in.jpg` (45,070 B), `inside-to-dais.jpg` (28,310 B), `overview-entrance-and-chamber.jpg` (62,735 B). Each is below 300 KB. Fourteen of fifteen fast trials were used. + +## Failed runs + +The first medium capture audit sampled a broad tunnel volume and failed the built-shape limit; the final wall annulus at curve parameter 0.58 passed. The first full attempt passed its 30 determinism cells but failed extracting an 806 MB historical archive and filled `/tmp`. The final full run used a temporary Git wrapper to export only the build inputs (Core, Source, Compat and build scripts); its historical build, cost pairs and all full checks passed. No tracked verifier change was needed. + +## Orchestrator review (Claude) + +Accepted. The stamp matches the final Clang and GCC builds (`20d1ae65…`); demo-off owner hashes are identical to L45. +The segmented ramp (57 carves plus 107 pillars) is replaced by one continuous Bezier sweep with a hard floor cap; +headroom rose from 0.085 m to 2.8 m. Both controls fail as they should: the L45 ramp on floor ripple, and the buried +L43 layout on reachability. Follow-up for the verifier: the full tier's baseline export copies the whole HEAD tree +(about 785 MB) into `/tmp`, which is RAM-backed on this machine, and never deletes it. About 6 GB had accumulated. +It must export only the build inputs, into `Saved/Linux/`, and clean up after itself. diff --git a/Standalone/Research/WorldAssemblerM3/Linux46/captures/entrance-looking-in.jpg b/Standalone/Research/WorldAssemblerM3/Linux46/captures/entrance-looking-in.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3f54447c145482bfd40590693ab5fca479f3e46d GIT binary patch literal 45070 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

`%Q^!Dq(xus?V3( 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 WR(CVdP~pC_#PumZIzj)b`+ouE$SiLF literal 0 HcmV?d00001 diff --git a/Standalone/Research/WorldAssemblerM3/Linux46/captures/ground-entrance.jpg b/Standalone/Research/WorldAssemblerM3/Linux46/captures/ground-entrance.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9ccfdf056f32f2e1e68e99e67c258501d1e7905d GIT binary patch literal 39430 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 literal 0 HcmV?d00001 diff --git a/Standalone/Research/WorldAssemblerM3/Linux46/captures/inside-to-dais.jpg b/Standalone/Research/WorldAssemblerM3/Linux46/captures/inside-to-dais.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8c9167865e07ae1de0deba6db13ab4c5c09416d8 GIT binary patch literal 28310 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 z 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 X_Sxd^FGGu+%#fDC8Z2<^7^wO`uQ!)+ literal 0 HcmV?d00001 diff --git a/Standalone/Research/WorldAssemblerM3/Linux46/captures/overview-entrance-and-chamber.jpg b/Standalone/Research/WorldAssemblerM3/Linux46/captures/overview-entrance-and-chamber.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e0f4f84995b1acc8835d0f93075bae10ac9bbf84 GIT binary patch literal 62735 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 literal 0 HcmV?d00001 diff --git a/Standalone/Research/WorldAssemblerM3/Research13/REPORT.md b/Standalone/Research/WorldAssemblerM3/Research13/REPORT.md new file mode 100644 index 0000000..b84e9e9 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Research13/REPORT.md @@ -0,0 +1,44 @@ +## Key recommendations + +1. **Add one shared curve sweep evaluator** for passage bores, the L45 ramp and R12 graph edges. Evaluate a smooth spline over global arc length, rather than taking a minimum over piecewise-linear capsules or discrete ramp stamps. +2. **Accelerate the global search with a conservative BVH.** Seed each surviving spline span from its chord projection, refine with a safeguarded solver, and compare all candidates whose bounds could beat the current best. One Newton solve from one seed is not a global closest-point solve. +3. **Keep the floor cap separate from tube ownership.** Use an explicit floor profile and R3’s floor-near candidate rule for overlapping bends and coils; keep relief and smooth unions away from the floor zero set. +4. **Use a sphere sweep as the fast, bounded field.** A general framed cross-section sweep is not guaranteed to be a true distance field on tight curves. Use rotation-minimising frames for its orientation, but validate curvature and retain conservative bounds for culling. + +## Math and exactness + +Represent the spine as a C1 or preferably C2 cubic spline \(C(s)\), parameterised by arc length \(s\). Interpolate radius \(r(s)\) and floor height \(h(s)\) as explicit route attributes. The round sweep is: + +\[ +\phi_{\text{tube}}(p)=\min_{s\in[0,L]} \left(\|p-C(s)\|-r(s)\right) +\] + +This gives a continuous field with the correct zero set for the union of swept balls. It is exact distance outside that union; inside overlapping spheres it is only a distance bound, as the swept-volume literature cautions. The exact nearest-point equation for a cubic Bezier has degree five; Iñigo Quílez’s compact Bezier distance function is for a quadratic curve, so it is a useful small-case reference, not a complete cubic-tube solution. [IQ’s 2D distance functions](https://iquilezles.org/articles/distfunctions2d/), [point-to-NURBS distance paper](https://doi.org/10.1016/j.cad.2008.06.008), [swept-volume SDF analysis](https://doi.org/10.1145/3610548.3618196) + +For the floor, define a separate cap from the selected route branch. For a single-valued floor graph \(h(x,y)\), a useful field is \((h(x,y)-z)/\sqrt{1+\lambda^2}\), where \(\lambda\) bounds the floor slope. Its zero set is the authored floor; it is exact distance for a planar floor and a conservative bound otherwise. Combine the tube and cap as an intersection, then carve using Core’s positive-air convention. Do not smooth-blend the floor edge or apply wall relief at the floor: either can shift the walk surface. The cave-network paper uses this slope-bounded height-field form and notes that arbitrary cross-section sweeps along high-curvature curves do not generally yield a continuous SDF. [Paris et al., *Synthesizing Geologically Coherent Cave Networks*](https://diglib.eg.org/items/a4e7985c-af52-4b55-bd1a-eeeff6c47422) + +A circular sweep needs no frame. For an asymmetric cross-section, precompute a rotation-minimising frame along the arc-length table; the double-reflection method is a practical low-cost choice and avoids the Frenet frame’s flips at inflections. [Wang et al., *Computation of Rotation Minimizing Frames*](https://doi.org/10.1145/1330511.1330513) + +## Cost and culling + +Build an arc-length lookup table, spline-span bounds and optional frames once when resolving the layout. At sample time, traverse a BVH ordered by the conservative lower bound `distance(point, spanBounds) - maxRadius`. The current passage BVH already uses the analogous `AABB distance - maximum radius` bound; spline bounds must include each cubic span’s control hull, not just its endpoints. + +Use the chord projection to seed a bracketed Newton or similar local solve on each promising span, include span endpoints, and retain the lowest result globally. A single seed can converge to the wrong local minimum near a bend or helix. For exact refinement, isolate all roots of the quintic stationary equation on the few surviving spans; variable radius adds a derivative term and makes the solve more costly. [The NURBS distance method](https://doi.org/10.1016/j.cad.2008.06.008) is a relevant example of clipping candidate regions before numerical refinement. + +The exact minimum of 1-Lipschitz sphere fields is itself 1-Lipschitz. An *approximate* solver’s sampled value is not automatically a safe culling bound: it may overestimate the minimum. Keep a certified lower bound from the BVH/subdivision search for skip decisions, or stop with a known error gap and subtract that error. Report node visits, candidate spans, refinements and bound gaps per sample; a 256-span curve has about eight BVH levels before pruning, but tight overlapping coils may leave many candidates alive. Unreal’s spline API exposes closest-key queries, distance-along-spline queries and a reparameterisation table, matching the useful precomputed lookup pattern for game workloads. [Epic’s `USplineComponent` API](https://dev.epicgames.com/documentation/unreal-engine/API/Runtime/Engine/USplineComponent) + +## Floor ownership and code fit + +For overlapping coils, do **not** use the closest 3D centerline to choose the floor. R3 recommends considering only floor candidates within half a voxel (0.125 m) vertically and inside the bore radius, then choosing the nearest floor point with a stable ID tie-break. Its synthetic helix showed roughly 3.37 m peak-to-peak error for closest-centerline ownership versus 0.029 m for floor-near ownership; it did not prove the rule fixes the real L38 ripple, so the real FI Style 2 route still needs measurement. [R3 floor ownership]() · [R12 graph-field recommendation]() + +Put a shared evaluator in a Core internal sweep helper, with a resolved curve record carrying stable ID, cubic controls, arc-length attributes, floor/radius profiles and conservative bounds. + +- **Passages:** adapt [`ApplyResolvedStratePassageCarves`]() to call it; retain bridge, mouth and support policy around the shared geometry query. Update [`BuildPassageNearestNode`]() and its lower bounds for spline hulls. +- **L45 ramp:** replace the repeated ramp carve/support requests in [`MakeLandmarkChamberDemoRequests`]() with one floor-capped curve record. Route its evaluation through the shared composition/stamp path in [`ShapeWorldInternal.h`](). +- **R12 graph edges:** store the same curve/floor data on each resolved edge and evaluate it from [`BindResolvedStratumFields`](); include blend, relief and floor-support reach in tile and shape-free bounds. + +## Failure modes and suggested checks + +The main risks are missed local minima, spline overshoot, curvature causing a swept cross-section to fold or overlap, variable radius changing the floor, frame flips, and hard floor-owner changes across nearby branches. Validate radius slope and curvature; use monotone floor interpolation where route height must not overshoot. Sphere-swept canal surfaces also become degenerate when radius changes as fast as the spine advances. [Canal surface conditions](https://mathworld.wolfram.com/CanalSurface.html) + +For a zero-relief tight S-bend with nonuniform knot spacing, sample the floor crossing every 0.125 m of arc length against its analytic \(h(s)\): the analytic ripple should be 0, with measured error only from the solver/voxel tolerance and no peak at the knot spacing. Then run an overlapping helix with floor-near ownership, and the real L35/L36/L45 controls. Track floor peak-to-peak error, the 0.5–8 m ripple spectrum, culling false negatives, solver gaps and candidate counts. L35 and L36 documented the passage wall/floor beats, while L45 records the 57-carve/107-support ramp ribs. [L35]() · [L36]() · [L45]() \ No newline at end of file diff --git a/Standalone/Source/StreamBenchmarkHeadless.cpp b/Standalone/Source/StreamBenchmarkHeadless.cpp index 12287d3..35c8717 100644 --- a/Standalone/Source/StreamBenchmarkHeadless.cpp +++ b/Standalone/Source/StreamBenchmarkHeadless.cpp @@ -56,6 +56,7 @@ namespace int RenderDistanceOverride=0; bool FloatingIslandsRecipe=false; bool LandmarkChamberDemo=false; + bool LandmarkL45RampControl=false; bool AuditLandmarkReachability=false; bool TlsProbeRequested=false; int TlsProbeWorkers=0; @@ -115,10 +116,14 @@ namespace const VoxelForgeCore::WorldAssemblyManifest& worldAssembly, std::int32_t seed, std::vector& requests, + std::vector& cavernRooms, + std::vector& cavernEdges, + bool legacyRampControl, std::string& error) { using namespace VoxelForgeCore; requests.clear(); + cavernRooms.clear();cavernEdges.clear(); const auto surface=std::find_if(layout.strates.begin(),layout.strates.end(), [](const auto& value){return value.generatorKind=="SurfaceWorld";}); if(surface==layout.strates.end()) @@ -444,6 +449,100 @@ namespace return startZ+descent*std::clamp((beforeFirstTurn+afterFirstTurn) /slopedLength,0.0,1.0); }; + // The resolved edge is one cubic sweep. Its floor controls are independent + // of the curved XY centerline, so the floor remains an explicit profile. + const double startFloor=surfaceHeight; + const double endFloor=chamberCenterZ-0.5; + const double firstLength=std::hypot(firstTurn.x-start.x,firstTurn.y-start.y); + const double lastLength=std::hypot(end.x-firstTurn.x,end.y-firstTurn.y); + const double estimatedLength=firstLength+lastLength; + const double handleLength=estimatedLength/3.0; + const double firstDirX=(firstTurn.x-start.x)/firstLength; + const double firstDirY=(firstTurn.y-start.y)/firstLength; + const double lastDirX=(end.x-firstTurn.x)/lastLength; + const double lastDirY=(end.y-firstTurn.y)/lastLength; + std::array,4> curve={{ + {start.x,start.y,startFloor}, + {start.x+firstDirX*handleLength,start.y+firstDirY*handleLength, + startFloor+(endFloor-startFloor)/3.0}, + {end.x-lastDirX*handleLength,end.y-lastDirY*handleLength, + startFloor+2.0*(endFloor-startFloor)/3.0}, + {end.x,end.y,endFloor}}}; + ResolvedCavernRoomV1 room; + room.stableId=chamber.stableId;room.strateIndex=surface->strateIndex; + room.centerMicrometres=chamber.anchorPositionMicrometres; + room.halfExtentsMicrometres=chamber.halfExtentsMicrometres; + room.floorMicrometres=static_cast(std::llround(endFloor*1000000.0)); + room.smoothWidthMicrometres=chamber.smoothWidthMicrometres; + room.irregularityAmplitudeMicrometres=chamber.irregularityAmplitudeMicrometres; + room.irregularityWavelengthMicrometres=chamber.irregularityWavelengthMicrometres; + if(!legacyRampControl)cavernRooms.push_back(room); + ResolvedCavernEdgeV1 edge; + edge.stableId="landmark/sw-arrival-chamber-tunnel-v1"; + edge.startAnchorStableId=passage->stableId+"/surface-ground-approach"; + edge.endRoomStableId=room.stableId;edge.strateIndex=surface->strateIndex; + edge.boreRadiusMicrometres=2600000; + edge.boreHeightMicrometres=4800000; + edge.floorSupportDepthMicrometres=450000; + edge.smoothWidthMicrometres=300000; + edge.irregularityAmplitudeMicrometres=20000; + edge.irregularityWavelengthMicrometres=3200000; + double measuredCurveLength=0.0,arcAtThird=0.0,arcAtTwoThirds=0.0; + std::array previousCurvePoint={curve[0][0],curve[0][1]}; + for(int sample=1;sample<=256;++sample) + { + const double t=static_cast(sample)/256.0,one=1.0-t; + std::array point{}; + for(std::size_t axis=0;axis<3;++axis) + point[axis]=one*one*one*curve[0][axis] + +3.0*one*one*t*curve[1][axis]+3.0*one*t*t*curve[2][axis] + +t*t*t*curve[3][axis]; + const double dx=point[0]-previousCurvePoint[0]; + const double dy=point[1]-previousCurvePoint[1]; + const double horizontal=std::hypot(dx,dy); + measuredCurveLength+=horizontal; + if(sample==85)arcAtThird=measuredCurveLength; + if(sample==171)arcAtTwoThirds=measuredCurveLength; + previousCurvePoint={point[0],point[1]}; + } + const double targetFloorThird=startFloor+(endFloor-startFloor) + *(arcAtThird/measuredCurveLength); + const double targetFloorTwoThirds=startFloor+(endFloor-startFloor) + *(arcAtTwoThirds/measuredCurveLength); + const double firstEquation=targetFloorThird + -(8.0*startFloor+endFloor)/27.0; + const double secondEquation=targetFloorTwoThirds + -(startFloor+8.0*endFloor)/27.0; + curve[1][2]=1.5*(2.0*firstEquation-secondEquation); + curve[2][2]=1.5*(2.0*secondEquation-firstEquation); + double maximumCurveGrade=0.0; + std::array previousCurvePosition=curve[0]; + for(int sample=1;sample<=256;++sample) + { + const double t=static_cast(sample)/256.0,one=1.0-t; + std::array point{}; + for(std::size_t axis=0;axis<3;++axis) + point[axis]=one*one*one*curve[0][axis] + +3.0*one*one*t*curve[1][axis]+3.0*one*t*t*curve[2][axis] + +t*t*t*curve[3][axis]; + const double horizontal=std::hypot(point[0]-previousCurvePosition[0], + point[1]-previousCurvePosition[1]); + maximumCurveGrade=std::max(maximumCurveGrade,std::atan2( + std::abs(point[2]-previousCurvePosition[2]), + std::max(1.0e-6,horizontal))*180.0/3.14159265358979323846); + previousCurvePosition=point; + } + for(std::size_t i=0;i( + std::llround(curve[i][axis]*1000000.0)); + if(!legacyRampControl)cavernEdges.push_back(edge); + std::printf("[CavernGraphPlan] rooms=1 edges=1 room=%s edge=%s curve=cubic-bezier floor=cubic-profile length_m=%.3f max_grade_deg=%.3f bore_radius_m=2.600 bore_height_m=4.800 floor_um=%lld..%lld\n", + room.stableId.c_str(),edge.stableId.c_str(),measuredCurveLength,maximumCurveGrade, + static_cast(room.floorMicrometres), + static_cast(edge.controlPointsMicrometres[0][2])); + if(maximumCurveGrade>15.0) + {error="landmark cavern edge exceeds the 15 degree walk grade";return false;} const double pointSpacing=0.6; const std::size_t rampCount=static_cast(std::ceil(totalLength/pointSpacing)); std::vector rampCarves; @@ -506,12 +605,15 @@ namespace } requests.push_back(std::move(mouthStamp)); requests.push_back(std::move(mouthPortal)); - requests.insert(requests.end(),std::make_move_iterator(rampCarves.begin()), - std::make_move_iterator(rampCarves.end())); - requests.push_back(std::move(chamber)); - requests.insert(requests.end(),std::make_move_iterator(rampSupports.begin()), - std::make_move_iterator(rampSupports.end())); - requests.push_back(std::move(mouthLanding)); + if(legacyRampControl) + { + requests.insert(requests.end(),std::make_move_iterator(rampCarves.begin()), + std::make_move_iterator(rampCarves.end())); + requests.push_back(std::move(chamber)); + requests.insert(requests.end(),std::make_move_iterator(rampSupports.begin()), + std::make_move_iterator(rampSupports.end())); + } + if(legacyRampControl)requests.push_back(std::move(mouthLanding)); requests.push_back(std::move(chamberFloor)); requests.push_back(std::move(dais)); (void)seed; @@ -571,7 +673,8 @@ namespace " --mesh-export=FILE (write the exact generated tile meshes for offline rendering)\n" " --profile-column-overlap --far-horizon-lod8-step=N --force-generate\n" " --seed=N --recipe=surface-world|floating-islands\n" - " --landmark-chamber-demo (opt-in natural SW landmark chamber and ramp)\n" + " --landmark-chamber-demo (opt-in SW cavern room and swept tunnel)\n" + " --landmark-l45-ramp-control (use the buried L45 stamp geometry as an audit control)\n" " --resolved-layout-output=FILE (canonical plan-time layout snapshot)\n" " --resolved-layout-only (exit after plan and snapshot validation)\n" " --audit-resolved-layout-boundaries (query Core at each recorded endpoint)\n" @@ -640,6 +743,7 @@ namespace else {std::fprintf(stderr,"Invalid --recipe value: %.*s\n",static_cast(recipe.size()),recipe.data());return false;} } else if(arg=="--landmark-chamber-demo")options.LandmarkChamberDemo=true; + else if(arg=="--landmark-l45-ramp-control")options.LandmarkL45RampControl=true; else if(arg.starts_with("--resolved-layout-output=")) options.ResolvedLayoutOutputPath=std::string(arg.substr(std::string_view("--resolved-layout-output=").size())); else if(arg.starts_with("--seed=")) @@ -722,6 +826,8 @@ namespace {std::fprintf(stderr,"--tls-probe-touch requires --tls-probe=N\n");return false;} if(options.RegionCrossing&&!options.HasCameraStep) {std::fprintf(stderr,"--region-crossing requires --camera-step=X,Y,Z\n");return false;} + if(options.LandmarkL45RampControl&&!options.LandmarkChamberDemo) + {std::fprintf(stderr,"--landmark-l45-ramp-control requires --landmark-chamber-demo\n");return false;} if(options.TlsProbeRequested)return true; if(!options.WorldAssemblerPrototype||!options.StreamBenchmark||!options.StreamPipeline) { @@ -820,10 +926,15 @@ int main(int argc,char** argv) if(options.LandmarkChamberDemo) { std::vector requests; - if(!MakeLandmarkChamberDemoRequests(resolvedLayout,worldAssembly,seed,requests,error) + std::vector rooms; + std::vector edges; + if(!MakeLandmarkChamberDemoRequests(resolvedLayout,worldAssembly,seed, + requests,rooms,edges,options.LandmarkL45RampControl,error) ||!VoxelForgeCore::ResolveResolvedLayoutStampsV2( - resolvedLayout,seed,requests,error)) - {std::fprintf(stderr,"Landmark chamber stamp resolution failed: %s\n",error.c_str());return 2;} + resolvedLayout,seed,requests,error) + ||!VoxelForgeCore::ResolveResolvedCavernGraphV1( + resolvedLayout,seed,rooms,edges,error)) + {std::fprintf(stderr,"Landmark cavern graph resolution failed: %s\n",error.c_str());return 2;} const auto audit=VoxelForgeCore::AuditResolvedLayoutStampPlacementV1(resolvedLayout); std::printf("[ResolvedStampPlacementAudit] vocabulary=%u records=%zu route_control=rejected band_control=rejected controls=%u/%u status=%s\n", resolvedLayout.stampVocabularyVersion,resolvedLayout.stamps.size(), @@ -849,9 +960,14 @@ int main(int argc,char** argv) stamp.irregularityAmplitudeMicrometres/1000000.0, stamp.irregularityWavelengthMicrometres/1000000.0, static_cast(stamp.irregularitySeed)); - std::printf("[LandmarkStampDemo] enabled=true seed=%d layout_id=%s passage_safe=true walk_route_clear=true version=%u deterministic_hash=seed,strate,stable_id status=PASS\n", - seed,resolvedLayout.stableIdentity.c_str(), - resolvedLayout.stampVocabularyVersion); + if(options.LandmarkL45RampControl) + std::printf("[LandmarkL45RampControl] enabled=true stamps=%zu graph_records=0 expected=segmented status=PASS\n", + resolvedLayout.stamps.size()); + else + std::printf("[LandmarkCavernGraphDemo] enabled=true seed=%d layout_id=%s rooms=%zu edges=%zu support_stamps=%zu passage_safe=true deterministic_hash=seed,strate,stable_id version=%u status=PASS\n", + seed,resolvedLayout.stableIdentity.c_str(), + resolvedLayout.cavernRooms.size(),resolvedLayout.cavernEdges.size(), + resolvedLayout.stamps.size(),resolvedLayout.cavernGraphVocabularyVersion); } const std::string layoutJson=resolvedLayout.ToJson(); const auto roundTrip=VoxelForgeCore::ResolvedLayoutSnapshotV3::FromJson(layoutJson,error); diff --git a/Standalone/Source/StreamBenchmarkPipeline.h b/Standalone/Source/StreamBenchmarkPipeline.h index a91e81e..366eb9b 100644 --- a/Standalone/Source/StreamBenchmarkPipeline.h +++ b/Standalone/Source/StreamBenchmarkPipeline.h @@ -130,6 +130,7 @@ namespace StandaloneStreamBenchmark std::uint64_t supportSamples=0,unsupportedSamples=0,gradeFailures=0; double maximumGradeDegrees=0.0,maximumSupportDepthMetres=0.0; double minimumClearanceMetres=std::numeric_limits::infinity(); + double minimumHeadroomMetres=std::numeric_limits::infinity(); double latestPointMinimumClearanceMetres=std::numeric_limits::infinity(); std::array latestPointMinimumClearancePosition{}; bool passed=false; @@ -153,13 +154,19 @@ namespace StandaloneStreamBenchmark if(surfacePlan==assembly->plan.strates.end())return result; const VoxelForgeCore::Heightfield2D surfaceField(assembly->recipe,*surfacePlan); if(!surfaceField.isValid())return result; - std::vector ramp; - for(const auto& stamp:layout.stamps) - if(stamp.stableId.starts_with("landmark/sw-arrival-chamber-ramp-")) - ramp.push_back(&stamp); - std::sort(ramp.begin(),ramp.end(),[](const auto* a,const auto* b) - {return a->stableIdstableId;}); - if(ramp.size()<2)return result; + const auto tunnel=std::find_if(layout.cavernEdges.begin(),layout.cavernEdges.end(), + [](const auto& edge) + {return edge.stableId=="landmark/sw-arrival-chamber-tunnel-v1";}); + std::vector legacyRamp; + if(tunnel==layout.cavernEdges.end()) + { + for(const auto& stamp:layout.stamps) + if(stamp.stableId.starts_with("landmark/sw-arrival-chamber-ramp-")) + legacyRamp.push_back(&stamp); + std::sort(legacyRamp.begin(),legacyRamp.end(),[](const auto* a,const auto* b) + {return a->stableIdstableId;}); + if(legacyRamp.size()<2)return result; + } const double voxelMetres=static_cast(std::max(1, coreVoxelSizeMicrometres))/1000000.0; @@ -177,12 +184,13 @@ namespace StandaloneStreamBenchmark return static_cast(shape.densityAt({x/voxelMetres, y/voxelMetres,z/voxelMetres}))*voxelMetres; }; - const auto samplePoint=[&](double x,double y,double centerZ,bool tunnel) + const auto samplePoint=[&](double x,double y,double centerZ,bool inTunnel, + bool measureHeadroom=false) { constexpr double FloorStep=0.01; double floorZ=centerZ-5.0; bool foundFloor=false; - if(tunnel) + if(inTunnel) { if(densityMetres(x,y,centerZ)>0.025) for(double offset=-0.10;offset>=-10.0;offset-=FloorStep) @@ -240,6 +248,23 @@ namespace StandaloneStreamBenchmark } } + if(measureHeadroom) + { + constexpr std::array,5> headOffsets={{{0.0,0.0}, + {0.24,0.0},{-0.24,0.0},{0.0,0.24},{0.0,-0.24}}}; + constexpr double HeadStep=0.05,HeadLimit=4.0; + for(const auto& offset:headOffsets) + { + double headroom=HeadLimit; + for(double rise=0.0;rise<=HeadLimit;rise+=HeadStep) + if(densityMetres(x+offset[0],y+offset[1], + floorZ+CapsuleHeight+rise)<0.0) + {headroom=std::max(0.0,rise-HeadStep);break;} + result.minimumHeadroomMetres=std::min( + result.minimumHeadroomMetres,headroom); + } + } + const std::array,5> supportOffsets={{{0.0,0.0}, {0.28,0.0},{-0.28,0.0},{0.0,0.28},{0.0,-0.28}}}; std::uint32_t supported=0; @@ -270,10 +295,16 @@ namespace StandaloneStreamBenchmark double floorZ=0.0,cost=std::numeric_limits::infinity(); int parent=-1; }; + const double edgeStartX=tunnel!=layout.cavernEdges.end() + ?tunnel->controlPointsMicrometres[0][0]/1000000.0 + :legacyRamp.front()->positionMicrometres[0]/1000000.0; + const double edgeStartY=tunnel!=layout.cavernEdges.end() + ?tunnel->controlPointsMicrometres[0][1]/1000000.0 + :legacyRamp.front()->positionMicrometres[1]/1000000.0; const int goalX=GridRadius+static_cast(std::llround( - (ramp.front()->positionMicrometres[0]/1000000.0-arrivalX)/GridStep)); + (edgeStartX-arrivalX)/GridStep)); const int goalY=GridRadius+static_cast(std::llround( - (ramp.front()->positionMicrometres[1]/1000000.0-arrivalY)/GridStep)); + (edgeStartY-arrivalY)/GridStep)); const auto nodeIndex=[](int x,int y){return y*GridWidth+x;}; std::vector surfaceNodes(static_cast(GridWidth*GridWidth)); std::uint64_t exploredNodes=0; @@ -364,7 +395,7 @@ namespace StandaloneStreamBenchmark double surfaceStartDistance=std::numeric_limits::infinity(); if(!surfaceRoute.empty()) { - path.reserve(surfaceRoute.size()+ramp.size()); + path.reserve(surfaceRoute.size()+128); const int startX=surfaceRoute.front()%GridWidth; const int startY=surfaceRoute.front()/GridWidth; surfaceStartDistance=GridStep*std::hypot(startX-GridRadius,startY-GridRadius); @@ -375,10 +406,39 @@ namespace StandaloneStreamBenchmark path.push_back({arrivalX+(ix-GridRadius)*GridStep, arrivalY+(iy-GridRadius)*GridStep,node.floorZ+0.9,false}); } - for(const auto* stamp:ramp) - path.push_back({stamp->positionMicrometres[0]/1000000.0, - stamp->positionMicrometres[1]/1000000.0, - stamp->positionMicrometres[2]/1000000.0,true}); + if(tunnel!=layout.cavernEdges.end()) + { + const auto curveValue=[&](double t,std::size_t axis) + { + const double one=1.0-t; + return (one*one*one*tunnel->controlPointsMicrometres[0][axis] + +3.0*one*one*t*tunnel->controlPointsMicrometres[1][axis] + +3.0*one*t*t*tunnel->controlPointsMicrometres[2][axis] + +t*t*t*tunnel->controlPointsMicrometres[3][axis])/1000000.0; + }; + double curveLength=0.0,previousCurveX=curveValue(0.0,0),previousCurveY=curveValue(0.0,1); + for(int sample=1;sample<=128;++sample) + { + const double t=static_cast(sample)/128.0; + const double x=curveValue(t,0),y=curveValue(t,1); + curveLength+=std::hypot(x-previousCurveX,y-previousCurveY); + previousCurveX=x;previousCurveY=y; + } + const int curveSteps=std::max(2,static_cast(std::ceil(curveLength/0.60))); + for(int sample=0;sample<=curveSteps;++sample) + { + const double t=static_cast(sample)/curveSteps; + path.push_back({curveValue(t,0),curveValue(t,1), + curveValue(t,2)+0.9,true}); + } + } + else + { + for(const auto* stamp:legacyRamp) + path.push_back({stamp->positionMicrometres[0]/1000000.0, + stamp->positionMicrometres[1]/1000000.0, + stamp->positionMicrometres[2]/1000000.0,true}); + } } std::printf("[LandmarkSurfaceRoute] reachable=%s goal_walkable=%s grid_step_m=%.2f start_offset_m=%.2f explored=%llu surface_points=%zu grade_limit_deg=15.000\n", surfaceRoute.empty()?"false":"true",surfaceGoalWalkable?"true":"false",GridStep,surfaceStartDistance, @@ -405,7 +465,7 @@ namespace StandaloneStreamBenchmark const auto capsuleBefore=result.capsuleSamples; const auto supportBefore=result.supportSamples; const bool tunnel=from.tunnel&&to.tunnel; - const auto [floorZ,supportOk]=samplePoint(x,y,centerZ,tunnel); + const auto [floorZ,supportOk]=samplePoint(x,y,centerZ,tunnel,true); allSupport&=supportOk; double grade=0.0; if(havePrior) @@ -421,7 +481,7 @@ namespace StandaloneStreamBenchmark ||!supportOk||grade>15.0)) { std::printf("[LandmarkWalkPoint] field=%.*s mode=%s x=%.3f y=%.3f center_z=%.3f floor_z=%.3f previous_x=%.3f previous_y=%.3f previous_floor_z=%.3f horizontal_step_m=%.3f blocked=%llu/%llu support_probes=%llu grade_deg=%.3f density_center_m=%.3f capsule_min_m=%.3f capsule_min_at_m=(%.3f,%.3f,%.3f)\n", - static_cast(label.size()),label.data(),tunnel?"ramp":"surface",x,y,centerZ,floorZ, + static_cast(label.size()),label.data(),tunnel?"tunnel":"surface",x,y,centerZ,floorZ, priorX,priorY,priorFloor,std::hypot(x-priorX,y-priorY), static_cast(result.blockedCapsuleSamples-blockedBefore), static_cast(result.capsuleSamples-capsuleBefore), @@ -437,7 +497,214 @@ namespace StandaloneStreamBenchmark } result.passed=result.pathSamples>1&&result.capsuleSamples>0 &&result.blockedCapsuleSamples==0&&allSupport&&allGrade - &&result.unsupportedSamples==0&&result.gradeFailures==0; + &&result.unsupportedSamples==0&&result.gradeFailures==0 + &&result.minimumHeadroomMetres>=0.300; + return result; + } + + struct FLandmarkTunnelRippleResult + { + std::size_t floorSamples=0,wallSamples=0; + double floorPeakPeriodMetres=0.0,floorPeakAmplitudeMetres=0.0; + double wallPeakPeriodMetres=0.0,wallPeakAmplitudeMetres=0.0; + bool valid=false,passed=false; + }; + + inline FLandmarkTunnelRippleResult AuditLandmarkTunnelRipples( + const VoxelForgeCore::World& shape, + const VoxelForgeCore::ResolvedLayoutSnapshotV3& layout, + std::uint32_t coreVoxelSizeMicrometres) + { + FLandmarkTunnelRippleResult result; + const double voxelMetres=static_cast(std::max(1, + coreVoxelSizeMicrometres))/1000000.0; + const auto densityMetres=[&](double x,double y,double z) + {return static_cast(shape.densityAt({x/voxelMetres,y/voxelMetres, + z/voxelMetres}))*voxelMetres;}; + struct FRoutePoint { double x=0.0,y=0.0,floor=0.0; }; + std::vector route; + const auto edge=std::find_if(layout.cavernEdges.begin(),layout.cavernEdges.end(), + [](const auto& value) + {return value.stableId=="landmark/sw-arrival-chamber-tunnel-v1";}); + if(edge!=layout.cavernEdges.end()) + { + constexpr int CurveSamples=512; + route.reserve(CurveSamples+1); + for(int index=0;index<=CurveSamples;++index) + { + const double t=static_cast(index)/CurveSamples,one=1.0-t; + const std::array weights={one*one*one,3.0*one*one*t, + 3.0*one*t*t,t*t*t}; + FRoutePoint point; + point.x=point.y=point.floor=0.0; + for(std::size_t control=0;control<4;++control) + { + point.x+=weights[control]*edge->controlPointsMicrometres[control][0]/1e6; + point.y+=weights[control]*edge->controlPointsMicrometres[control][1]/1e6; + point.floor+=weights[control]*edge->controlPointsMicrometres[control][2]/1e6; + } + route.push_back(point); + } + } + else + { + std::vector ramp; + for(const auto& stamp:layout.stamps) + if(stamp.stableId.starts_with("landmark/sw-arrival-chamber-ramp-")) + ramp.push_back(&stamp); + std::sort(ramp.begin(),ramp.end(),[](const auto* a,const auto* b) + {return a->stableIdstableId;}); + for(const auto* stamp:ramp) + route.push_back({stamp->positionMicrometres[0]/1e6, + stamp->positionMicrometres[1]/1e6, + stamp->positionMicrometres[2]/1e6-1.6}); + } + if(route.size()<2)return result; + std::vector arc(route.size(),0.0); + for(std::size_t index=1;index(route.size()-1, + static_cast(upper-arc.begin())); + const std::size_t lo=hi?hi-1:0; + const double span=arc[hi]-arc[lo]; + const double t=span>0.0?(distance-arc[lo])/span:0.0; + return FRoutePoint{std::lerp(route[lo].x,route[hi].x,t), + std::lerp(route[lo].y,route[hi].y,t), + std::lerp(route[lo].floor,route[hi].floor,t)}; + }; + const auto floorRoot=[&](double x,double y,double expected)->std::optional + { + constexpr double Step=0.025,Below=0.65,Above=0.35; + double priorZ=expected-Below; + double priorDensity=densityMetres(x,y,priorZ); + std::optional closest; + double closestError=std::numeric_limits::infinity(); + for(double z=priorZ+Step;z<=expected+Above+1.0e-9;z+=Step) + { + const double density=densityMetres(x,y,z); + if(priorDensity<0.0&&density>=0.0) + { + const double span=density-priorDensity; + const double root=span>0.0?priorZ+Step*(-priorDensity)/span:z; + const double error=std::abs(root-expected); + if(error> floorResiduals; + for(double distance=0.75;distance<=length-0.75;distance+=0.125) + { + const auto point=atDistance(distance); + const auto prior=atDistance(std::max(0.0,distance-0.1)); + const auto next=atDistance(std::min(length,distance+0.1)); + const double tx=next.x-prior.x,ty=next.y-prior.y; + const double tangentLength=std::hypot(tx,ty); + if(tangentLength<1.0e-6)continue; + const double normalX=-ty/tangentLength,normalY=tx/tangentLength; + double offsetSum=0.0;std::size_t offsetCount=0; + for(const double lateral:{-0.35,0.0,0.35}) + { + const auto root=floorRoot(point.x+normalX*lateral, + point.y+normalY*lateral,point.floor); + if(root){offsetSum+=*root-point.floor;++offsetCount;} + } + if(offsetCount) + {floorResiduals.push_back({distance,offsetSum/offsetCount});} + } + result.floorSamples=floorResiduals.size(); + const auto peakPeriod=[&](const std::vector>& samples, + double minimumPeriod,double maximumPeriod, + double& outPeriod) + { + double amplitude=0.0;outPeriod=0.0; + if(samples.size()<16)return amplitude; + for(double period=minimumPeriod;period<=maximumPeriod+1.0e-9;period+=0.125) + { + double cosine=0.0,sine=0.0; + for(const auto& [distance,residual]:samples) + { + const double phase=2.0*3.14159265358979323846*distance/period; + cosine+=residual*std::cos(phase);sine+=residual*std::sin(phase); + } + const double candidate=2.0*std::hypot(cosine,sine)/samples.size(); + if(candidate>amplitude){amplitude=candidate;outPeriod=period;} + } + return amplitude; + }; + result.floorPeakAmplitudeMetres=peakPeriod(floorResiduals,0.375, + std::min(8.0,length/2.0),result.floorPeakPeriodMetres); + + std::vector> wallResiduals; + const double expectedWallRadius=edge!=layout.cavernEdges.end() + ?edge->boreRadiusMicrometres/1e6*std::sqrt(1.0 + -std::pow(2.0/(edge->boreHeightMicrometres/1e6),2.0)) + :2.6*std::sqrt(1.0-std::pow(0.4/2.0,2.0)); + // Keep the edge audit away from the ground portal and the room union; + // those are transitions between features, not tunnel-wall ripple. + const double wallAuditStart=std::max(4.0,length*0.38); + const double wallAuditEnd=std::min(length*0.55,length-4.0); + for(double distance=wallAuditStart;distance<=wallAuditEnd;distance+=0.0625) + { + const auto point=atDistance(distance); + const auto prior=atDistance(std::max(0.0,distance-0.1)); + const auto next=atDistance(std::min(length,distance+0.1)); + const double tx=next.x-prior.x,ty=next.y-prior.y; + const double tangentLength=std::hypot(tx,ty); + if(tangentLength<1.0e-6)continue; + const double normalX=-ty/tangentLength,normalY=tx/tangentLength; + const double height=point.floor+2.0; + for(const double sign:{-1.0,1.0}) + { + double priorRadius=0.0; + double priorDensity=densityMetres(point.x,point.y,height); + bool found=false; + for(double radius=0.025;radius<=8.0;radius+=0.025) + { + const double density=densityMetres(point.x+normalX*sign*radius, + point.y+normalY*sign*radius,height); + if(priorDensity>=0.0&&density<0.0) + { + const double span=priorDensity-density; + const double root=span>0.0?priorRadius+0.025*priorDensity/span:radius; + wallResiduals.push_back({distance,root-expectedWallRadius}); + found=true;break; + } + priorRadius=radius;priorDensity=density; + } + if(!found)continue; + } + } + result.wallSamples=wallResiduals.size(); + // Remove the slow wall envelope from the amplitude profile before testing + // for segment-scale ribs. The moving baseline covers 2 m, while the audit + // band covers the shorter repeated seams seen in the L35/L36 controls. + std::vector> wallRippleResiduals; + wallRippleResiduals.reserve(wallResiduals.size()); + for(const auto& sample:wallResiduals) + { + double baseline=0.0;std::size_t neighbours=0; + for(const auto& other:wallResiduals) + if(std::abs(other.first-sample.first)<=1.0) + {baseline+=other.second;++neighbours;} + if(neighbours>0&&sample.first>=wallAuditStart+1.0 + &&sample.first<=wallAuditEnd-1.0) + wallRippleResiduals.push_back({sample.first, + sample.second-baseline/static_cast(neighbours)}); + } + result.wallPeakAmplitudeMetres=peakPeriod(wallRippleResiduals,0.25, + std::min(1.0,length/2.0),result.wallPeakPeriodMetres); + result.valid=result.floorSamples>=16&&result.wallSamples>=16; + result.passed=result.valid&&result.floorPeakAmplitudeMetres<=0.015 + &&result.wallPeakAmplitudeMetres<=0.035; return result; } @@ -447,6 +714,8 @@ namespace StandaloneStreamBenchmark { using namespace VoxelForgeCore; control=source;control.stamps.clear();control.stampVocabularyVersion=1; + control.cavernGraphVocabularyVersion=0; + control.cavernRooms.clear();control.cavernEdges.clear(); const auto add=[&](std::string id,ResolvedSdfStampKindV1 kind, std::array position, std::array half,std::int64_t smooth, @@ -971,11 +1240,32 @@ namespace StandaloneStreamBenchmark if(bAuditLandmarkReachability) { Streamer.Stop(); - if(ResolvedLayout==nullptr||!CoreSnapshot.resolvedLayout() - ||ResolvedLayout->stamps.empty()) - {std::fprintf(stderr,"[LandmarkReachabilityAudit] missing demo layout and stamps\n");return 2;} + if(ResolvedLayout==nullptr||!CoreSnapshot.resolvedLayout()) + {std::fprintf(stderr,"[LandmarkReachabilityAudit] missing resolved layout\n");return 2;} + if(ResolvedLayout->cavernEdges.empty()) + { + const auto control=AuditLandmarkTunnelRipples(Shape,*ResolvedLayout, + World.CoreVoxelSizeMicrons); + const bool expectedFail=control.valid + &&control.floorPeakAmplitudeMetres>0.015; + std::printf("[CavernTunnelRippleControl] layout=L45-ramp floor_samples=%zu floor_peak_period_m=%.3f floor_peak_amplitude_m=%.4f floor_limit_m=0.015 wall_samples=%zu wall_peak_period_m=%.3f wall_peak_amplitude_m=%.4f wall_limit_m=0.035 observed=%s expected=FAIL status=%s\n", + control.floorSamples,control.floorPeakPeriodMetres, + control.floorPeakAmplitudeMetres,control.wallSamples, + control.wallPeakPeriodMetres,control.wallPeakAmplitudeMetres, + expectedFail?"FAIL":"PASS",expectedFail?"PASS":"FAIL"); + return expectedFail?0:14; + } + if(ResolvedLayout->cavernRooms.empty()) + {std::fprintf(stderr,"[LandmarkReachabilityAudit] missing cavern room node\n");return 2;} + const auto ripples=AuditLandmarkTunnelRipples(Shape,*ResolvedLayout, + World.CoreVoxelSizeMicrons); + std::printf("[CavernTunnelRipple] layout=L46 floor_samples=%zu floor_peak_period_m=%.3f floor_peak_amplitude_m=%.4f floor_limit_m=0.015 wall_samples=%zu wall_peak_period_m=%.3f wall_peak_amplitude_m=%.4f wall_period_band_m=0.250..1.000 wall_detrend_window_m=2.000 wall_limit_m=0.035 status=%s\n", + ripples.floorSamples,ripples.floorPeakPeriodMetres, + ripples.floorPeakAmplitudeMetres,ripples.wallSamples, + ripples.wallPeakPeriodMetres,ripples.wallPeakAmplitudeMetres, + ripples.passed?"PASS":"FAIL"); const auto candidate=AuditLandmarkWalkPath(Shape,Assembly,*ResolvedLayout, - World.CoreVoxelSizeMicrons,"L45"); + World.CoreVoxelSizeMicrons,"L46"); const auto daisStamp=std::find_if(ResolvedLayout->stamps.begin(), ResolvedLayout->stamps.end(),[](const auto& stamp) {return stamp.stableId=="landmark/sw-arrival-chamber-v1-dais";}); @@ -1009,7 +1299,7 @@ namespace StandaloneStreamBenchmark oldBuildError.c_str());return 2;} const auto buried=AuditLandmarkWalkPath(oldShape,Assembly,*ResolvedLayout, World.CoreVoxelSizeMicrons,"L43"); - std::printf("[LandmarkWalkability] layout=L45 path_samples=%llu capsule_samples=%llu blocked=%llu support_samples=%llu unsupported=%llu grade_failures=%llu max_grade_deg=%.3f limit_deg=15.000 min_clearance_m=%.3f max_support_depth_m=%.3f status=%s\n", + std::printf("[LandmarkWalkability] layout=L46 path_samples=%llu capsule_samples=%llu blocked=%llu support_samples=%llu unsupported=%llu grade_failures=%llu max_grade_deg=%.3f limit_deg=15.000 min_clearance_m=%.3f max_support_depth_m=%.3f min_headroom_m=%.3f headroom_required_m=0.300 status=%s\n", static_cast(candidate.pathSamples), static_cast(candidate.capsuleSamples), static_cast(candidate.blockedCapsuleSamples), @@ -1017,14 +1307,18 @@ namespace StandaloneStreamBenchmark static_cast(candidate.unsupportedSamples), static_cast(candidate.gradeFailures), candidate.maximumGradeDegrees,candidate.minimumClearanceMetres, - candidate.maximumSupportDepthMetres,candidate.passed?"PASS":"FAIL"); + candidate.maximumSupportDepthMetres,candidate.minimumHeadroomMetres, + candidate.passed?"PASS":"FAIL"); + std::printf("[LandmarkHeadroom] minimum_m=%.3f required_m=0.300 status=%s\n", + candidate.minimumHeadroomMetres, + candidate.minimumHeadroomMetres>=0.300?"PASS":"FAIL"); std::printf("[LandmarkReachabilityControl] layout=L43-buried path_samples=%llu blocked=%llu unsupported=%llu grade_failures=%llu result=%s expected=FAIL status=%s\n", static_cast(buried.pathSamples), static_cast(buried.blockedCapsuleSamples), static_cast(buried.unsupportedSamples), static_cast(buried.gradeFailures), buried.passed?"PASS":"FAIL",buried.passed?"FAIL":"PASS"); - const bool passed=candidate.passed&&!buried.passed; + const bool passed=ripples.passed&&candidate.passed&&!buried.passed; std::printf("[LandmarkReachabilityAudit] positive=PASS control=%s status=%s\n", buried.passed?"FAILED":"EXPECTED_FAIL",passed?"PASS":"FAIL"); return passed?0:13; diff --git a/Standalone/Tools/verify/verify.py b/Standalone/Tools/verify/verify.py index a7ff4a5..2920cf0 100644 --- a/Standalone/Tools/verify/verify.py +++ b/Standalone/Tools/verify/verify.py @@ -192,7 +192,7 @@ def run_landmark_reachability(runner: VerificationRunner, expected_style=2, expected_strate_styles={"0": 2, "1": 1}, max_level=0, layout_path=OUT / "layouts" / f"{passage_name}.json") runner.require(f"{passage_name}:demo-enabled", - all_match(passage_output, r"^\[LandmarkStampDemo\] enabled=true .*status=PASS$"), + all_match(passage_output, r"^\[LandmarkCavernGraphDemo\] enabled=true .*rooms=1 edges=1 .*status=PASS$"), "landmark demo flag resolved while running passage audits") validate_passage(runner, passage_name, passage_output) @@ -203,7 +203,7 @@ def run_landmark_reachability(runner: VerificationRunner, expected_style=2, expected_strate_styles={"0": 2, "1": 1}, max_level=0, layout_path=OUT / "layouts" / f"{interface_name}.json") runner.require(f"{interface_name}:demo-enabled", - all_match(interface_output, r"^\[LandmarkStampDemo\] enabled=true .*status=PASS$"), + all_match(interface_output, r"^\[LandmarkCavernGraphDemo\] enabled=true .*rooms=1 edges=1 .*status=PASS$"), "landmark demo flag resolved while running interface audits") runner.require(f"{interface_name}:hole-audit", all_match(interface_output, r"^\[StrateTransitionAuditSummary\].*hole_columns=0 " @@ -213,8 +213,14 @@ def run_landmark_reachability(runner: VerificationRunner, all_match(interface_output, r"^\[StrateTransitionNegativeControls\].*status=PASS$"), "demo-on interface audit detects both thickness controls") runner.require(f"{name}:candidate-walkability", - all_match(output, r"^\[LandmarkWalkability\] layout=L45 .*status=PASS$"), - "demo capsule clearance, support and <=15 degree grade") + all_match(output, r"^\[LandmarkWalkability\] layout=L46 .*headroom_required_m=0\.300 status=PASS$"), + "demo capsule clearance, support, headroom and <=15 degree grade") + runner.require(f"{name}:headroom", + all_match(output, r"^\[LandmarkHeadroom\] minimum_m=(?:0\.[3-9][0-9][0-9]|[1-9][0-9]*\.[0-9]{3}) required_m=0\.300 status=PASS$"), + "at least 0.3 m above the capsule along the complete route") + runner.require(f"{name}:continuous-tunnel-ripple", + all_match(output, r"^\[CavernTunnelRipple\] layout=L46 .*floor_limit_m=0\.015 .*wall_limit_m=0\.035 status=PASS$"), + "floor and wall ripple remain below L36/L35 limits") runner.require(f"{name}:buried-l43-control", all_match(output, r"^\[LandmarkReachabilityControl\] layout=L43-buried .*" r"result=FAIL expected=FAIL status=PASS$"), @@ -227,6 +233,18 @@ def run_landmark_reachability(runner: VerificationRunner, all_match(output, r"^\[LandmarkReachabilityAudit\] positive=PASS " r"control=EXPECTED_FAIL status=PASS$"), "candidate passes and buried control fails") + ripple_control = f"landmark-l45-ramp-ripple-control-{compiler}" + _, control_output = runner.run_cpp(ripple_control, binary, OWNER, "surface-world", + ["--landmark-chamber-demo", "--landmark-l45-ramp-control", + "--audit-landmark-reachability", "--max-clip-level=0", + "--render-distance-chunks=0"], + expected_style=2, expected_strate_styles={"0": 2, "1": 1}, + max_level=0, layout_path=OUT / "layouts" / f"{ripple_control}.json") + runner.require(f"{ripple_control}:negative-control", + all_match(control_output, + r"^\[CavernTunnelRippleControl\] layout=L45-ramp .*" + r"floor_limit_m=0\.015 .*observed=FAIL expected=FAIL status=PASS$"), + "the segmented L45 ramp exceeds the floor ripple threshold") def expected_control_row(runner: VerificationRunner, name: str) -> None: @@ -398,6 +416,36 @@ def parse_tile_hashes(output: str) -> dict[tuple[int, int, int, int], tuple[str, return rows +def cavern_audit_cameras(layout_path: Path) -> list[tuple[float, float, float, float, float]]: + layout = json.loads(layout_path.read_text(encoding="utf-8")) + edges = layout.get("cavern_edges", []) + if layout.get("cavern_graph_vocabulary_version") != 1 or len(edges) != 1: + raise VerificationFailure(f"missing one resolved cavern edge in {layout_path}") + points = [[value / 1_000_000.0 for value in point] + for point in edges[0]["control_points_um"]] + + def value(t: float, axis: int) -> float: + one = 1.0 - t + return (one ** 3 * points[0][axis] + 3.0 * one ** 2 * t * points[1][axis] + + 3.0 * one * t ** 2 * points[2][axis] + t ** 3 * points[3][axis]) + + def derivative(t: float, axis: int) -> float: + one = 1.0 - t + return (3.0 * one ** 2 * (points[1][axis] - points[0][axis]) + + 6.0 * one * t * (points[2][axis] - points[1][axis]) + + 3.0 * t ** 2 * (points[3][axis] - points[2][axis])) + + cameras = [] + for t, lateral in ((0.50, 0.0), (0.32, 3.3)): + x, y, floor = value(t, 0), value(t, 1), value(t, 2) + dx, dy = derivative(t, 0), derivative(t, 1) + length = math.hypot(dx, dy) + side_x, side_y = -dy / length, dx / length + cameras.append((x + side_x * lateral, y + side_y * lateral, + floor + 1.6, math.degrees(math.atan2(dx, dy)), 0.0)) + return cameras + + def run_random_camera_sweep(runner: VerificationRunner, binaries: tuple[Path, ...], camera_count: int, configurations: tuple[tuple[int, int, int], ...]) -> None: @@ -464,6 +512,42 @@ def run_random_camera_sweep(runner: VerificationRunner, binaries: tuple[Path, .. runner.require(f"{label}:W{left}-W{right}", left_rows == right_rows, detail) + # Keep a pair of fixed cameras on the current owner graph: one in the bore and + # one beside its curved wall. Their tile hashes join the seeded W1/W8/W12 sweep. + layout_path = OUT / "layouts" / "landmark-reachability-clang.json" + if layout_path.is_file(): + graph_cameras = cavern_audit_cameras(layout_path) + for binary in binaries: + compiler = compiler_name(binary) + for camera_index, camera in enumerate(graph_cameras): + x, y, z, yaw, pitch = camera + camera_flag = f"--camera={x:.6f},{y:.6f},{z:.6f},{yaw:.3f},{pitch:.3f}" + label = f"cavern-camera-{compiler}-s{CAMERA_SWEEP_SEED:06x}-p{camera_index:02d}" + signatures: dict[int, dict[tuple[int, int, int, int], tuple[str, ...]]] = {} + for workers in (1, 8, 12): + name = f"{label}-w{workers}" + row, output = runner.run_cpp(name, binary, OWNER, "surface-world", + ["--stream-hash-tiles", camera_flag, "--landmark-chamber-demo", + "--clip-radius=3", "--max-clip-level=0", "--max-level=0", + "--render-distance-chunks=0"], + expected_style=2, + expected_strate_styles={"0": 2, "1": 1}, workers=workers, + max_level=0, layout_path=OUT / "layouts" / f"{name}.json") + row["camera_position_m"] = [round(value, 6) for value in camera[:3]] + row["camera_region"] = "inside-tunnel" if camera_index == 0 else "near-tunnel-wall" + signatures[workers] = parse_tile_hashes(output) + whole = re.search(r"^\[PipelineWholeViewHash\] tiles=(\d+)", output, re.M) + expected_rows = int(whole.group(1)) if whole else -1 + runner.require(f"{name}:tile-hash-rows", + bool(signatures[workers]) and len(signatures[workers]) == expected_rows, + f"rows={len(signatures[workers])} expected={expected_rows} camera={camera[:3]}") + for left, right in ((1, 8), (1, 12), (8, 12)): + runner.require(f"{label}:W{left}-W{right}", + signatures[left] == signatures[right], + f"camera_m=({x:.3f},{y:.3f},{z:.3f}) " + f"region={'inside' if camera_index == 0 else 'near'}-tunnel " + f"W{left}=W{right} density/mesh/payload/topology tile hashes match") + def run_ring(runner: VerificationRunner, *, full: bool) -> None: script = LINUX37 / "RunRingAuditsL37.py" @@ -611,11 +695,11 @@ def run_captures_and_mesh_audits(runner: VerificationRunner) -> None: script_inputs=[overview_script, VERIFY_DIR / "capture_quality.py", OUT / "owner-fi-ground-mouth.vfmesh"]) - landmark_script = ROOT / "Standalone/Research/WorldAssemblerM3/Linux45/CaptureLandmarkL45.py" + landmark_script = ROOT / "Standalone/Research/WorldAssemblerM3/Linux46/CaptureLandmarkL46.py" landmark_layout = OUT / "layouts" / "landmark-reachability-clang.json" - runner.run_stage("capture-landmark-l45", landmark_script, + runner.run_stage("capture-landmark-l46", landmark_script, ["--layout", str(landmark_layout), "--artifact-dir", str(OUT), - "--capture-dir", str(ROOT / "Standalone/Research/WorldAssemblerM3/Linux45/captures"), + "--capture-dir", str(ROOT / "Standalone/Research/WorldAssemblerM3/Linux46/captures"), "--data", str(OWNER)], binary=CLANG, data=OWNER, recipe="surface-world", style=1, layout_path=landmark_layout, env=env_for(capture=True),