core: Tunnel Network cave planner with a continuous backbone floor (L134, evaluator v230)

Backbone rooms and edges share one floor profile h(s); side branches keep flat floors; overlap backing capped at the
lower floor. Default seed: route audit 0 blocked / 0 unsupported / 0 grade over 51,975 capsule samples; passages
open; fast tier, seams, Clang/GCC W1/W8/W12 green. Open: seed 1 (24 blocked at the 2-3 arrival mouth) and seed 2
(22 unsupported at passage 1-2) fail composed audits; near-ready +6..+14% on layers 1-3. Not for merge.

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-31 14:03:35 +01:00
1 parent 4a97023b24
commit a3c8e3687d
23 files changed
+338 -53

No files matched your search

@@ -119,6 +119,48 @@ namespace VoxelForgeCore::PassageSpline
return std::lerp(plannedFloor,receivingFloor,blend);
}
inline void BlendEndpointFloorToRoute(std::span<const FPoint> controls,
std::vector<double>& floorProfile,std::size_t endpoint,
std::span<const std::array<double,3>> receivingRoute,double unitsPerMetre,
double playerCenterAboveFloorMetres=1.13,double maximumGradeDegrees=12.0)
{
if(controls.size()!=floorProfile.size()||endpoint>=floorProfile.size()
||receivingRoute.empty()||!(unitsPerMetre>0.0)
||!(maximumGradeDegrees>0.0&&maximumGradeDegrees<45.0))return;
const double maximumGrade=std::tan(maximumGradeDegrees
*3.14159265358979323846/180.0);
const double routeReach=controls[endpoint][3]/unitsPerMetre+0.5;
constexpr double MouthProfileBlendDistanceMetres=10.0;
double routeHeight=0.0,routeDistanceSquared=0.0;
if(!ClosestRouteHeightAtXY(receivingRoute,controls[endpoint][0],
controls[endpoint][1],routeHeight,routeDistanceSquared)
||routeDistanceSquared>std::pow(routeReach*unitsPerMetre,2.0))return;
floorProfile[endpoint]=routeHeight
-playerCenterAboveFloorMetres*unitsPerMetre;
double pathDistance=0.0;
for(std::size_t step=1;step<controls.size();++step)
{
if(endpoint>0&&step>endpoint)break;
const std::size_t index=endpoint==0?step:endpoint-step;
const std::size_t previous=endpoint==0?index-1:index+1;
const double dx=(controls[index][0]-controls[previous][0])/unitsPerMetre;
const double dy=(controls[index][1]-controls[previous][1])/unitsPerMetre;
const double horizontal=std::hypot(dx,dy);
pathDistance+=horizontal;
double localRouteHeight=0.0,localRouteDistanceSquared=0.0;
const bool followsRoute=pathDistance<=MouthProfileBlendDistanceMetres
&&ClosestRouteHeightAtXY(receivingRoute,
controls[index][0],controls[index][1],localRouteHeight,localRouteDistanceSquared)
&&localRouteDistanceSquared<=std::pow(routeReach*unitsPerMetre,2.0);
double target=floorProfile[index];
if(followsRoute)
target=localRouteHeight-playerCenterAboveFloorMetres*unitsPerMetre;
const double maximumStep=maximumGrade*horizontal*unitsPerMetre;
floorProfile[index]=std::clamp(target,
floorProfile[previous]-maximumStep,floorProfile[previous]+maximumStep);
}
}
inline std::vector<FSegment> BuildSegments(std::span<const FPoint> controls,
std::size_t spanCount,double spacingMetres,double unitsPerMetre=1.0,
std::span<const double> plannedFloorProfile={},
+22 -15
View File
@@ -1247,7 +1247,7 @@ bool ResolvedLayoutSnapshotV3::validate(ResolvedLayoutValidationModeV3 mode,
else if(cavernGraphVocabularyVersion!=ResolvedLayoutCavernGraphVocabularyVersionV1
&&cavernGraphVocabularyVersion!=ResolvedLayoutCavernGraphVocabularyVersionV2)
add("cavern_graph_vocabulary_version",
"cavern graph records require vocabulary version 1");
"cavern graph records require a supported vocabulary version");
if(cavernGraphVocabularyVersion!=0)
{
if(cavernRooms.empty()||cavernEdges.empty())
@@ -1447,9 +1447,13 @@ bool ResolvedLayoutSnapshotV3::validate(ResolvedLayoutValidationModeV3 mode,
previous->floorProfileEndDistanceMicrometres);
if(low!=previousHigh
||std::abs(endpointZ(*previous,false)-endpointZ(*edge,true))>1000
||std::abs(endpointSlope(*previous,false)-slope0)>0.000001)
add("cavern_floor_profile_joint",profileId+
" has a gap, height step, or slope step between adjacent edges");
||std::abs(endpointSlope(*previous,false)-slope0)>0.00001)
add("cavern_floor_profile_joint",profileId+" between "+previous->stableId
+" and "+edge->stableId+" has distance gap="
+std::to_string(low-previousHigh)+" um height step="
+std::to_string(endpointZ(*previous,false)-endpointZ(*edge,true))
+" um slope step="
+std::to_string(endpointSlope(*previous,false)-slope0));
}
previous=edge;
(void)high;
@@ -2320,7 +2324,9 @@ bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout,
points[2][axis]=routeStart[axis]+(delta*2)/3;
}
points[1][0]+=perpX;points[1][1]+=perpY;
points[2][0]-=perpX;points[2][1]-=perpY;
// Keep both control points on the same side of the mouth axis. Opposite
// offsets create a tight hairpin whose descending bores overlap in XY.
points[2][0]+=perpX;points[2][1]+=perpY;
return points;
};
const auto routeLength=[&](const std::array<FPoint,4>& points)
@@ -2545,8 +2551,15 @@ bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout,
room.floorGradientQ16=node.floorGradientQ16;
room.halfExtentsMicrometres={halfX,halfY,halfZ};
room.centerMicrometres={node.point[0]+(-pathAxisY)*centerOffset/routeLengthXY,
node.point[1]+pathAxisX*centerOffset/routeLengthXY,
node.point[2]+fractional(halfZ,floorCut)};
node.point[1]+pathAxisX*centerOffset/routeLengthXY,0};
// Keep the local floor plane on the backbone profile at the route point,
// then place each offset primitive from that plane at its own XY centre.
room.floorMicrometres=node.point[2]
+(static_cast<std::int64_t>(room.floorGradientQ16[0])
*(room.centerMicrometres[0]-node.point[0])
+static_cast<std::int64_t>(room.floorGradientQ16[1])
*(room.centerMicrometres[1]-node.point[1]))/65536;
room.centerMicrometres[2]=room.floorMicrometres+fractional(halfZ,floorCut);
if(component>0)
{
const std::int64_t headroom=std::max<std::int64_t>(0,
@@ -2557,11 +2570,6 @@ bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout,
%static_cast<std::uint64_t>(jitterRange+1)):0;
room.centerMicrometres[2]+=ceilingJitter;
}
room.floorMicrometres=node.point[2]
-(static_cast<std::int64_t>(room.floorGradientQ16[0])
*(room.centerMicrometres[0]-node.point[0])
+static_cast<std::int64_t>(room.floorGradientQ16[1])
*(room.centerMicrometres[1]-node.point[1]))/65536;
room.smoothWidthMicrometres=smooth;
room.irregularityAmplitudeMicrometres=irregularity;
room.irregularityWavelengthMicrometres=settings.irregularityWavelengthMicrometres;
@@ -2665,9 +2673,8 @@ bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout,
branch.point={parent.point[0]+(-pathAxisY)*branchLength/pathAxisLength*direction,
parent.point[1]+pathAxisX*branchLength/pathAxisLength*direction,
parent.point[2]};
branch.floorProfileStableId=parent.floorProfileStableId;
branch.floorProfileDistanceMicrometres=parent.floorProfileDistanceMicrometres;
branch.floorGradientQ16=parent.floorGradientQ16;
// A side branch leaves the backbone laterally, so it has its own local flat
// floor and must not reference the parent's one-dimensional h(s) profile.
makeCompound(branch,false,rooms);
const std::string branchEdgeId="tunnel-network/strate-"
+std::to_string(strateIndex)+"/side-edge-"+std::to_string(branchCount);
+91 -7
View File
@@ -1221,15 +1221,25 @@ public:
double unionDistance=std::numeric_limits<double>::infinity();
double unionShellDistance=std::numeric_limits<double>::infinity();
const std::uint32_t reliefSeed=static_cast<std::uint32_t>(Owner?Owner->Seed:0);
const auto* queryStratum=FindBoundStratumForManagerZ(Z);
for(std::size_t passageIndex=0;passageIndex<ResolvedLayout->passages.size();
++passageIndex)
{
const auto& passage=ResolvedLayout->passages[passageIndex];
if(passage.controlPoints.size()<2)continue;
// Native graph fields own their local rooms and tunnels, not the
// inter-strate connection above their mouth. Union the shared passage
// field here as well so the connector remains open through the boundary
// band for both native caves and future native lattice layers.
const bool passageTargetsNativeCaveField=queryStratum&&queryStratum->Resolved
&&queryStratum->Resolved->passageCapabilities.hasNativeCaveField
&&passage.lowerStrateIndex==queryStratum->StrateIndex;
const bool passageTargetsPlannedTunnelNetwork=queryStratum
&&queryStratum->Resolved
&&queryStratum->Resolved->generatorKind=="TunnelNetwork"
&&queryStratum->Resolved->recipeId=="Core.Caves.PrimitiveField"
&&queryStratum->Resolved->recipeVersion==1;
// Legacy native cave fields own the floor and mouth carve on their
// lower-strate boundary. The planner's TunnelNetwork field instead
// needs the shared passage composer to carry its incoming mouth open.
if(passageTargetsNativeCaveField&&!passageTargetsPlannedTunnelNetwork)
continue;
if(passageProfile)++passageProfile->passageIterations;
if(passageTileScopeMatches&&!passageTileQueries->hasRouteFeature(passageIndex))
continue;
@@ -3397,6 +3407,10 @@ private:
double upperMouthPlannedFloor=0.0;
double upperMouthTangentX=0.0,upperMouthTangentY=0.0;
std::vector<std::array<double,3>> upperMouthReceivingRoute;
double lowerMouthX=0.0,lowerMouthY=0.0;
double lowerMouthPlannedFloor=0.0;
double lowerMouthTangentX=0.0,lowerMouthTangentY=0.0;
std::vector<std::array<double,3>> lowerMouthReceivingRoute;
std::size_t routeEndControlIndex=0;
};
struct FPassageSegmentData
@@ -3456,14 +3470,21 @@ private:
+profileT*profileSegment.authoredFloorZDelta;
}
}
const double mouthBlendFloor=VoxelForgeCore::PassageSpline::BlendUpperMouthFloorAtXY(
const double upperBlendFloor=VoxelForgeCore::PassageSpline::BlendUpperMouthFloorAtXY(
constants.upperMouthReceivingRoute,queryXSubvoxels,queryYSubvoxels,
plannedFloor,constants.upperMouthX,constants.upperMouthY,
constants.upperMouthPlannedFloor,
constants.upperMouthTangentX,constants.upperMouthTangentY,
VoxelForgeCore::PassageSpline::UpperMouthFloorBlendRadiusMetres,1.13,
qPerMetre);
return mouthBlendFloor+floorDepthSubvoxels;
const double lowerBlendFloor=VoxelForgeCore::PassageSpline::BlendUpperMouthFloorAtXY(
constants.lowerMouthReceivingRoute,queryXSubvoxels,queryYSubvoxels,
upperBlendFloor,constants.lowerMouthX,constants.lowerMouthY,
constants.lowerMouthPlannedFloor,
-constants.lowerMouthTangentX,-constants.lowerMouthTangentY,
VoxelForgeCore::PassageSpline::UpperMouthFloorBlendRadiusMetres,1.13,
qPerMetre);
return lowerBlendFloor+floorDepthSubvoxels;
}
static float PassageFlatBottomDistance(float roundDistanceSubvoxels,
@@ -4625,7 +4646,8 @@ private:
ResolvedLayout->strates.end(),[&](const auto& stratum)
{return stratum.strateIndex==passage.upperStrateIndex;});
if(upperStratum!=ResolvedLayout->strates.end()
&&upperStratum->generatorKind=="FloatingIslands")
&&(upperStratum->generatorKind=="FloatingIslands"
||upperStratum->generatorKind=="TunnelNetwork"))
{
std::vector<std::array<double,3>> route;
for(const auto& anchor:upperStratum->anchors)
@@ -4633,16 +4655,77 @@ private:
route.push_back({static_cast<double>(anchor.values[0]),
static_cast<double>(anchor.values[1]),
static_cast<double>(anchor.values[2])});
if(upperStratum->generatorKind=="TunnelNetwork"&&route.size()>2)
route.erase(route.begin(),route.end()-2);
constants.upperMouthReceivingRoute=route;
double routeHeight=0.0,distanceSquared=0.0;
if(VoxelForgeCore::PassageSpline::ClosestRouteHeightAtXY(route,
controls.front()[0],controls.front()[1],routeHeight,distanceSquared)
&&distanceSquared<=std::pow(controls.front()[3]+2.0*qPerMetre,2.0))
{
upperMouthFloorDepth=std::max(upperMouthFloorDepth,
std::max(0.0,(controls.front()[2]-routeHeight)/qPerMetre+1.13));
if(upperStratum->generatorKind=="TunnelNetwork")
VoxelForgeCore::PassageSpline::BlendEndpointFloorToRoute(
controls,plannedFloorProfile,0,route,qPerMetre);
}
}
controls.front()[3]=std::max(controls.front()[3],
upperMouthFloorDepth*qPerMetre);
if(upperStratum!=ResolvedLayout->strates.end()
&&(upperStratum->generatorKind=="FloatingIslands"
||upperStratum->generatorKind=="TunnelNetwork"))
for(const auto& anchor:upperStratum->anchors)
if(anchor.kind=="route_point"&&anchor.values.size()>=3)
constants.upperMouthReceivingRoute.push_back({
static_cast<double>(anchor.values[0]),
static_cast<double>(anchor.values[1]),
static_cast<double>(anchor.values[2])});
if(upperStratum!=ResolvedLayout->strates.end()
&&upperStratum->generatorKind=="TunnelNetwork"
&&constants.upperMouthReceivingRoute.size()>2)
constants.upperMouthReceivingRoute.erase(
constants.upperMouthReceivingRoute.begin(),
constants.upperMouthReceivingRoute.end()-2);
const auto lowerStratum=std::find_if(ResolvedLayout->strates.begin(),
ResolvedLayout->strates.end(),[&](const auto& stratum)
{return stratum.strateIndex==passage.lowerStrateIndex;});
if(lowerStratum!=ResolvedLayout->strates.end()
&&lowerStratum->generatorKind=="TunnelNetwork"
&&routeEnd<controls.size())
{
double routeHeight=0.0,distanceSquared=0.0;
for(const auto& anchor:lowerStratum->anchors)
if(anchor.kind=="route_point"&&anchor.values.size()>=3)
constants.lowerMouthReceivingRoute.push_back({
static_cast<double>(anchor.values[0]),
static_cast<double>(anchor.values[1]),
static_cast<double>(anchor.values[2])});
if(constants.lowerMouthReceivingRoute.size()>2)
constants.lowerMouthReceivingRoute.resize(2);
if(VoxelForgeCore::PassageSpline::ClosestRouteHeightAtXY(
constants.lowerMouthReceivingRoute,controls[routeEnd][0],
controls[routeEnd][1],routeHeight,distanceSquared)
&&distanceSquared<=std::pow(controls[routeEnd][3]
+2.0*qPerMetre,2.0))
{
VoxelForgeCore::PassageSpline::BlendEndpointFloorToRoute(
controls,plannedFloorProfile,routeEnd,
constants.lowerMouthReceivingRoute,qPerMetre);
constants.lowerMouthX=controls[routeEnd][0];
constants.lowerMouthY=controls[routeEnd][1];
constants.lowerMouthPlannedFloor=plannedFloorProfile[routeEnd];
if(routeEnd>0)
{
constants.lowerMouthTangentX=controls[routeEnd][0]
-controls[routeEnd-1][0];
constants.lowerMouthTangentY=controls[routeEnd][1]
-controls[routeEnd-1][1];
}
}
else constants.lowerMouthReceivingRoute.clear();
}
const auto sampledSegments=VoxelForgeCore::PassageSpline::BuildSegments(
controls,controls.size()-1,PassageSegmentSpacingMetres,qPerMetre,
plannedFloorProfile,PassageSegmentChordErrorMetres);
@@ -7631,6 +7714,7 @@ private:
||(nearestFloor==floorSupportFloor
&&feature.edge->stableId<*floorSupportEdgeId))
{
floorSupportScore=candidateScore;
floorSupportDepth=candidateSupportDepth;
floorSupportFloor=nearestFloor;
floorSupportGrade=nearestGrade;