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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
This commit is contained in:
1 parent
f13b145f23
commit
e016f6a20f
14 files changed
+1753
-48
No files matched your search
@@ -707,6 +707,7 @@ namespace VoxelForgeCore
|
|||||||
inline constexpr std::uint32_t ResolvedLayoutSchemaVersionV3 = 3;
|
inline constexpr std::uint32_t ResolvedLayoutSchemaVersionV3 = 3;
|
||||||
inline constexpr std::uint32_t ResolvedLayoutStampVocabularyVersionV1 = 1;
|
inline constexpr std::uint32_t ResolvedLayoutStampVocabularyVersionV1 = 1;
|
||||||
inline constexpr std::uint32_t ResolvedLayoutStampVocabularyVersionV2 = 2;
|
inline constexpr std::uint32_t ResolvedLayoutStampVocabularyVersionV2 = 2;
|
||||||
|
inline constexpr std::uint32_t ResolvedLayoutCavernGraphVocabularyVersionV1 = 1;
|
||||||
enum class ResolvedLayoutValidationModeV3 : std::uint8_t { compatibility, strict };
|
enum class ResolvedLayoutValidationModeV3 : std::uint8_t { compatibility, strict };
|
||||||
|
|
||||||
// Stable, bounded geometry edits composed into every Core field after the shared
|
// Stable, bounded geometry edits composed into every Core field after the shared
|
||||||
@@ -750,6 +751,42 @@ namespace VoxelForgeCore
|
|||||||
std::uint8_t yawQuarterTurns = 0;
|
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<std::int64_t, 3> centerMicrometres{};
|
||||||
|
std::array<std::int64_t, 3> 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<std::int64_t, 3> boundsMinimumMicrometres{};
|
||||||
|
std::array<std::int64_t, 3> 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<std::array<std::int64_t, 3>, 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<std::int64_t, 3> boundsMinimumMicrometres{};
|
||||||
|
std::array<std::int64_t, 3> boundsMaximumMicrometres{};
|
||||||
|
};
|
||||||
|
|
||||||
struct ResolvedLayoutAnchorV2
|
struct ResolvedLayoutAnchorV2
|
||||||
{
|
{
|
||||||
std::string kind;
|
std::string kind;
|
||||||
@@ -918,6 +955,10 @@ namespace VoxelForgeCore
|
|||||||
// Kept absent from canonical layout identity when no feature requests stamps.
|
// Kept absent from canonical layout identity when no feature requests stamps.
|
||||||
std::uint32_t stampVocabularyVersion = 0;
|
std::uint32_t stampVocabularyVersion = 0;
|
||||||
std::vector<ResolvedLayoutStampV1> stamps;
|
std::vector<ResolvedLayoutStampV1> stamps;
|
||||||
|
// Kept absent from canonical identity when there are no graph records.
|
||||||
|
std::uint32_t cavernGraphVocabularyVersion = 0;
|
||||||
|
std::vector<ResolvedCavernRoomV1> cavernRooms;
|
||||||
|
std::vector<ResolvedCavernEdgeV1> cavernEdges;
|
||||||
std::string stableIdentity;
|
std::string stableIdentity;
|
||||||
|
|
||||||
bool finalize(std::string& error);
|
bool finalize(std::string& error);
|
||||||
@@ -970,6 +1011,12 @@ namespace VoxelForgeCore
|
|||||||
VFCORE_API ResolvedLayoutStampPlacementAuditV1 AuditResolvedLayoutStampPlacementV1(
|
VFCORE_API ResolvedLayoutStampPlacementAuditV1 AuditResolvedLayoutStampPlacementV1(
|
||||||
const ResolvedLayoutSnapshotV3& layout);
|
const ResolvedLayoutSnapshotV3& layout);
|
||||||
|
|
||||||
|
VFCORE_API bool ResolveResolvedCavernGraphV1(
|
||||||
|
ResolvedLayoutSnapshotV3& layout, std::int32_t seed,
|
||||||
|
std::span<const ResolvedCavernRoomV1> rooms,
|
||||||
|
std::span<const ResolvedCavernEdgeV1> edges,
|
||||||
|
std::string& error);
|
||||||
|
|
||||||
struct FloatingIslandBoundsAuditResult
|
struct FloatingIslandBoundsAuditResult
|
||||||
{
|
{
|
||||||
std::uint64_t boxesTested = 0;
|
std::uint64_t boxesTested = 0;
|
||||||
|
|||||||
@@ -404,6 +404,56 @@ namespace
|
|||||||
}
|
}
|
||||||
out<<']';
|
out<<']';
|
||||||
}
|
}
|
||||||
|
if(!s.cavernRooms.empty()||!s.cavernEdges.empty())
|
||||||
|
{
|
||||||
|
out<<",\"cavern_graph_vocabulary_version\":"
|
||||||
|
<<s.cavernGraphVocabularyVersion<<",\"cavern_rooms\":[";
|
||||||
|
for(std::size_t i=0;i<s.cavernRooms.size();++i)
|
||||||
|
{
|
||||||
|
if(i)out<<',';const auto& room=s.cavernRooms[i];
|
||||||
|
out<<"{\"stable_id\":"<<LayoutEscape(room.stableId)
|
||||||
|
<<",\"strate_index\":"<<room.strateIndex
|
||||||
|
<<",\"center_um\":["<<room.centerMicrometres[0]<<','
|
||||||
|
<<room.centerMicrometres[1]<<','<<room.centerMicrometres[2]
|
||||||
|
<<"],\"half_extents_um\":["<<room.halfExtentsMicrometres[0]<<','
|
||||||
|
<<room.halfExtentsMicrometres[1]<<','<<room.halfExtentsMicrometres[2]
|
||||||
|
<<"],\"floor_um\":"<<room.floorMicrometres
|
||||||
|
<<",\"smooth_width_um\":"<<room.smoothWidthMicrometres
|
||||||
|
<<",\"irregularity_amplitude_um\":"<<room.irregularityAmplitudeMicrometres
|
||||||
|
<<",\"irregularity_wavelength_um\":"<<room.irregularityWavelengthMicrometres
|
||||||
|
<<",\"irregularity_seed\":"<<room.irregularitySeed
|
||||||
|
<<",\"bounds_min_um\":["<<room.boundsMinimumMicrometres[0]<<','
|
||||||
|
<<room.boundsMinimumMicrometres[1]<<','<<room.boundsMinimumMicrometres[2]
|
||||||
|
<<"],\"bounds_max_um\":["<<room.boundsMaximumMicrometres[0]<<','
|
||||||
|
<<room.boundsMaximumMicrometres[1]<<','<<room.boundsMaximumMicrometres[2]<<"]}";
|
||||||
|
}
|
||||||
|
out<<"],\"cavern_edges\":[";
|
||||||
|
for(std::size_t i=0;i<s.cavernEdges.size();++i)
|
||||||
|
{
|
||||||
|
if(i)out<<',';const auto& edge=s.cavernEdges[i];
|
||||||
|
out<<"{\"stable_id\":"<<LayoutEscape(edge.stableId)
|
||||||
|
<<",\"start_anchor_stable_id\":"<<LayoutEscape(edge.startAnchorStableId)
|
||||||
|
<<",\"end_room_stable_id\":"<<LayoutEscape(edge.endRoomStableId)
|
||||||
|
<<",\"strate_index\":"<<edge.strateIndex<<",\"control_points_um\":[";
|
||||||
|
for(std::size_t point=0;point<edge.controlPointsMicrometres.size();++point)
|
||||||
|
{
|
||||||
|
if(point)out<<',';const auto& p=edge.controlPointsMicrometres[point];
|
||||||
|
out<<'['<<p[0]<<','<<p[1]<<','<<p[2]<<']';
|
||||||
|
}
|
||||||
|
out<<"],\"bore_radius_um\":"<<edge.boreRadiusMicrometres
|
||||||
|
<<",\"bore_height_um\":"<<edge.boreHeightMicrometres
|
||||||
|
<<",\"floor_support_depth_um\":"<<edge.floorSupportDepthMicrometres
|
||||||
|
<<",\"smooth_width_um\":"<<edge.smoothWidthMicrometres
|
||||||
|
<<",\"irregularity_amplitude_um\":"<<edge.irregularityAmplitudeMicrometres
|
||||||
|
<<",\"irregularity_wavelength_um\":"<<edge.irregularityWavelengthMicrometres
|
||||||
|
<<",\"irregularity_seed\":"<<edge.irregularitySeed
|
||||||
|
<<",\"bounds_min_um\":["<<edge.boundsMinimumMicrometres[0]<<','
|
||||||
|
<<edge.boundsMinimumMicrometres[1]<<','<<edge.boundsMinimumMicrometres[2]
|
||||||
|
<<"],\"bounds_max_um\":["<<edge.boundsMaximumMicrometres[0]<<','
|
||||||
|
<<edge.boundsMaximumMicrometres[1]<<','<<edge.boundsMaximumMicrometres[2]<<"]}";
|
||||||
|
}
|
||||||
|
out<<']';
|
||||||
|
}
|
||||||
out<<'}';return out.str();
|
out<<'}';return out.str();
|
||||||
}
|
}
|
||||||
std::int64_t LayoutInt(const VFJson::Value& obj,std::string_view key)
|
std::int64_t LayoutInt(const VFJson::Value& obj,std::string_view key)
|
||||||
@@ -566,6 +616,10 @@ bool ResolvedLayoutSnapshotV3::finalize(std::string& error)
|
|||||||
std::stable_sort(strates.begin(),strates.end(),[](const auto& a,const auto& b)
|
std::stable_sort(strates.begin(),strates.end(),[](const auto& a,const auto& b)
|
||||||
{return a.interiorTopMicrometres!=b.interiorTopMicrometres
|
{return a.interiorTopMicrometres!=b.interiorTopMicrometres
|
||||||
?a.interiorTopMicrometres>b.interiorTopMicrometres:a.order<b.order;});
|
?a.interiorTopMicrometres>b.interiorTopMicrometres:a.order<b.order;});
|
||||||
|
std::sort(cavernRooms.begin(),cavernRooms.end(),[](const auto& a,const auto& b)
|
||||||
|
{return a.stableId<b.stableId;});
|
||||||
|
std::sort(cavernEdges.begin(),cavernEdges.end(),[](const auto& a,const auto& b)
|
||||||
|
{return a.stableId<b.stableId;});
|
||||||
for(std::size_t i=0;i<strates.size();++i)
|
for(std::size_t i=0;i<strates.size();++i)
|
||||||
{
|
{
|
||||||
auto& x=strates[i];x.order=static_cast<std::int32_t>(i);
|
auto& x=strates[i];x.order=static_cast<std::int32_t>(i);
|
||||||
@@ -870,6 +924,110 @@ bool ResolvedLayoutSnapshotV3::validate(ResolvedLayoutValidationModeV3 mode,
|
|||||||
if(!LayoutStampSafe(*this,stamp,&safetyReason))
|
if(!LayoutStampSafe(*this,stamp,&safetyReason))
|
||||||
add("stamp_placement",stamp.stableId+" is unsafe: "+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<std::string> 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.floorMicrometres<room.centerMicrometres[2]-room.halfExtentsMicrometres[2]
|
||||||
|
||room.floorMicrometres>room.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<std::int64_t,3> low=edge.controlPointsMicrometres[0];
|
||||||
|
std::array<std::int64_t,3> 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]<room->centerMicrometres[0]-room->halfExtentsMicrometres[0]
|
||||||
|
||endpoint[0]>room->centerMicrometres[0]+room->halfExtentsMicrometres[0]
|
||||||
|
||endpoint[1]<room->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;}
|
if(strict&&!issues.empty()){error="strict resolved-layout validation rejected "+issues.front().code;return false;}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -886,6 +1044,8 @@ std::optional<ResolvedLayoutSnapshotV3> ResolvedLayoutSnapshotV3::FromJson(
|
|||||||
s.strateLayoutChunkSizeMicrometres=static_cast<std::uint32_t>(LayoutInt(root,"strate_layout_chunk_size_um"));
|
s.strateLayoutChunkSizeMicrometres=static_cast<std::uint32_t>(LayoutInt(root,"strate_layout_chunk_size_um"));
|
||||||
s.strateLayoutChunkWidthSubvoxels=LayoutInt(root,"strate_layout_chunk_width_subvoxels");
|
s.strateLayoutChunkWidthSubvoxels=LayoutInt(root,"strate_layout_chunk_width_subvoxels");
|
||||||
s.stampVocabularyVersion=static_cast<std::uint32_t>(LayoutInt(root,"stamp_vocabulary_version"));
|
s.stampVocabularyVersion=static_cast<std::uint32_t>(LayoutInt(root,"stamp_vocabulary_version"));
|
||||||
|
s.cavernGraphVocabularyVersion=static_cast<std::uint32_t>(
|
||||||
|
LayoutInt(root,"cavern_graph_vocabulary_version"));
|
||||||
for(const auto& v:root.Get("strates").AsArray())
|
for(const auto& v:root.Get("strates").AsArray())
|
||||||
{
|
{
|
||||||
ResolvedLayoutStratumV2 x;x.stableId=LayoutString(v,"stable_id");
|
ResolvedLayoutStratumV2 x;x.stableId=LayoutString(v,"stable_id");
|
||||||
@@ -955,6 +1115,57 @@ std::optional<ResolvedLayoutSnapshotV3> ResolvedLayoutSnapshotV3::FromJson(
|
|||||||
stamp.yawQuarterTurns=static_cast<std::uint8_t>(LayoutInt(v,"yaw_quarter_turns"));
|
stamp.yawQuarterTurns=static_cast<std::uint8_t>(LayoutInt(v,"yaw_quarter_turns"));
|
||||||
s.stamps.push_back(std::move(stamp));
|
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<std::int32_t>(LayoutInt(v,"strate_index"));
|
||||||
|
const auto readTriplet=[&](std::string_view key,std::array<std::int64_t,3>& target)
|
||||||
|
{
|
||||||
|
const auto& values=v.Get(key).AsArray();
|
||||||
|
for(std::size_t axis=0;axis<3&&axis<values.size();++axis)
|
||||||
|
target[axis]=values[axis].Integer();
|
||||||
|
};
|
||||||
|
readTriplet("center_um",room.centerMicrometres);
|
||||||
|
readTriplet("half_extents_um",room.halfExtentsMicrometres);
|
||||||
|
readTriplet("bounds_min_um",room.boundsMinimumMicrometres);
|
||||||
|
readTriplet("bounds_max_um",room.boundsMaximumMicrometres);
|
||||||
|
room.floorMicrometres=LayoutInt(v,"floor_um");
|
||||||
|
room.smoothWidthMicrometres=LayoutInt(v,"smooth_width_um");
|
||||||
|
room.irregularityAmplitudeMicrometres=LayoutInt(v,"irregularity_amplitude_um");
|
||||||
|
room.irregularityWavelengthMicrometres=LayoutInt(v,"irregularity_wavelength_um");
|
||||||
|
room.irregularitySeed=static_cast<std::uint64_t>(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<std::int32_t>(LayoutInt(v,"strate_index"));
|
||||||
|
const auto& points=v.Get("control_points_um").AsArray();
|
||||||
|
for(std::size_t i=0;i<edge.controlPointsMicrometres.size()&&i<points.size();++i)
|
||||||
|
{
|
||||||
|
const auto& point=points[i].AsArray();
|
||||||
|
for(std::size_t axis=0;axis<3&&axis<point.size();++axis)
|
||||||
|
edge.controlPointsMicrometres[i][axis]=point[axis].Integer();
|
||||||
|
}
|
||||||
|
const auto readTriplet=[&](std::string_view key,std::array<std::int64_t,3>& target)
|
||||||
|
{
|
||||||
|
const auto& values=v.Get(key).AsArray();
|
||||||
|
for(std::size_t axis=0;axis<3&&axis<values.size();++axis)
|
||||||
|
target[axis]=values[axis].Integer();
|
||||||
|
};
|
||||||
|
readTriplet("bounds_min_um",edge.boundsMinimumMicrometres);
|
||||||
|
readTriplet("bounds_max_um",edge.boundsMaximumMicrometres);
|
||||||
|
edge.boreRadiusMicrometres=LayoutInt(v,"bore_radius_um");
|
||||||
|
edge.boreHeightMicrometres=LayoutInt(v,"bore_height_um");
|
||||||
|
edge.floorSupportDepthMicrometres=LayoutInt(v,"floor_support_depth_um");
|
||||||
|
edge.smoothWidthMicrometres=LayoutInt(v,"smooth_width_um");
|
||||||
|
edge.irregularityAmplitudeMicrometres=LayoutInt(v,"irregularity_amplitude_um");
|
||||||
|
edge.irregularityWavelengthMicrometres=LayoutInt(v,"irregularity_wavelength_um");
|
||||||
|
edge.irregularitySeed=static_cast<std::uint64_t>(LayoutInt(v,"irregularity_seed"));
|
||||||
|
s.cavernEdges.push_back(std::move(edge));
|
||||||
|
}
|
||||||
const std::string serializedIdentity=LayoutString(root,"stable_identity");
|
const std::string serializedIdentity=LayoutString(root,"stable_identity");
|
||||||
if(!s.finalize(error))return std::nullopt;
|
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;}
|
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;
|
layout=std::move(candidate);return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool ResolveResolvedCavernGraphV1(ResolvedLayoutSnapshotV3& layout,std::int32_t seed,
|
||||||
|
std::span<const ResolvedCavernRoomV1> rooms,
|
||||||
|
std::span<const ResolvedCavernEdgeV1> 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<std::int64_t,3> low=edge.controlPointsMicrometres[0];
|
||||||
|
std::array<std::int64_t,3> 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<ResolvedLayoutIssueV2> 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(
|
ResolvedLayoutStampPlacementAuditV1 AuditResolvedLayoutStampPlacementV1(
|
||||||
const ResolvedLayoutSnapshotV3& layout)
|
const ResolvedLayoutSnapshotV3& layout)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -169,6 +169,8 @@ public:
|
|||||||
featureIndices_=shape.BuildPassageFeatureTileList(
|
featureIndices_=shape.BuildPassageFeatureTileList(
|
||||||
minimumSubvoxels_,maximumSubvoxels_);
|
minimumSubvoxels_,maximumSubvoxels_);
|
||||||
stampIndices_=shape.BuildStampTileList(minimumSubvoxels_,maximumSubvoxels_);
|
stampIndices_=shape.BuildStampTileList(minimumSubvoxels_,maximumSubvoxels_);
|
||||||
|
cavernFeatureIndices_=shape.BuildCavernFeatureTileList(
|
||||||
|
minimumSubvoxels_,maximumSubvoxels_);
|
||||||
passageRoutes_.assign(shape.ResolvedLayout?shape.ResolvedLayout->passages.size():0,0);
|
passageRoutes_.assign(shape.ResolvedLayout?shape.ResolvedLayout->passages.size():0,0);
|
||||||
passageColumns_=passageRoutes_;
|
passageColumns_=passageRoutes_;
|
||||||
passageMouthSupports_=passageRoutes_;
|
passageMouthSupports_=passageRoutes_;
|
||||||
@@ -219,12 +221,16 @@ public:
|
|||||||
std::size_t featureCount() const{return featureIndices_.size();}
|
std::size_t featureCount() const{return featureIndices_.size();}
|
||||||
bool hasStampCandidates() const{return !stampIndices_.empty();}
|
bool hasStampCandidates() const{return !stampIndices_.empty();}
|
||||||
const std::vector<std::uint32_t>& stampCandidates() const{return stampIndices_;}
|
const std::vector<std::uint32_t>& stampCandidates() const{return stampIndices_;}
|
||||||
|
bool hasCavernCandidates() const{return !cavernFeatureIndices_.empty();}
|
||||||
|
const std::vector<std::uint32_t>& cavernFeatureCandidates() const
|
||||||
|
{return cavernFeatureIndices_;}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
const FStandaloneShapeWorld* shape_;
|
const FStandaloneShapeWorld* shape_;
|
||||||
std::array<std::int64_t,3> minimumSubvoxels_,maximumSubvoxels_;
|
std::array<std::int64_t,3> minimumSubvoxels_,maximumSubvoxels_;
|
||||||
std::vector<std::uint32_t> featureIndices_;
|
std::vector<std::uint32_t> featureIndices_;
|
||||||
std::vector<std::uint32_t> stampIndices_;
|
std::vector<std::uint32_t> stampIndices_;
|
||||||
|
std::vector<std::uint32_t> cavernFeatureIndices_;
|
||||||
std::vector<std::uint8_t> passageRoutes_,passageColumns_,passageMouthSupports_;
|
std::vector<std::uint8_t> passageRoutes_,passageColumns_,passageMouthSupports_;
|
||||||
bool routeFeatureOverlap_=false,columnZOverlap_=false;
|
bool routeFeatureOverlap_=false,columnZOverlap_=false;
|
||||||
bool mayContainResolvedBoundaryBand_=true;
|
bool mayContainResolvedBoundaryBand_=true;
|
||||||
@@ -1780,9 +1786,13 @@ public:
|
|||||||
|
|
||||||
float ApplyResolvedStrateComposition(float density,float X,float Y,float Z) const
|
float ApplyResolvedStrateComposition(float density,float X,float Y,float Z) const
|
||||||
{
|
{
|
||||||
|
const float graphDensity=ApplyResolvedCavernGraph(density,X,Y,Z);
|
||||||
const float composed=ApplyResolvedStratePassageCarves(
|
const float composed=ApplyResolvedStratePassageCarves(
|
||||||
ApplyResolvedStrateBoundaryBand(density,X,Y,Z),X,Y,Z);
|
ApplyResolvedStrateBoundaryBand(graphDensity,X,Y,Z),X,Y,Z);
|
||||||
return StampRuntime.empty()?composed:ApplyResolvedSdfStamps(composed,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
|
bool ResolvedPassageIntersectsBox(const int32 minimum[3],const int32 maximum[3]) const
|
||||||
@@ -3198,6 +3208,21 @@ private:
|
|||||||
std::uint64_t irregularitySeed=0;
|
std::uint64_t irregularitySeed=0;
|
||||||
std::uint8_t quarterTurns=0;
|
std::uint8_t quarterTurns=0;
|
||||||
};
|
};
|
||||||
|
struct FCavernRuntimeFeature
|
||||||
|
{
|
||||||
|
const VoxelForgeCore::ResolvedCavernRoomV1* room=nullptr;
|
||||||
|
const VoxelForgeCore::ResolvedCavernEdgeV1* edge=nullptr;
|
||||||
|
std::array<std::int64_t,3> boundsMinimumMicrometres{},boundsMaximumMicrometres{};
|
||||||
|
std::array<float,3> centerVoxels{},halfExtentsVoxels{};
|
||||||
|
std::array<std::array<float,3>,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)
|
int BuildStampSpatialNode(std::size_t first,std::size_t end)
|
||||||
{
|
{
|
||||||
@@ -3346,6 +3371,104 @@ private:
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void BuildCavernSpatialIndex()
|
||||||
|
{
|
||||||
|
CavernRuntime.clear();
|
||||||
|
if(!ResolvedLayout||!Owner||Owner->CoreVoxelSizeMicrons==0
|
||||||
|
||ResolvedLayout->cavernGraphVocabularyVersion
|
||||||
|
!=VoxelForgeCore::ResolvedLayoutCavernGraphVocabularyVersionV1)return;
|
||||||
|
const float micronsPerVoxel=static_cast<float>(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<float>(
|
||||||
|
static_cast<double>(room.centerMicrometres[axis])/micronsPerVoxel);
|
||||||
|
runtime.halfExtentsVoxels[axis]=static_cast<float>(
|
||||||
|
static_cast<double>(room.halfExtentsMicrometres[axis])/micronsPerVoxel);
|
||||||
|
}
|
||||||
|
runtime.floorVoxels=static_cast<float>(
|
||||||
|
static_cast<double>(room.floorMicrometres)/micronsPerVoxel);
|
||||||
|
runtime.smoothWidthVoxels=static_cast<float>(
|
||||||
|
static_cast<double>(room.smoothWidthMicrometres)/micronsPerVoxel);
|
||||||
|
runtime.irregularityAmplitudeVoxels=static_cast<float>(
|
||||||
|
static_cast<double>(room.irregularityAmplitudeMicrometres)/micronsPerVoxel);
|
||||||
|
runtime.irregularityWavelengthVoxels=static_cast<float>(
|
||||||
|
static_cast<double>(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<float>(
|
||||||
|
static_cast<double>(edge.controlPointsMicrometres[point][axis])
|
||||||
|
/micronsPerVoxel);
|
||||||
|
runtime.boreRadiusVoxels=static_cast<float>(
|
||||||
|
static_cast<double>(edge.boreRadiusMicrometres)/micronsPerVoxel);
|
||||||
|
runtime.boreHeightVoxels=static_cast<float>(
|
||||||
|
static_cast<double>(edge.boreHeightMicrometres)/micronsPerVoxel);
|
||||||
|
runtime.floorSupportDepthVoxels=static_cast<float>(
|
||||||
|
static_cast<double>(edge.floorSupportDepthMicrometres)/micronsPerVoxel);
|
||||||
|
runtime.smoothWidthVoxels=static_cast<float>(
|
||||||
|
static_cast<double>(edge.smoothWidthMicrometres)/micronsPerVoxel);
|
||||||
|
runtime.irregularityAmplitudeVoxels=static_cast<float>(
|
||||||
|
static_cast<double>(edge.irregularityAmplitudeMicrometres)/micronsPerVoxel);
|
||||||
|
runtime.irregularityWavelengthVoxels=static_cast<float>(
|
||||||
|
static_cast<double>(edge.irregularityWavelengthMicrometres)/micronsPerVoxel);
|
||||||
|
runtime.irregularitySeed=edge.irregularitySeed;
|
||||||
|
CavernRuntime.push_back(runtime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::uint32_t> BuildCavernFeatureTileList(
|
||||||
|
const std::array<std::int64_t,3>& minimumSubvoxels,
|
||||||
|
const std::array<std::int64_t,3>& maximumSubvoxels) const
|
||||||
|
{
|
||||||
|
std::vector<std::uint32_t> result;
|
||||||
|
if(CavernRuntime.empty())return result;
|
||||||
|
if(!Owner||Owner->CoreVoxelSizeMicrons==0)
|
||||||
|
{
|
||||||
|
result.resize(CavernRuntime.size());
|
||||||
|
for(std::size_t i=0;i<result.size();++i)result[i]=static_cast<std::uint32_t>(i);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
constexpr long double Q=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
||||||
|
std::array<long double,3> 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<result.size();++i)
|
||||||
|
result[i]=static_cast<std::uint32_t>(i);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
minimumMicrometres[axis]=static_cast<long double>(minimumSubvoxels[axis])
|
||||||
|
*Owner->CoreVoxelSizeMicrons/Q;
|
||||||
|
maximumMicrometres[axis]=static_cast<long double>(maximumSubvoxels[axis])
|
||||||
|
*Owner->CoreVoxelSizeMicrons/Q;
|
||||||
|
}
|
||||||
|
for(std::size_t index=0;index<CavernRuntime.size();++index)
|
||||||
|
{
|
||||||
|
const auto& feature=CavernRuntime[index];bool overlap=true;
|
||||||
|
for(std::size_t axis=0;axis<3;++axis)
|
||||||
|
overlap=overlap&&feature.boundsMaximumMicrometres[axis]
|
||||||
|
>=minimumMicrometres[axis]
|
||||||
|
&&feature.boundsMinimumMicrometres[axis]
|
||||||
|
<=maximumMicrometres[axis];
|
||||||
|
if(overlap)result.push_back(static_cast<std::uint32_t>(index));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
int BuildPassageNearestNode(std::size_t passageIndex,std::vector<std::size_t> segments)
|
int BuildPassageNearestNode(std::size_t passageIndex,std::vector<std::size_t> segments)
|
||||||
{
|
{
|
||||||
const auto& points=ResolvedLayout->passages[passageIndex].controlPoints;
|
const auto& points=ResolvedLayout->passages[passageIndex].controlPoints;
|
||||||
@@ -3519,6 +3642,7 @@ private:
|
|||||||
void BuildPassageSpatialIndices()
|
void BuildPassageSpatialIndices()
|
||||||
{
|
{
|
||||||
BuildStampSpatialIndex();
|
BuildStampSpatialIndex();
|
||||||
|
BuildCavernSpatialIndex();
|
||||||
PassageTileFeatures.clear();PassageFeatureBounds.clear();
|
PassageTileFeatures.clear();PassageFeatureBounds.clear();
|
||||||
PassageFeatureOrder.clear();PassageFeatureNodes.clear();
|
PassageFeatureOrder.clear();PassageFeatureNodes.clear();
|
||||||
PassageFeatureBoundsFailOpen=false;
|
PassageFeatureBoundsFailOpen=false;
|
||||||
@@ -4459,6 +4583,19 @@ public:
|
|||||||
static_cast<std::int64_t>(BMax)*Q};
|
static_cast<std::int64_t>(BMax)*Q};
|
||||||
if(!BuildStampTileList(minimum,maximum).empty())return false;
|
if(!BuildStampTileList(minimum,maximum).empty())return false;
|
||||||
}
|
}
|
||||||
|
if(!CavernRuntime.empty())
|
||||||
|
{
|
||||||
|
constexpr std::int64_t Q=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
||||||
|
const std::array<std::int64_t,3> minimum={
|
||||||
|
static_cast<std::int64_t>(Origin.X-Pad)*Q,
|
||||||
|
static_cast<std::int64_t>(Origin.Y-Pad)*Q,
|
||||||
|
static_cast<std::int64_t>(BMin)*Q};
|
||||||
|
const std::array<std::int64_t,3> maximum={
|
||||||
|
static_cast<std::int64_t>(Origin.X+(Cells+1)*Step)*Q,
|
||||||
|
static_cast<std::int64_t>(Origin.Y+(Cells+1)*Step)*Q,
|
||||||
|
static_cast<std::int64_t>(BMax)*Q};
|
||||||
|
if(!BuildCavernFeatureTileList(minimum,maximum).empty())return false;
|
||||||
|
}
|
||||||
const float LayoutWidth=static_cast<float>(GetResolvedLayoutChunkWidthVoxels());
|
const float LayoutWidth=static_cast<float>(GetResolvedLayoutChunkWidthVoxels());
|
||||||
const int32 FirstChunk=FMath::FloorToInt(static_cast<float>(BMin)/LayoutWidth);
|
const int32 FirstChunk=FMath::FloorToInt(static_cast<float>(BMin)/LayoutWidth);
|
||||||
const int32 LastChunk=FMath::FloorToInt(static_cast<float>(BMax)/LayoutWidth);
|
const int32 LastChunk=FMath::FloorToInt(static_cast<float>(BMax)/LayoutWidth);
|
||||||
@@ -4563,6 +4700,20 @@ public:
|
|||||||
if(!BuildStampTileList(minimum,maximum).empty())
|
if(!BuildStampTileList(minimum,maximum).empty())
|
||||||
return Finish(EVoxelTileClass::Mixed,true);
|
return Finish(EVoxelTileClass::Mixed,true);
|
||||||
}
|
}
|
||||||
|
if(!CavernRuntime.empty())
|
||||||
|
{
|
||||||
|
constexpr std::int64_t Q=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
||||||
|
const std::array<std::int64_t,3> minimum={
|
||||||
|
static_cast<std::int64_t>(BMin[0])*Q,
|
||||||
|
static_cast<std::int64_t>(BMin[1])*Q,
|
||||||
|
static_cast<std::int64_t>(BMin[2])*Q};
|
||||||
|
const std::array<std::int64_t,3> maximum={
|
||||||
|
static_cast<std::int64_t>(BMax[0])*Q,
|
||||||
|
static_cast<std::int64_t>(BMax[1])*Q,
|
||||||
|
static_cast<std::int64_t>(BMax[2])*Q};
|
||||||
|
if(!BuildCavernFeatureTileList(minimum,maximum).empty())
|
||||||
|
return Finish(EVoxelTileClass::Mixed,true);
|
||||||
|
}
|
||||||
if(ResolvedPassageIntersectsBox(BMin,BMax))
|
if(ResolvedPassageIntersectsBox(BMin,BMax))
|
||||||
return Finish(EVoxelTileClass::Mixed,true);
|
return Finish(EVoxelTileClass::Mixed,true);
|
||||||
if(!EditBrushes.empty()&&EditBoundsMin.X<=BMax[0]&&EditBoundsMax.X>=BMin[0]
|
if(!EditBrushes.empty()&&EditBoundsMin.X<=BMax[0]&&EditBoundsMax.X>=BMin[0]
|
||||||
@@ -5727,6 +5878,252 @@ private:
|
|||||||
apply(static_cast<std::uint32_t>(index));
|
apply(static_cast<std::uint32_t>(index));
|
||||||
return composed;
|
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<double,3> 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.boundsMinimumMicrometres[axis]
|
||||||
|
||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(Z<feature.floorVoxels)return;
|
||||||
|
float distance=StampEllipsoidSdf(localX,localY,localZ,
|
||||||
|
feature.halfExtentsVoxels);
|
||||||
|
if(feature.irregularityAmplitudeVoxels>0.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<float>::infinity();
|
||||||
|
for(int sample=0;sample<=16;++sample)
|
||||||
|
{
|
||||||
|
const float candidate=static_cast<float>(sample)/16.0f;
|
||||||
|
const float dx=X-bezier(candidate,0),dy=Y-bezier(candidate,1);
|
||||||
|
const float distanceSquared=dx*dx+dy*dy;
|
||||||
|
if(distanceSquared<bestDistanceSquared)
|
||||||
|
{bestDistanceSquared=distanceSquared;t=candidate;}
|
||||||
|
}
|
||||||
|
for(int iteration=0;iteration<5;++iteration)
|
||||||
|
{
|
||||||
|
const float dx=bezier(t,0)-X,dy=bezier(t,1)-Y;
|
||||||
|
const float vx=derivative(t,0),vy=derivative(t,1);
|
||||||
|
const float ax=secondDerivative(t,0),ay=secondDerivative(t,1);
|
||||||
|
const float denominator=vx*vx+vy*vy+dx*ax+dy*ay;
|
||||||
|
if(std::abs(denominator)<1.0e-5f)break;
|
||||||
|
t=std::clamp(t-(dx*vx+dy*vy)/denominator,0.0f,1.0f);
|
||||||
|
}
|
||||||
|
const float centerX=bezier(t,0),centerY=bezier(t,1);
|
||||||
|
const float floorZ=bezier(t,2);
|
||||||
|
const float radial=std::hypot(X-centerX,Y-centerY);
|
||||||
|
const float radius=feature.boreRadiusVoxels;
|
||||||
|
const float vertical=(Z-floorZ)/feature.boreHeightVoxels;
|
||||||
|
const float normalized=std::sqrt((radial/radius)*(radial/radius)
|
||||||
|
+vertical*vertical);
|
||||||
|
float distance=(normalized-1.0f)*std::min(radius,feature.boreHeightVoxels);
|
||||||
|
if(feature.irregularityAmplitudeVoxels>0.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(Z<floorZ)
|
||||||
|
{
|
||||||
|
if(composed>0.0f&&feature.floorSupportDepthVoxels>0.0f
|
||||||
|
&&Z>=floorZ-feature.floorSupportDepthVoxels
|
||||||
|
&&radial<radius-0.12f)
|
||||||
|
{
|
||||||
|
const float supportDistance=std::max(radial-(radius-0.12f),
|
||||||
|
std::max(floorZ-feature.floorSupportDepthVoxels-Z,Z-floorZ));
|
||||||
|
composed=std::min(composed,supportDistance);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
composed=SmoothMaxDistance(composed,-distance,feature.smoothWidthVoxels);
|
||||||
|
// The hard profile cap is deliberately applied after the smooth union and
|
||||||
|
// relief. Below it, native rock remains untouched; the narrow support above
|
||||||
|
// fills only baseline air.
|
||||||
|
if(radial<=radius)
|
||||||
|
composed=std::min(composed,Z-floorZ);
|
||||||
|
};
|
||||||
|
if(scoped)
|
||||||
|
for(const auto index:tileQueries->cavernFeatureCandidates())apply(index);
|
||||||
|
else
|
||||||
|
for(std::size_t index=0;index<CavernRuntime.size();++index)
|
||||||
|
apply(static_cast<std::uint32_t>(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<double,3> 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.boundsMinimumMicrometres[axis]
|
||||||
|
||worldMicrometres[axis]>feature.boundsMaximumMicrometres[axis])return;
|
||||||
|
if(feature.room)
|
||||||
|
{
|
||||||
|
if(Z<feature.floorVoxels)return;
|
||||||
|
const float localX=X-feature.centerVoxels[0];
|
||||||
|
const float localY=Y-feature.centerVoxels[1];
|
||||||
|
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)
|
||||||
|
capped=std::min(capped,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<float>::infinity();
|
||||||
|
for(int sample=0;sample<=16;++sample)
|
||||||
|
{
|
||||||
|
const float candidate=static_cast<float>(sample)/16.0f;
|
||||||
|
const float dx=X-bezier(candidate,0),dy=Y-bezier(candidate,1);
|
||||||
|
const float distanceSquared=dx*dx+dy*dy;
|
||||||
|
if(distanceSquared<bestDistanceSquared)
|
||||||
|
{bestDistanceSquared=distanceSquared;t=candidate;}
|
||||||
|
}
|
||||||
|
for(int iteration=0;iteration<5;++iteration)
|
||||||
|
{
|
||||||
|
const float dx=bezier(t,0)-X,dy=bezier(t,1)-Y;
|
||||||
|
const float vx=derivative(t,0),vy=derivative(t,1);
|
||||||
|
const float ax=secondDerivative(t,0),ay=secondDerivative(t,1);
|
||||||
|
const float denominator=vx*vx+vy*vy+dx*ax+dy*ay;
|
||||||
|
if(std::abs(denominator)<1.0e-5f)break;
|
||||||
|
t=std::clamp(t-(dx*vx+dy*vy)/denominator,0.0f,1.0f);
|
||||||
|
}
|
||||||
|
const float floorZ=bezier(t,2);
|
||||||
|
const float radial=std::hypot(X-bezier(t,0),Y-bezier(t,1));
|
||||||
|
if(Z<floorZ)
|
||||||
|
{
|
||||||
|
// Re-evaluate the narrow fallback after passages and stamps, since
|
||||||
|
// those later operations may open air beneath an otherwise native
|
||||||
|
// floor. Negative native density is preserved by the min operation.
|
||||||
|
if(capped>0.0f&&feature.floorSupportDepthVoxels>0.0f
|
||||||
|
&&Z>=floorZ-feature.floorSupportDepthVoxels
|
||||||
|
&&radial<feature.boreRadiusVoxels-0.12f)
|
||||||
|
{
|
||||||
|
const float supportDistance=std::max(radial-
|
||||||
|
(feature.boreRadiusVoxels-0.12f),
|
||||||
|
std::max(floorZ-feature.floorSupportDepthVoxels-Z,Z-floorZ));
|
||||||
|
capped=std::min(capped,supportDistance);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(radial<=feature.boreRadiusVoxels)
|
||||||
|
capped=std::min(capped,Z-floorZ);
|
||||||
|
};
|
||||||
|
if(scoped)
|
||||||
|
for(const auto index:tileQueries->cavernFeatureCandidates())apply(index);
|
||||||
|
else
|
||||||
|
for(std::size_t index=0;index<CavernRuntime.size();++index)
|
||||||
|
apply(static_cast<std::uint32_t>(index));
|
||||||
|
return capped;
|
||||||
|
}
|
||||||
static float SmoothUnit(float V)
|
static float SmoothUnit(float V)
|
||||||
{
|
{
|
||||||
V=std::clamp(V,0.0f,1.0f);
|
V=std::clamp(V,0.0f,1.0f);
|
||||||
@@ -8977,6 +9374,7 @@ private:
|
|||||||
std::vector<FStampSpatialNode> StampSpatialNodes;
|
std::vector<FStampSpatialNode> StampSpatialNodes;
|
||||||
std::vector<std::uint32_t> StampSpatialOrder;
|
std::vector<std::uint32_t> StampSpatialOrder;
|
||||||
std::vector<FStampRuntime> StampRuntime;
|
std::vector<FStampRuntime> StampRuntime;
|
||||||
|
std::vector<FCavernRuntimeFeature> CavernRuntime;
|
||||||
std::vector<VoxelForgeCore::WorldAssemblyFeatureBoundsV1> PassageFeatureBounds;
|
std::vector<VoxelForgeCore::WorldAssemblyFeatureBoundsV1> PassageFeatureBounds;
|
||||||
std::vector<FPassageTileFeature> PassageTileFeatures;
|
std::vector<FPassageTileFeature> PassageTileFeatures;
|
||||||
std::vector<std::uint32_t> PassageFeatureOrder;
|
std::vector<std::uint32_t> PassageFeatureOrder;
|
||||||
|
|||||||
@@ -1334,6 +1334,34 @@ namespace VoxelForgeCore
|
|||||||
}
|
}
|
||||||
return false;
|
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<double>(minimum.X),
|
||||||
|
static_cast<double>(minimum.Y),static_cast<double>(minimum.Z)};
|
||||||
|
const double tileMaximum[3]={static_cast<double>(maximum.X),
|
||||||
|
static_cast<double>(maximum.Y),static_cast<double>(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<double>(feature.boundsMinimumMicrometres[axis])
|
||||||
|
/coreVoxelSizeMicrons;
|
||||||
|
const double high=static_cast<double>(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)
|
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(RouteTouchesBox(*selectedPlan,subMinimum,subMaximum))return false;
|
||||||
if(ResolvedStampTouchesBox(*impl_->resolvedLayout,boundsMin,boundsMax,
|
if(ResolvedStampTouchesBox(*impl_->resolvedLayout,boundsMin,boundsMax,
|
||||||
impl_->owner->CoreVoxelSizeMicrons))return false;
|
impl_->owner->CoreVoxelSizeMicrons))return false;
|
||||||
|
if(ResolvedCavernGraphTouchesBox(*impl_->resolvedLayout,boundsMin,boundsMax,
|
||||||
|
impl_->owner->CoreVoxelSizeMicrons))return false;
|
||||||
if(ResolvedPassageTouchesBox(*impl_->resolvedLayout,boundsMin,boundsMax,
|
if(ResolvedPassageTouchesBox(*impl_->resolvedLayout,boundsMin,boundsMax,
|
||||||
impl_->owner->CoreVoxelSizeMicrons))return false;
|
impl_->owner->CoreVoxelSizeMicrons))return false;
|
||||||
if(OwnerPassageTouchesBox(*impl_->owner,boundsMin,boundsMax))return false;
|
if(OwnerPassageTouchesBox(*impl_->owner,boundsMin,boundsMax))return false;
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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.
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 61 KiB |
@@ -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](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Research3/REPORT.md:5>) · [R12 graph-field recommendation](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Research12/REPORT.md:13>)
|
||||||
|
|
||||||
|
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`](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:906>) to call it; retain bridge, mouth and support policy around the shared geometry query. Update [`BuildPassageNearestNode`](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:3349>) and its lower bounds for spline hulls.
|
||||||
|
- **L45 ramp:** replace the repeated ramp carve/support requests in [`MakeLandmarkChamberDemoRequests`](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Source/StreamBenchmarkHeadless.cpp:113>) with one floor-capped curve record. Route its evaluation through the shared composition/stamp path in [`ShapeWorldInternal.h`](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:1781>).
|
||||||
|
- **R12 graph edges:** store the same curve/floor data on each resolved edge and evaluate it from [`BindResolvedStratumFields`](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:286>); 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](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux35/REPORT.md>) · [L36](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux36/REPORT.md>) · [L45](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux45/REPORT.md>)
|
||||||
@@ -56,6 +56,7 @@ namespace
|
|||||||
int RenderDistanceOverride=0;
|
int RenderDistanceOverride=0;
|
||||||
bool FloatingIslandsRecipe=false;
|
bool FloatingIslandsRecipe=false;
|
||||||
bool LandmarkChamberDemo=false;
|
bool LandmarkChamberDemo=false;
|
||||||
|
bool LandmarkL45RampControl=false;
|
||||||
bool AuditLandmarkReachability=false;
|
bool AuditLandmarkReachability=false;
|
||||||
bool TlsProbeRequested=false;
|
bool TlsProbeRequested=false;
|
||||||
int TlsProbeWorkers=0;
|
int TlsProbeWorkers=0;
|
||||||
@@ -115,10 +116,14 @@ namespace
|
|||||||
const VoxelForgeCore::WorldAssemblyManifest& worldAssembly,
|
const VoxelForgeCore::WorldAssemblyManifest& worldAssembly,
|
||||||
std::int32_t seed,
|
std::int32_t seed,
|
||||||
std::vector<VoxelForgeCore::ResolvedLayoutStampRequestV1>& requests,
|
std::vector<VoxelForgeCore::ResolvedLayoutStampRequestV1>& requests,
|
||||||
|
std::vector<VoxelForgeCore::ResolvedCavernRoomV1>& cavernRooms,
|
||||||
|
std::vector<VoxelForgeCore::ResolvedCavernEdgeV1>& cavernEdges,
|
||||||
|
bool legacyRampControl,
|
||||||
std::string& error)
|
std::string& error)
|
||||||
{
|
{
|
||||||
using namespace VoxelForgeCore;
|
using namespace VoxelForgeCore;
|
||||||
requests.clear();
|
requests.clear();
|
||||||
|
cavernRooms.clear();cavernEdges.clear();
|
||||||
const auto surface=std::find_if(layout.strates.begin(),layout.strates.end(),
|
const auto surface=std::find_if(layout.strates.begin(),layout.strates.end(),
|
||||||
[](const auto& value){return value.generatorKind=="SurfaceWorld";});
|
[](const auto& value){return value.generatorKind=="SurfaceWorld";});
|
||||||
if(surface==layout.strates.end())
|
if(surface==layout.strates.end())
|
||||||
@@ -444,6 +449,100 @@ namespace
|
|||||||
return startZ+descent*std::clamp((beforeFirstTurn+afterFirstTurn)
|
return startZ+descent*std::clamp((beforeFirstTurn+afterFirstTurn)
|
||||||
/slopedLength,0.0,1.0);
|
/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<std::array<double,3>,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::int64_t>(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<double,2> previousCurvePoint={curve[0][0],curve[0][1]};
|
||||||
|
for(int sample=1;sample<=256;++sample)
|
||||||
|
{
|
||||||
|
const double t=static_cast<double>(sample)/256.0,one=1.0-t;
|
||||||
|
std::array<double,3> 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<double,3> previousCurvePosition=curve[0];
|
||||||
|
for(int sample=1;sample<=256;++sample)
|
||||||
|
{
|
||||||
|
const double t=static_cast<double>(sample)/256.0,one=1.0-t;
|
||||||
|
std::array<double,3> 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<curve.size();++i)
|
||||||
|
for(std::size_t axis=0;axis<3;++axis)
|
||||||
|
edge.controlPointsMicrometres[i][axis]=static_cast<std::int64_t>(
|
||||||
|
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<long long>(room.floorMicrometres),
|
||||||
|
static_cast<long long>(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 double pointSpacing=0.6;
|
||||||
const std::size_t rampCount=static_cast<std::size_t>(std::ceil(totalLength/pointSpacing));
|
const std::size_t rampCount=static_cast<std::size_t>(std::ceil(totalLength/pointSpacing));
|
||||||
std::vector<ResolvedLayoutStampRequestV1> rampCarves;
|
std::vector<ResolvedLayoutStampRequestV1> rampCarves;
|
||||||
@@ -506,12 +605,15 @@ namespace
|
|||||||
}
|
}
|
||||||
requests.push_back(std::move(mouthStamp));
|
requests.push_back(std::move(mouthStamp));
|
||||||
requests.push_back(std::move(mouthPortal));
|
requests.push_back(std::move(mouthPortal));
|
||||||
requests.insert(requests.end(),std::make_move_iterator(rampCarves.begin()),
|
if(legacyRampControl)
|
||||||
std::make_move_iterator(rampCarves.end()));
|
{
|
||||||
requests.push_back(std::move(chamber));
|
requests.insert(requests.end(),std::make_move_iterator(rampCarves.begin()),
|
||||||
requests.insert(requests.end(),std::make_move_iterator(rampSupports.begin()),
|
std::make_move_iterator(rampCarves.end()));
|
||||||
std::make_move_iterator(rampSupports.end()));
|
requests.push_back(std::move(chamber));
|
||||||
requests.push_back(std::move(mouthLanding));
|
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(chamberFloor));
|
||||||
requests.push_back(std::move(dais));
|
requests.push_back(std::move(dais));
|
||||||
(void)seed;
|
(void)seed;
|
||||||
@@ -571,7 +673,8 @@ namespace
|
|||||||
" --mesh-export=FILE (write the exact generated tile meshes for offline rendering)\n"
|
" --mesh-export=FILE (write the exact generated tile meshes for offline rendering)\n"
|
||||||
" --profile-column-overlap --far-horizon-lod8-step=N --force-generate\n"
|
" --profile-column-overlap --far-horizon-lod8-step=N --force-generate\n"
|
||||||
" --seed=N --recipe=surface-world|floating-islands\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-output=FILE (canonical plan-time layout snapshot)\n"
|
||||||
" --resolved-layout-only (exit after plan and snapshot validation)\n"
|
" --resolved-layout-only (exit after plan and snapshot validation)\n"
|
||||||
" --audit-resolved-layout-boundaries (query Core at each recorded endpoint)\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<int>(recipe.size()),recipe.data());return false;}
|
else {std::fprintf(stderr,"Invalid --recipe value: %.*s\n",static_cast<int>(recipe.size()),recipe.data());return false;}
|
||||||
}
|
}
|
||||||
else if(arg=="--landmark-chamber-demo")options.LandmarkChamberDemo=true;
|
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="))
|
else if(arg.starts_with("--resolved-layout-output="))
|
||||||
options.ResolvedLayoutOutputPath=std::string(arg.substr(std::string_view("--resolved-layout-output=").size()));
|
options.ResolvedLayoutOutputPath=std::string(arg.substr(std::string_view("--resolved-layout-output=").size()));
|
||||||
else if(arg.starts_with("--seed="))
|
else if(arg.starts_with("--seed="))
|
||||||
@@ -722,6 +826,8 @@ namespace
|
|||||||
{std::fprintf(stderr,"--tls-probe-touch requires --tls-probe=N\n");return false;}
|
{std::fprintf(stderr,"--tls-probe-touch requires --tls-probe=N\n");return false;}
|
||||||
if(options.RegionCrossing&&!options.HasCameraStep)
|
if(options.RegionCrossing&&!options.HasCameraStep)
|
||||||
{std::fprintf(stderr,"--region-crossing requires --camera-step=X,Y,Z\n");return false;}
|
{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.TlsProbeRequested)return true;
|
||||||
if(!options.WorldAssemblerPrototype||!options.StreamBenchmark||!options.StreamPipeline)
|
if(!options.WorldAssemblerPrototype||!options.StreamBenchmark||!options.StreamPipeline)
|
||||||
{
|
{
|
||||||
@@ -820,10 +926,15 @@ int main(int argc,char** argv)
|
|||||||
if(options.LandmarkChamberDemo)
|
if(options.LandmarkChamberDemo)
|
||||||
{
|
{
|
||||||
std::vector<VoxelForgeCore::ResolvedLayoutStampRequestV1> requests;
|
std::vector<VoxelForgeCore::ResolvedLayoutStampRequestV1> requests;
|
||||||
if(!MakeLandmarkChamberDemoRequests(resolvedLayout,worldAssembly,seed,requests,error)
|
std::vector<VoxelForgeCore::ResolvedCavernRoomV1> rooms;
|
||||||
|
std::vector<VoxelForgeCore::ResolvedCavernEdgeV1> edges;
|
||||||
|
if(!MakeLandmarkChamberDemoRequests(resolvedLayout,worldAssembly,seed,
|
||||||
|
requests,rooms,edges,options.LandmarkL45RampControl,error)
|
||||||
||!VoxelForgeCore::ResolveResolvedLayoutStampsV2(
|
||!VoxelForgeCore::ResolveResolvedLayoutStampsV2(
|
||||||
resolvedLayout,seed,requests,error))
|
resolvedLayout,seed,requests,error)
|
||||||
{std::fprintf(stderr,"Landmark chamber stamp resolution failed: %s\n",error.c_str());return 2;}
|
||!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);
|
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",
|
std::printf("[ResolvedStampPlacementAudit] vocabulary=%u records=%zu route_control=rejected band_control=rejected controls=%u/%u status=%s\n",
|
||||||
resolvedLayout.stampVocabularyVersion,resolvedLayout.stamps.size(),
|
resolvedLayout.stampVocabularyVersion,resolvedLayout.stamps.size(),
|
||||||
@@ -849,9 +960,14 @@ int main(int argc,char** argv)
|
|||||||
stamp.irregularityAmplitudeMicrometres/1000000.0,
|
stamp.irregularityAmplitudeMicrometres/1000000.0,
|
||||||
stamp.irregularityWavelengthMicrometres/1000000.0,
|
stamp.irregularityWavelengthMicrometres/1000000.0,
|
||||||
static_cast<unsigned long long>(stamp.irregularitySeed));
|
static_cast<unsigned long long>(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",
|
if(options.LandmarkL45RampControl)
|
||||||
seed,resolvedLayout.stableIdentity.c_str(),
|
std::printf("[LandmarkL45RampControl] enabled=true stamps=%zu graph_records=0 expected=segmented status=PASS\n",
|
||||||
resolvedLayout.stampVocabularyVersion);
|
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 std::string layoutJson=resolvedLayout.ToJson();
|
||||||
const auto roundTrip=VoxelForgeCore::ResolvedLayoutSnapshotV3::FromJson(layoutJson,error);
|
const auto roundTrip=VoxelForgeCore::ResolvedLayoutSnapshotV3::FromJson(layoutJson,error);
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ namespace StandaloneStreamBenchmark
|
|||||||
std::uint64_t supportSamples=0,unsupportedSamples=0,gradeFailures=0;
|
std::uint64_t supportSamples=0,unsupportedSamples=0,gradeFailures=0;
|
||||||
double maximumGradeDegrees=0.0,maximumSupportDepthMetres=0.0;
|
double maximumGradeDegrees=0.0,maximumSupportDepthMetres=0.0;
|
||||||
double minimumClearanceMetres=std::numeric_limits<double>::infinity();
|
double minimumClearanceMetres=std::numeric_limits<double>::infinity();
|
||||||
|
double minimumHeadroomMetres=std::numeric_limits<double>::infinity();
|
||||||
double latestPointMinimumClearanceMetres=std::numeric_limits<double>::infinity();
|
double latestPointMinimumClearanceMetres=std::numeric_limits<double>::infinity();
|
||||||
std::array<double,3> latestPointMinimumClearancePosition{};
|
std::array<double,3> latestPointMinimumClearancePosition{};
|
||||||
bool passed=false;
|
bool passed=false;
|
||||||
@@ -153,13 +154,19 @@ namespace StandaloneStreamBenchmark
|
|||||||
if(surfacePlan==assembly->plan.strates.end())return result;
|
if(surfacePlan==assembly->plan.strates.end())return result;
|
||||||
const VoxelForgeCore::Heightfield2D surfaceField(assembly->recipe,*surfacePlan);
|
const VoxelForgeCore::Heightfield2D surfaceField(assembly->recipe,*surfacePlan);
|
||||||
if(!surfaceField.isValid())return result;
|
if(!surfaceField.isValid())return result;
|
||||||
std::vector<const VoxelForgeCore::ResolvedLayoutStampV1*> ramp;
|
const auto tunnel=std::find_if(layout.cavernEdges.begin(),layout.cavernEdges.end(),
|
||||||
for(const auto& stamp:layout.stamps)
|
[](const auto& edge)
|
||||||
if(stamp.stableId.starts_with("landmark/sw-arrival-chamber-ramp-"))
|
{return edge.stableId=="landmark/sw-arrival-chamber-tunnel-v1";});
|
||||||
ramp.push_back(&stamp);
|
std::vector<const VoxelForgeCore::ResolvedLayoutStampV1*> legacyRamp;
|
||||||
std::sort(ramp.begin(),ramp.end(),[](const auto* a,const auto* b)
|
if(tunnel==layout.cavernEdges.end())
|
||||||
{return a->stableId<b->stableId;});
|
{
|
||||||
if(ramp.size()<2)return result;
|
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->stableId<b->stableId;});
|
||||||
|
if(legacyRamp.size()<2)return result;
|
||||||
|
}
|
||||||
|
|
||||||
const double voxelMetres=static_cast<double>(std::max<std::uint32_t>(1,
|
const double voxelMetres=static_cast<double>(std::max<std::uint32_t>(1,
|
||||||
coreVoxelSizeMicrometres))/1000000.0;
|
coreVoxelSizeMicrometres))/1000000.0;
|
||||||
@@ -177,12 +184,13 @@ namespace StandaloneStreamBenchmark
|
|||||||
return static_cast<double>(shape.densityAt({x/voxelMetres,
|
return static_cast<double>(shape.densityAt({x/voxelMetres,
|
||||||
y/voxelMetres,z/voxelMetres}))*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;
|
constexpr double FloorStep=0.01;
|
||||||
double floorZ=centerZ-5.0;
|
double floorZ=centerZ-5.0;
|
||||||
bool foundFloor=false;
|
bool foundFloor=false;
|
||||||
if(tunnel)
|
if(inTunnel)
|
||||||
{
|
{
|
||||||
if(densityMetres(x,y,centerZ)>0.025)
|
if(densityMetres(x,y,centerZ)>0.025)
|
||||||
for(double offset=-0.10;offset>=-10.0;offset-=FloorStep)
|
for(double offset=-0.10;offset>=-10.0;offset-=FloorStep)
|
||||||
@@ -240,6 +248,23 @@ namespace StandaloneStreamBenchmark
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(measureHeadroom)
|
||||||
|
{
|
||||||
|
constexpr std::array<std::array<double,2>,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<std::array<double,2>,5> supportOffsets={{{0.0,0.0},
|
const std::array<std::array<double,2>,5> supportOffsets={{{0.0,0.0},
|
||||||
{0.28,0.0},{-0.28,0.0},{0.0,0.28},{0.0,-0.28}}};
|
{0.28,0.0},{-0.28,0.0},{0.0,0.28},{0.0,-0.28}}};
|
||||||
std::uint32_t supported=0;
|
std::uint32_t supported=0;
|
||||||
@@ -270,10 +295,16 @@ namespace StandaloneStreamBenchmark
|
|||||||
double floorZ=0.0,cost=std::numeric_limits<double>::infinity();
|
double floorZ=0.0,cost=std::numeric_limits<double>::infinity();
|
||||||
int parent=-1;
|
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<int>(std::llround(
|
const int goalX=GridRadius+static_cast<int>(std::llround(
|
||||||
(ramp.front()->positionMicrometres[0]/1000000.0-arrivalX)/GridStep));
|
(edgeStartX-arrivalX)/GridStep));
|
||||||
const int goalY=GridRadius+static_cast<int>(std::llround(
|
const int goalY=GridRadius+static_cast<int>(std::llround(
|
||||||
(ramp.front()->positionMicrometres[1]/1000000.0-arrivalY)/GridStep));
|
(edgeStartY-arrivalY)/GridStep));
|
||||||
const auto nodeIndex=[](int x,int y){return y*GridWidth+x;};
|
const auto nodeIndex=[](int x,int y){return y*GridWidth+x;};
|
||||||
std::vector<FSurfaceNode> surfaceNodes(static_cast<std::size_t>(GridWidth*GridWidth));
|
std::vector<FSurfaceNode> surfaceNodes(static_cast<std::size_t>(GridWidth*GridWidth));
|
||||||
std::uint64_t exploredNodes=0;
|
std::uint64_t exploredNodes=0;
|
||||||
@@ -364,7 +395,7 @@ namespace StandaloneStreamBenchmark
|
|||||||
double surfaceStartDistance=std::numeric_limits<double>::infinity();
|
double surfaceStartDistance=std::numeric_limits<double>::infinity();
|
||||||
if(!surfaceRoute.empty())
|
if(!surfaceRoute.empty())
|
||||||
{
|
{
|
||||||
path.reserve(surfaceRoute.size()+ramp.size());
|
path.reserve(surfaceRoute.size()+128);
|
||||||
const int startX=surfaceRoute.front()%GridWidth;
|
const int startX=surfaceRoute.front()%GridWidth;
|
||||||
const int startY=surfaceRoute.front()/GridWidth;
|
const int startY=surfaceRoute.front()/GridWidth;
|
||||||
surfaceStartDistance=GridStep*std::hypot(startX-GridRadius,startY-GridRadius);
|
surfaceStartDistance=GridStep*std::hypot(startX-GridRadius,startY-GridRadius);
|
||||||
@@ -375,10 +406,39 @@ namespace StandaloneStreamBenchmark
|
|||||||
path.push_back({arrivalX+(ix-GridRadius)*GridStep,
|
path.push_back({arrivalX+(ix-GridRadius)*GridStep,
|
||||||
arrivalY+(iy-GridRadius)*GridStep,node.floorZ+0.9,false});
|
arrivalY+(iy-GridRadius)*GridStep,node.floorZ+0.9,false});
|
||||||
}
|
}
|
||||||
for(const auto* stamp:ramp)
|
if(tunnel!=layout.cavernEdges.end())
|
||||||
path.push_back({stamp->positionMicrometres[0]/1000000.0,
|
{
|
||||||
stamp->positionMicrometres[1]/1000000.0,
|
const auto curveValue=[&](double t,std::size_t axis)
|
||||||
stamp->positionMicrometres[2]/1000000.0,true});
|
{
|
||||||
|
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<double>(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<int>(std::ceil(curveLength/0.60)));
|
||||||
|
for(int sample=0;sample<=curveSteps;++sample)
|
||||||
|
{
|
||||||
|
const double t=static_cast<double>(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",
|
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,
|
surfaceRoute.empty()?"false":"true",surfaceGoalWalkable?"true":"false",GridStep,surfaceStartDistance,
|
||||||
@@ -405,7 +465,7 @@ namespace StandaloneStreamBenchmark
|
|||||||
const auto capsuleBefore=result.capsuleSamples;
|
const auto capsuleBefore=result.capsuleSamples;
|
||||||
const auto supportBefore=result.supportSamples;
|
const auto supportBefore=result.supportSamples;
|
||||||
const bool tunnel=from.tunnel&&to.tunnel;
|
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;
|
allSupport&=supportOk;
|
||||||
double grade=0.0;
|
double grade=0.0;
|
||||||
if(havePrior)
|
if(havePrior)
|
||||||
@@ -421,7 +481,7 @@ namespace StandaloneStreamBenchmark
|
|||||||
||!supportOk||grade>15.0))
|
||!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",
|
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<int>(label.size()),label.data(),tunnel?"ramp":"surface",x,y,centerZ,floorZ,
|
static_cast<int>(label.size()),label.data(),tunnel?"tunnel":"surface",x,y,centerZ,floorZ,
|
||||||
priorX,priorY,priorFloor,std::hypot(x-priorX,y-priorY),
|
priorX,priorY,priorFloor,std::hypot(x-priorX,y-priorY),
|
||||||
static_cast<unsigned long long>(result.blockedCapsuleSamples-blockedBefore),
|
static_cast<unsigned long long>(result.blockedCapsuleSamples-blockedBefore),
|
||||||
static_cast<unsigned long long>(result.capsuleSamples-capsuleBefore),
|
static_cast<unsigned long long>(result.capsuleSamples-capsuleBefore),
|
||||||
@@ -437,7 +497,214 @@ namespace StandaloneStreamBenchmark
|
|||||||
}
|
}
|
||||||
result.passed=result.pathSamples>1&&result.capsuleSamples>0
|
result.passed=result.pathSamples>1&&result.capsuleSamples>0
|
||||||
&&result.blockedCapsuleSamples==0&&allSupport&&allGrade
|
&&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<double>(std::max<std::uint32_t>(1,
|
||||||
|
coreVoxelSizeMicrometres))/1000000.0;
|
||||||
|
const auto densityMetres=[&](double x,double y,double z)
|
||||||
|
{return static_cast<double>(shape.densityAt({x/voxelMetres,y/voxelMetres,
|
||||||
|
z/voxelMetres}))*voxelMetres;};
|
||||||
|
struct FRoutePoint { double x=0.0,y=0.0,floor=0.0; };
|
||||||
|
std::vector<FRoutePoint> 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<double>(index)/CurveSamples,one=1.0-t;
|
||||||
|
const std::array<double,4> weights={one*one*one,3.0*one*one*t,
|
||||||
|
3.0*one*t*t,t*t*t};
|
||||||
|
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<const VoxelForgeCore::ResolvedLayoutStampV1*> 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->stableId<b->stableId;});
|
||||||
|
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<double> arc(route.size(),0.0);
|
||||||
|
for(std::size_t index=1;index<route.size();++index)
|
||||||
|
arc[index]=arc[index-1]+std::hypot(route[index].x-route[index-1].x,
|
||||||
|
route[index].y-route[index-1].y);
|
||||||
|
const double length=arc.back();
|
||||||
|
if(length<4.0)return result;
|
||||||
|
const auto atDistance=[&](double distance)
|
||||||
|
{
|
||||||
|
distance=std::clamp(distance,0.0,length);
|
||||||
|
const auto upper=std::lower_bound(arc.begin(),arc.end(),distance);
|
||||||
|
const std::size_t hi=std::min<std::size_t>(route.size()-1,
|
||||||
|
static_cast<std::size_t>(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<double>
|
||||||
|
{
|
||||||
|
constexpr double Step=0.025,Below=0.65,Above=0.35;
|
||||||
|
double priorZ=expected-Below;
|
||||||
|
double priorDensity=densityMetres(x,y,priorZ);
|
||||||
|
std::optional<double> closest;
|
||||||
|
double closestError=std::numeric_limits<double>::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<closestError){closest=root;closestError=error;}
|
||||||
|
}
|
||||||
|
priorZ=z;priorDensity=density;
|
||||||
|
}
|
||||||
|
return closest;
|
||||||
|
};
|
||||||
|
std::vector<std::pair<double,double>> 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<std::pair<double,double>>& 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<std::pair<double,double>> 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<std::pair<double,double>> 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<double>(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;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -447,6 +714,8 @@ namespace StandaloneStreamBenchmark
|
|||||||
{
|
{
|
||||||
using namespace VoxelForgeCore;
|
using namespace VoxelForgeCore;
|
||||||
control=source;control.stamps.clear();control.stampVocabularyVersion=1;
|
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,
|
const auto add=[&](std::string id,ResolvedSdfStampKindV1 kind,
|
||||||
std::array<std::int64_t,3> position,
|
std::array<std::int64_t,3> position,
|
||||||
std::array<std::int64_t,3> half,std::int64_t smooth,
|
std::array<std::int64_t,3> half,std::int64_t smooth,
|
||||||
@@ -971,11 +1240,32 @@ namespace StandaloneStreamBenchmark
|
|||||||
if(bAuditLandmarkReachability)
|
if(bAuditLandmarkReachability)
|
||||||
{
|
{
|
||||||
Streamer.Stop();
|
Streamer.Stop();
|
||||||
if(ResolvedLayout==nullptr||!CoreSnapshot.resolvedLayout()
|
if(ResolvedLayout==nullptr||!CoreSnapshot.resolvedLayout())
|
||||||
||ResolvedLayout->stamps.empty())
|
{std::fprintf(stderr,"[LandmarkReachabilityAudit] missing resolved layout\n");return 2;}
|
||||||
{std::fprintf(stderr,"[LandmarkReachabilityAudit] missing demo layout and stamps\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,
|
const auto candidate=AuditLandmarkWalkPath(Shape,Assembly,*ResolvedLayout,
|
||||||
World.CoreVoxelSizeMicrons,"L45");
|
World.CoreVoxelSizeMicrons,"L46");
|
||||||
const auto daisStamp=std::find_if(ResolvedLayout->stamps.begin(),
|
const auto daisStamp=std::find_if(ResolvedLayout->stamps.begin(),
|
||||||
ResolvedLayout->stamps.end(),[](const auto& stamp)
|
ResolvedLayout->stamps.end(),[](const auto& stamp)
|
||||||
{return stamp.stableId=="landmark/sw-arrival-chamber-v1-dais";});
|
{return stamp.stableId=="landmark/sw-arrival-chamber-v1-dais";});
|
||||||
@@ -1009,7 +1299,7 @@ namespace StandaloneStreamBenchmark
|
|||||||
oldBuildError.c_str());return 2;}
|
oldBuildError.c_str());return 2;}
|
||||||
const auto buried=AuditLandmarkWalkPath(oldShape,Assembly,*ResolvedLayout,
|
const auto buried=AuditLandmarkWalkPath(oldShape,Assembly,*ResolvedLayout,
|
||||||
World.CoreVoxelSizeMicrons,"L43");
|
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<unsigned long long>(candidate.pathSamples),
|
static_cast<unsigned long long>(candidate.pathSamples),
|
||||||
static_cast<unsigned long long>(candidate.capsuleSamples),
|
static_cast<unsigned long long>(candidate.capsuleSamples),
|
||||||
static_cast<unsigned long long>(candidate.blockedCapsuleSamples),
|
static_cast<unsigned long long>(candidate.blockedCapsuleSamples),
|
||||||
@@ -1017,14 +1307,18 @@ namespace StandaloneStreamBenchmark
|
|||||||
static_cast<unsigned long long>(candidate.unsupportedSamples),
|
static_cast<unsigned long long>(candidate.unsupportedSamples),
|
||||||
static_cast<unsigned long long>(candidate.gradeFailures),
|
static_cast<unsigned long long>(candidate.gradeFailures),
|
||||||
candidate.maximumGradeDegrees,candidate.minimumClearanceMetres,
|
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",
|
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<unsigned long long>(buried.pathSamples),
|
static_cast<unsigned long long>(buried.pathSamples),
|
||||||
static_cast<unsigned long long>(buried.blockedCapsuleSamples),
|
static_cast<unsigned long long>(buried.blockedCapsuleSamples),
|
||||||
static_cast<unsigned long long>(buried.unsupportedSamples),
|
static_cast<unsigned long long>(buried.unsupportedSamples),
|
||||||
static_cast<unsigned long long>(buried.gradeFailures),
|
static_cast<unsigned long long>(buried.gradeFailures),
|
||||||
buried.passed?"PASS":"FAIL",buried.passed?"FAIL":"PASS");
|
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",
|
std::printf("[LandmarkReachabilityAudit] positive=PASS control=%s status=%s\n",
|
||||||
buried.passed?"FAILED":"EXPECTED_FAIL",passed?"PASS":"FAIL");
|
buried.passed?"FAILED":"EXPECTED_FAIL",passed?"PASS":"FAIL");
|
||||||
return passed?0:13;
|
return passed?0:13;
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ def run_landmark_reachability(runner: VerificationRunner,
|
|||||||
expected_style=2, expected_strate_styles={"0": 2, "1": 1},
|
expected_style=2, expected_strate_styles={"0": 2, "1": 1},
|
||||||
max_level=0, layout_path=OUT / "layouts" / f"{passage_name}.json")
|
max_level=0, layout_path=OUT / "layouts" / f"{passage_name}.json")
|
||||||
runner.require(f"{passage_name}:demo-enabled",
|
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")
|
"landmark demo flag resolved while running passage audits")
|
||||||
validate_passage(runner, passage_name, passage_output)
|
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},
|
expected_style=2, expected_strate_styles={"0": 2, "1": 1},
|
||||||
max_level=0, layout_path=OUT / "layouts" / f"{interface_name}.json")
|
max_level=0, layout_path=OUT / "layouts" / f"{interface_name}.json")
|
||||||
runner.require(f"{interface_name}:demo-enabled",
|
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")
|
"landmark demo flag resolved while running interface audits")
|
||||||
runner.require(f"{interface_name}:hole-audit",
|
runner.require(f"{interface_name}:hole-audit",
|
||||||
all_match(interface_output, r"^\[StrateTransitionAuditSummary\].*hole_columns=0 "
|
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$"),
|
all_match(interface_output, r"^\[StrateTransitionNegativeControls\].*status=PASS$"),
|
||||||
"demo-on interface audit detects both thickness controls")
|
"demo-on interface audit detects both thickness controls")
|
||||||
runner.require(f"{name}:candidate-walkability",
|
runner.require(f"{name}:candidate-walkability",
|
||||||
all_match(output, r"^\[LandmarkWalkability\] layout=L45 .*status=PASS$"),
|
all_match(output, r"^\[LandmarkWalkability\] layout=L46 .*headroom_required_m=0\.300 status=PASS$"),
|
||||||
"demo capsule clearance, support and <=15 degree grade")
|
"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",
|
runner.require(f"{name}:buried-l43-control",
|
||||||
all_match(output, r"^\[LandmarkReachabilityControl\] layout=L43-buried .*"
|
all_match(output, r"^\[LandmarkReachabilityControl\] layout=L43-buried .*"
|
||||||
r"result=FAIL expected=FAIL status=PASS$"),
|
r"result=FAIL expected=FAIL status=PASS$"),
|
||||||
@@ -227,6 +233,18 @@ def run_landmark_reachability(runner: VerificationRunner,
|
|||||||
all_match(output, r"^\[LandmarkReachabilityAudit\] positive=PASS "
|
all_match(output, r"^\[LandmarkReachabilityAudit\] positive=PASS "
|
||||||
r"control=EXPECTED_FAIL status=PASS$"),
|
r"control=EXPECTED_FAIL status=PASS$"),
|
||||||
"candidate passes and buried control fails")
|
"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:
|
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
|
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, ...],
|
def run_random_camera_sweep(runner: VerificationRunner, binaries: tuple[Path, ...],
|
||||||
camera_count: int,
|
camera_count: int,
|
||||||
configurations: tuple[tuple[int, int, int], ...]) -> None:
|
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}",
|
runner.require(f"{label}:W{left}-W{right}",
|
||||||
left_rows == right_rows, detail)
|
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:
|
def run_ring(runner: VerificationRunner, *, full: bool) -> None:
|
||||||
script = LINUX37 / "RunRingAuditsL37.py"
|
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",
|
script_inputs=[overview_script, VERIFY_DIR / "capture_quality.py",
|
||||||
OUT / "owner-fi-ground-mouth.vfmesh"])
|
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"
|
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),
|
["--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)],
|
"--data", str(OWNER)],
|
||||||
binary=CLANG, data=OWNER, recipe="surface-world", style=1,
|
binary=CLANG, data=OWNER, recipe="surface-world", style=1,
|
||||||
layout_path=landmark_layout, env=env_for(capture=True),
|
layout_path=landmark_layout, env=env_for(capture=True),
|
||||||
|
|||||||
Reference in new issue
Block a user