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:
Fr0zkaandClaude Opus 5.5 committed 2026-10-24 09:53:06 +02:00
1 parent f13b145f23
commit e016f6a20f
14 files changed
+1753 -48

No files matched your search

@@ -404,6 +404,56 @@ namespace
}
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();
}
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)
{return a.interiorTopMicrometres!=b.interiorTopMicrometres
?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)
{
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))
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;}
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.strateLayoutChunkWidthSubvoxels=LayoutInt(root,"strate_layout_chunk_width_subvoxels");
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())
{
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"));
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");
if(!s.finalize(error))return std::nullopt;
if(s.stableIdentity!=serializedIdentity){error="resolved layout stable identity does not match its canonical payload";return std::nullopt;}
@@ -1080,6 +1291,67 @@ bool ResolveResolvedLayoutStampsV2(ResolvedLayoutSnapshotV3& layout,std::int32_t
layout=std::move(candidate);return true;
}
bool ResolveResolvedCavernGraphV1(ResolvedLayoutSnapshotV3& layout,std::int32_t seed,
std::span<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(
const ResolvedLayoutSnapshotV3& layout)
{