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:
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={},
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user