passages: four-strate passage floors supported (L67, evaluator v212); 1-2 wall beat still red
- Shared composer: a passage's restored support is applied after a physically overlapping later bore, instead of being carved away by it (0-1: 59 -> 0 unsupported). - Capsule clearance for support restoration is tested against nearby route segments only (1-2: 3 -> 0, 2-3: 2 -> 0 unsupported). - L66 index switches replaced by a physical criterion (both mouths inside their strata bands, neither a SurfaceWorld landing). - Audit: floor zero-crossing refined by bisection (the 0.2498 m "step" was the gauge); wall ripple keeps the authored radius, cubic detrend per 32 m window. Controls detected: L35 ribs, beads, synthetic 0.25 m step, L36 washboard, L23 terrace. - Result: 0-1 and 2-3 pass all audits; 1-2 fails wall ripple 7.27 cm (limit 3.5) from an authored-radius beat at 7.06 m. Fast tier was green before the final gauge change and was not re-run after it. 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
5e2fc53d39
commit
087333bf8a
17 files changed
+1439
-43
No files matched your search
@@ -402,7 +402,8 @@ namespace VoxelForgeCore
|
||||
// Version 208 keeps the crater tread scoped to style-zero FI mouths.
|
||||
// Version 210 keeps bulk native-cave sampling on the scalar field path at strata edges.
|
||||
// Version 211 makes coordinate biasing in the shared cell hash unsigned, removing signed-overflow UB.
|
||||
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 211;
|
||||
// Version 212 balances generated entry routes against existing walk lanes and preserves their shared treads.
|
||||
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 212;
|
||||
|
||||
enum class FieldChannel : std::uint32_t
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <iomanip>
|
||||
#include <limits>
|
||||
#include <sstream>
|
||||
@@ -2459,18 +2460,29 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
}
|
||||
std::uint32_t phase=static_cast<std::uint32_t>(PassageSeed(seed,
|
||||
upper.strateIndex,lower.strateIndex,connection,0x50484153u)%16);
|
||||
if(hasUpperRoute)
|
||||
// Authored tangents and island anchors already determine the phase.
|
||||
// A generated mouth in a native cave uses its radial approach; an
|
||||
// island landing uses the tangent fit below to ease off the crater rim.
|
||||
const bool generatedIslandWormEntry=!hasUpperRoute&&!hasUpperTangent
|
||||
&&!anchoredSpiral&&upper.generatorKind=="FloatingIslands";
|
||||
if(hasUpperRoute||(anchoredIslandPassage&&!anchoredSpiral)
|
||||
||(!hasUpperRoute&&!anchoredIslandPassage&&!anchoredSpiral
|
||||
&&upper.generatorKind!="FloatingIslands"))
|
||||
{
|
||||
std::int64_t bestAlignment=std::numeric_limits<std::int64_t>::min();
|
||||
const std::uint32_t hashedPhase=phase;
|
||||
const std::int64_t phaseX=style==0&&anchoredIslandPassage
|
||||
&&!anchoredSpiral&&passageAnchor
|
||||
?upperMouth[0]-passageAnchor->values[0]
|
||||
:(hasUpperTangent?upperTangent[0]:upperMouth[0]);
|
||||
:(hasUpperTangent?upperTangent[0]:upperMouth[0]
|
||||
-(anchoredIslandPassage&&passageAnchor
|
||||
?passageAnchor->values[0]:0));
|
||||
const std::int64_t phaseY=style==0&&anchoredIslandPassage
|
||||
&&!anchoredSpiral&&passageAnchor
|
||||
?upperMouth[1]-passageAnchor->values[1]
|
||||
:(hasUpperTangent?upperTangent[1]:upperMouth[1]);
|
||||
:(hasUpperTangent?upperTangent[1]:upperMouth[1]
|
||||
-(anchoredIslandPassage&&passageAnchor
|
||||
?passageAnchor->values[1]:0));
|
||||
for(std::uint32_t offset=0;offset<16;++offset)
|
||||
{
|
||||
const std::uint32_t candidate=static_cast<std::uint32_t>(
|
||||
@@ -2528,7 +2540,7 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
clockwise=clockwiseTangentX*upperTangent[0]
|
||||
+clockwiseTangentY*upperTangent[1]>=0.0;
|
||||
}
|
||||
const int direction=clockwise?1:-1;
|
||||
int direction=clockwise?1:-1;
|
||||
constexpr std::int64_t TargetGradeQ16=12500; // leave margin below the 15-degree helix contract.
|
||||
const std::int64_t wormRadialWobble=std::min<std::int64_t>(
|
||||
std::max<std::int64_t>(0,wander/3),2*qPerMetre);
|
||||
@@ -2597,6 +2609,267 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
static_cast<std::int64_t>(std::llround(centerX+radius*directions[directionIndex][0]/65536.0)),
|
||||
static_cast<std::int64_t>(std::llround(centerY+radius*directions[directionIndex][1]/65536.0))};
|
||||
};
|
||||
if(generatedIslandWormEntry)
|
||||
{
|
||||
struct FPhaseScore
|
||||
{
|
||||
std::uint32_t phase=0;
|
||||
int direction=1;
|
||||
std::uint64_t routeLength=0;
|
||||
double clearance=0.0;
|
||||
double tangent=0.0;
|
||||
};
|
||||
std::array<FPhaseScore,32> phaseScores{};
|
||||
std::size_t phaseScoreCount=0;
|
||||
const std::uint32_t hashedPhase=phase;
|
||||
const int hashedDirection=direction;
|
||||
const std::int64_t entryVerticalDistance=std::abs(
|
||||
lowerMouth[2]-upperMouth[2]);
|
||||
const std::int64_t entryRouteGradeQ16=profiledSurfaceJoin?8000
|
||||
:style==0&&anchoredIslandPassage&&!anchoredSpiral?15500
|
||||
:TargetGradeQ16;
|
||||
const auto candidateRouteLength=[&](std::uint32_t candidate,int candidateDirection)
|
||||
{
|
||||
std::vector<std::array<std::int64_t,2>> candidateXY;
|
||||
const auto append=[&](std::array<std::int64_t,2> point)
|
||||
{if(candidateXY.empty()||candidateXY.back()!=point)
|
||||
candidateXY.push_back(point);};
|
||||
append({upperMouth[0],upperMouth[1]});
|
||||
append(ringPoint(candidate));
|
||||
const auto measure=[&]()
|
||||
{
|
||||
std::uint64_t length=0;
|
||||
for(std::size_t i=1;i<candidateXY.size();++i)
|
||||
length+=distanceQ(candidateXY[i][0]-candidateXY[i-1][0],
|
||||
candidateXY[i][1]-candidateXY[i-1][1]);
|
||||
length+=distanceQ(lowerMouth[0]-candidateXY.back()[0],
|
||||
lowerMouth[1]-candidateXY.back()[1]);
|
||||
return length;
|
||||
};
|
||||
std::uint32_t loops=0;
|
||||
std::uint64_t length=0;
|
||||
while(loops<256)
|
||||
{
|
||||
length=measure();
|
||||
if(length*static_cast<std::uint64_t>(entryRouteGradeQ16)
|
||||
>=static_cast<std::uint64_t>(entryVerticalDistance)*65536ull)break;
|
||||
for(int step=1;step<=16;++step)
|
||||
{
|
||||
int directionIndex=static_cast<int>(candidate)
|
||||
+candidateDirection*step;
|
||||
directionIndex=(directionIndex%16+16)%16;
|
||||
append(ringPoint(static_cast<std::uint32_t>(directionIndex),
|
||||
loops+(step==16?1u:0u)));
|
||||
}
|
||||
++loops;
|
||||
}
|
||||
if(loops>=256)return std::uint64_t{0};
|
||||
const std::int64_t mouthPlanSeparation=distanceQ(
|
||||
lowerMouth[0]-upperMouth[0],lowerMouth[1]-upperMouth[1]);
|
||||
if(!hasLowerTangent&&!anchoredSpiral&&pathStyle==1u&&loops<=1
|
||||
&&mouthPlanSeparation<=2*mouthRadius
|
||||
&&entryVerticalDistance<=2*mouthRadius+qPerMetre/2)
|
||||
{
|
||||
double bestExitScore=-std::numeric_limits<double>::infinity();
|
||||
std::uint32_t lowerPhase=candidate;
|
||||
const auto firstRing=ringPoint(candidate);
|
||||
for(std::uint32_t exitCandidate=0;exitCandidate<16;++exitCandidate)
|
||||
{
|
||||
if(exitCandidate==candidate)continue;
|
||||
const std::uint32_t previous=static_cast<std::uint32_t>(
|
||||
(static_cast<int>(exitCandidate)-candidateDirection+16)%16);
|
||||
const auto terminal=ringPoint(exitCandidate,loops);
|
||||
const auto beforeTerminal=ringPoint(previous,loops);
|
||||
const double incomingX=static_cast<double>(terminal[0]-beforeTerminal[0]);
|
||||
const double incomingY=static_cast<double>(terminal[1]-beforeTerminal[1]);
|
||||
const double outgoingX=static_cast<double>(lowerMouth[0]-terminal[0]);
|
||||
const double outgoingY=static_cast<double>(lowerMouth[1]-terminal[1]);
|
||||
const double incomingLength=std::hypot(incomingX,incomingY);
|
||||
const double outgoingLength=std::hypot(outgoingX,outgoingY);
|
||||
if(incomingLength<=1.0e-6||outgoingLength<=1.0e-6)continue;
|
||||
const double alignment=(incomingX*outgoingX+incomingY*outgoingY)
|
||||
/(incomingLength*outgoingLength);
|
||||
const double separation=std::hypot(
|
||||
static_cast<double>(terminal[0]-firstRing[0]),
|
||||
static_cast<double>(terminal[1]-firstRing[1]));
|
||||
const double score=alignment+0.20*std::clamp(
|
||||
separation/(2.0*qPerMetre),0.0,2.0);
|
||||
if(score>bestExitScore)
|
||||
{bestExitScore=score;lowerPhase=exitCandidate;}
|
||||
}
|
||||
for(int step=1;step<=16;++step)
|
||||
{
|
||||
int directionIndex=static_cast<int>(candidate)
|
||||
+candidateDirection*step;
|
||||
directionIndex=(directionIndex%16+16)%16;
|
||||
append(ringPoint(static_cast<std::uint32_t>(directionIndex),loops));
|
||||
if(static_cast<std::uint32_t>(directionIndex)==lowerPhase)break;
|
||||
}
|
||||
}
|
||||
return measure();
|
||||
};
|
||||
for(std::uint32_t offset=0;offset<16;++offset)
|
||||
{
|
||||
const std::uint32_t candidate=static_cast<std::uint32_t>(
|
||||
(hashedPhase+16-offset)%16);
|
||||
const auto point=ringPoint(candidate);
|
||||
const double entryX=static_cast<double>(point[0]-upperMouth[0]);
|
||||
const double entryY=static_cast<double>(point[1]-upperMouth[1]);
|
||||
const double entryLength=std::hypot(entryX,entryY);
|
||||
if(entryLength<=1.0e-6)continue;
|
||||
const auto entryDistance=distanceQ(point[0]-upperMouth[0],
|
||||
point[1]-upperMouth[1]);
|
||||
for(const int candidateDirection:{hashedDirection,-hashedDirection})
|
||||
{
|
||||
const std::uint64_t routeLength=candidateRouteLength(candidate,
|
||||
candidateDirection);
|
||||
if(routeLength==0)continue;
|
||||
const std::uint32_t next=static_cast<std::uint32_t>(
|
||||
(static_cast<int>(candidate)+candidateDirection+16)%16);
|
||||
const auto nextPoint=ringPoint(next);
|
||||
const double tangentX=static_cast<double>(nextPoint[0]-point[0]);
|
||||
const double tangentY=static_cast<double>(nextPoint[1]-point[1]);
|
||||
const double tangentLength=std::hypot(tangentX,tangentY);
|
||||
if(tangentLength<=1.0e-6)continue;
|
||||
const double tangentAlignment=(entryX*tangentX+entryY*tangentY)
|
||||
/(entryLength*tangentLength);
|
||||
const std::int64_t firstAxisZ=upperMouth[2]+mouthRadius
|
||||
-playerCenterAboveFloorQ;
|
||||
const std::int64_t secondAxisZ=firstAxisZ+PassageRoundRatio(
|
||||
lowerMouth[2]-upperMouth[2],static_cast<std::int64_t>(entryDistance),
|
||||
static_cast<std::int64_t>(routeLength));
|
||||
double candidateTreadClearance=std::numeric_limits<double>::infinity();
|
||||
const int entrySamples=std::max(1,static_cast<int>(std::ceil(
|
||||
entryLength/(0.25*qPerMetre))));
|
||||
for(int sample=0;sample<=entrySamples;++sample)
|
||||
{
|
||||
const double t=static_cast<double>(sample)/entrySamples;
|
||||
const double qx=upperMouth[0]+t*entryX;
|
||||
const double qy=upperMouth[1]+t*entryY;
|
||||
const double qz=firstAxisZ+t*(secondAxisZ-firstAxisZ)
|
||||
-mouthRadius+0.20*qPerMetre;
|
||||
for(const auto& previousPassage:layout.passages)
|
||||
{
|
||||
if(previousPassage.stableId==passage.stableId
|
||||
||previousPassage.controlPoints.size()<2)continue;
|
||||
for(std::size_t previousSegment=0;
|
||||
previousSegment+1<previousPassage.controlPoints.size();
|
||||
++previousSegment)
|
||||
{
|
||||
const auto& previousA=previousPassage.controlPoints[previousSegment];
|
||||
const auto& previousB=previousPassage.controlPoints[previousSegment+1];
|
||||
const double ax=static_cast<double>(previousA.positionSubvoxels[0]);
|
||||
const double ay=static_cast<double>(previousA.positionSubvoxels[1]);
|
||||
const double az=static_cast<double>(previousA.positionSubvoxels[2]);
|
||||
const double dx=static_cast<double>(previousB.positionSubvoxels[0])-ax;
|
||||
const double dy=static_cast<double>(previousB.positionSubvoxels[1])-ay;
|
||||
const double dz=static_cast<double>(previousB.positionSubvoxels[2])-az;
|
||||
const double horizontalLengthSquared=dx*dx+dy*dy;
|
||||
const double projection=horizontalLengthSquared>0.0
|
||||
?std::clamp(((qx-ax)*dx+(qy-ay)*dy)
|
||||
/horizontalLengthSquared,0.0,1.0):0.0;
|
||||
const double centerX=ax+projection*dx;
|
||||
const double centerY=ay+projection*dy;
|
||||
const double centerZ=az+projection*dz
|
||||
-(previousA.radiusSubvoxels+projection*(
|
||||
static_cast<double>(previousB.radiusSubvoxels)
|
||||
-previousA.radiusSubvoxels))
|
||||
+1.13*qPerMetre;
|
||||
const double vertical=std::max(0.0,
|
||||
std::abs(qz-centerZ)-0.54*qPerMetre);
|
||||
const double clearance=std::sqrt(
|
||||
(qx-centerX)*(qx-centerX)+(qy-centerY)*(qy-centerY)
|
||||
+vertical*vertical)-0.34*qPerMetre;
|
||||
candidateTreadClearance=std::min(candidateTreadClearance,
|
||||
clearance);
|
||||
}
|
||||
}
|
||||
}
|
||||
double previousTreadClearance=std::numeric_limits<double>::infinity();
|
||||
const double entryXQ=entryX,entryYQ=entryY;
|
||||
const double entryZQ=static_cast<double>(secondAxisZ-firstAxisZ);
|
||||
const double entryLengthSquared=entryXQ*entryXQ+entryYQ*entryYQ
|
||||
+entryZQ*entryZQ;
|
||||
const double mouthStartT=std::clamp(
|
||||
mouthRadius/entryLength,0.0,1.0);
|
||||
for(const auto& previousPassage:layout.passages)
|
||||
{
|
||||
if(previousPassage.stableId==passage.stableId
|
||||
||previousPassage.controlPoints.size()<2)continue;
|
||||
for(std::size_t previousSegment=0;
|
||||
previousSegment+1<previousPassage.controlPoints.size();
|
||||
++previousSegment)
|
||||
{
|
||||
const auto& previousA=previousPassage.controlPoints[previousSegment];
|
||||
const auto& previousB=previousPassage.controlPoints[previousSegment+1];
|
||||
const double ax=static_cast<double>(previousA.positionSubvoxels[0]);
|
||||
const double ay=static_cast<double>(previousA.positionSubvoxels[1]);
|
||||
const double az=static_cast<double>(previousA.positionSubvoxels[2]);
|
||||
const double dx=static_cast<double>(previousB.positionSubvoxels[0])-ax;
|
||||
const double dy=static_cast<double>(previousB.positionSubvoxels[1])-ay;
|
||||
const double dz=static_cast<double>(previousB.positionSubvoxels[2])-az;
|
||||
for(int sample=0;sample<=8;++sample)
|
||||
{
|
||||
const double t=static_cast<double>(sample)/8.0;
|
||||
const double px=ax+t*dx;
|
||||
const double py=ay+t*dy;
|
||||
const double pz=az+t*dz
|
||||
-(previousA.radiusSubvoxels+t*(
|
||||
static_cast<double>(previousB.radiusSubvoxels)
|
||||
-previousA.radiusSubvoxels))
|
||||
+0.20*qPerMetre;
|
||||
const double projection=entryLengthSquared>0.0
|
||||
?std::clamp(((px-upperMouth[0])*entryXQ
|
||||
+(py-upperMouth[1])*entryYQ
|
||||
+(pz-firstAxisZ)*entryZQ)/entryLengthSquared,
|
||||
mouthStartT,1.0):mouthStartT;
|
||||
const double ex=px-(upperMouth[0]+projection*entryXQ);
|
||||
const double ey=py-(upperMouth[1]+projection*entryYQ);
|
||||
const double ez=pz-(firstAxisZ+projection*entryZQ);
|
||||
const double clearance=std::sqrt(ex*ex+ey*ey+ez*ez)
|
||||
-mouthRadius;
|
||||
previousTreadClearance=std::min(previousTreadClearance,
|
||||
clearance);
|
||||
}
|
||||
}
|
||||
}
|
||||
const double clearanceScore=std::isfinite(candidateTreadClearance)
|
||||
&&std::isfinite(previousTreadClearance)
|
||||
?std::min(candidateTreadClearance,previousTreadClearance)/qPerMetre
|
||||
:0.0;
|
||||
if(phaseScoreCount<phaseScores.size())
|
||||
phaseScores[phaseScoreCount++]={candidate,candidateDirection,
|
||||
routeLength,clearanceScore,tangentAlignment};
|
||||
}
|
||||
}
|
||||
if(phaseScoreCount>0)
|
||||
{
|
||||
double bestClearance=-std::numeric_limits<double>::infinity();
|
||||
for(std::size_t index=0;index<phaseScoreCount;++index)
|
||||
bestClearance=std::max(bestClearance,phaseScores[index].clearance);
|
||||
constexpr double ClearanceToleranceMetres=0.10;
|
||||
const FPhaseScore* selected=nullptr;
|
||||
for(std::size_t index=0;index<phaseScoreCount;++index)
|
||||
{
|
||||
const auto& candidate=phaseScores[index];
|
||||
if(candidate.clearance+ClearanceToleranceMetres<bestClearance)continue;
|
||||
const bool shorter=!selected||candidate.routeLength<selected->routeLength;
|
||||
const bool stableLengthTie=selected
|
||||
&&candidate.routeLength==selected->routeLength
|
||||
&&(candidate.clearance>selected->clearance+1.0e-6
|
||||
||(std::abs(candidate.clearance-selected->clearance)<=1.0e-6
|
||||
&&candidate.tangent>selected->tangent));
|
||||
if(shorter||stableLengthTie)selected=&candidate;
|
||||
}
|
||||
if(selected)
|
||||
{
|
||||
phase=selected->phase;
|
||||
direction=selected->direction;
|
||||
clockwise=direction>0;
|
||||
}
|
||||
}
|
||||
}
|
||||
direction=clockwise?1:-1;
|
||||
std::vector<std::array<std::int64_t,2>> xy;
|
||||
const auto pushXY=[&](std::array<std::int64_t,2> p)
|
||||
{if(xy.empty()||xy.back()!=p)xy.push_back(p);};
|
||||
@@ -2731,6 +3004,12 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
}
|
||||
++loops;
|
||||
}
|
||||
const std::int64_t mouthPlanSeparation=distanceQ(
|
||||
lowerMouth[0]-upperMouth[0],lowerMouth[1]-upperMouth[1]);
|
||||
const bool shortGeneratedClosure=!hasLowerTangent&&!hasAnchorHelix
|
||||
&&pathStyle==1u&&loops<=1
|
||||
&&mouthPlanSeparation<=2*mouthRadius
|
||||
&&verticalDistance<=2*mouthRadius+qPerMetre/2;
|
||||
if(hasLowerTangent)
|
||||
{
|
||||
std::uint32_t lowerPhase=static_cast<std::uint32_t>(PassageSeed(seed,
|
||||
@@ -2789,6 +3068,48 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
}
|
||||
pushXY(ringPoint(lowerPhase));
|
||||
}
|
||||
else if(shortGeneratedClosure)
|
||||
{
|
||||
// A one-turn generated route whose mouths share a plan location
|
||||
// must leave and rejoin from different ring tangents. Returning on
|
||||
// the entry chord creates a hairpin that overlaps its own tread.
|
||||
double bestExitScore=-std::numeric_limits<double>::infinity();
|
||||
std::uint32_t lowerPhase=phase;
|
||||
const auto firstRing=ringPoint(phase,loops);
|
||||
for(std::uint32_t candidate=0;candidate<16;++candidate)
|
||||
{
|
||||
if(candidate==phase)continue;
|
||||
const std::uint32_t previous=static_cast<std::uint32_t>(
|
||||
(static_cast<int>(candidate)-direction+16)%16);
|
||||
const auto terminal=ringPoint(candidate,loops);
|
||||
const auto beforeTerminal=ringPoint(previous,loops);
|
||||
const double incomingX=static_cast<double>(terminal[0]-beforeTerminal[0]);
|
||||
const double incomingY=static_cast<double>(terminal[1]-beforeTerminal[1]);
|
||||
const double outgoingX=static_cast<double>(lowerMouth[0]-terminal[0]);
|
||||
const double outgoingY=static_cast<double>(lowerMouth[1]-terminal[1]);
|
||||
const double incomingLength=std::hypot(incomingX,incomingY);
|
||||
const double outgoingLength=std::hypot(outgoingX,outgoingY);
|
||||
if(incomingLength<=1.0e-6||outgoingLength<=1.0e-6)continue;
|
||||
const double tangentAlignment=(incomingX*outgoingX+incomingY*outgoingY)
|
||||
/(incomingLength*outgoingLength);
|
||||
const double separation=std::hypot(
|
||||
static_cast<double>(terminal[0]-firstRing[0]),
|
||||
static_cast<double>(terminal[1]-firstRing[1]));
|
||||
const double score=tangentAlignment
|
||||
+0.20*std::clamp(separation/(2.0*qPerMetre),0.0,2.0);
|
||||
if(score>bestExitScore)
|
||||
{bestExitScore=score;lowerPhase=candidate;}
|
||||
}
|
||||
for(int step=1;step<=16;++step)
|
||||
{
|
||||
int directionIndex=static_cast<int>(phase)+direction*step;
|
||||
directionIndex=(directionIndex%16+16)%16;
|
||||
pushXY(ringPoint(static_cast<std::uint32_t>(directionIndex),loops));
|
||||
if(static_cast<std::uint32_t>(directionIndex)==lowerPhase)break;
|
||||
}
|
||||
std::printf("[ResolvedPassageGeneratedExitApproach] id=%s turns=%u phase=%u exit_phase=%u tangent_score=%.3f status=PASS\n",
|
||||
passage.stableId.c_str(),loops,phase,lowerPhase,bestExitScore);
|
||||
}
|
||||
pushXY({lowerMouth[0],lowerMouth[1]});totalHorizontal=measurePath();
|
||||
std::size_t lowerMouthIndex=xy.size()-1;
|
||||
std::vector<std::array<std::int64_t,3>> lowerLandingPoints;
|
||||
|
||||
@@ -971,6 +971,7 @@ public:
|
||||
const double floorThicknessVoxels=qPerMetre/Q;
|
||||
const double playerCenterAboveFloorQ=1.13*qPerMetre;
|
||||
float composed=density;
|
||||
float deferredSupportDensity=std::numeric_limits<float>::infinity();
|
||||
const auto composeRouteMouthSupport=[&](std::int32_t strateIndex,
|
||||
const std::array<std::int64_t,3>& mouth,bool departure)
|
||||
{
|
||||
@@ -1045,7 +1046,6 @@ public:
|
||||
{
|
||||
const auto& passage=ResolvedLayout->passages[passageIndex];
|
||||
if(passage.controlPoints.size()<2)continue;
|
||||
const bool internalChainPassage=passageIndex>0;
|
||||
const auto supportStratum=[&](std::int32_t strateIndex)
|
||||
->const VoxelForgeCore::ResolvedLayoutStratumV2*
|
||||
{
|
||||
@@ -1056,6 +1056,31 @@ public:
|
||||
};
|
||||
const auto* supportUpper=supportStratum(passage.upperStrateIndex);
|
||||
const auto* supportLower=supportStratum(passage.lowerStrateIndex);
|
||||
const auto mouthInsideContent=[&](
|
||||
const VoxelForgeCore::ResolvedLayoutStratumV2* stratum,
|
||||
const std::array<std::int64_t,3>& mouth)
|
||||
{
|
||||
if(!stratum||!stratum->content.presentInCurrentField)return false;
|
||||
const auto zMicrometres=static_cast<std::int64_t>(std::llround(
|
||||
static_cast<double>(mouth[2])
|
||||
*ResolvedLayout->coreVoxelSizeMicrometres/Q));
|
||||
return zMicrometres>=stratum->content.bottomMicrometres
|
||||
&&zMicrometres<stratum->content.topMicrometres;
|
||||
};
|
||||
// A passage is internal when both mouths are bounded by their strate
|
||||
// content and neither endpoint is a SurfaceWorld landing. Route order
|
||||
// and the total number of strata are irrelevant.
|
||||
const bool internalChainPassage=supportUpper&&supportLower
|
||||
&&!supportUpper->passageCapabilities.hasSurfaceLanding
|
||||
&&!supportLower->passageCapabilities.hasSurfaceLanding
|
||||
&&mouthInsideContent(supportUpper,passage.upperMouthSubvoxels)
|
||||
&&mouthInsideContent(supportLower,passage.lowerMouthSubvoxels);
|
||||
const auto laterPassageMayCarve=[&](std::ptrdiff_t supportSegment,
|
||||
double sampleX,double sampleY,double sampleZ)
|
||||
{
|
||||
return PassageSupportMayBeCarvedByLaterBore(passageIndex,
|
||||
supportSegment,sampleX,sampleY,sampleZ,0.35*qPerMetre);
|
||||
};
|
||||
const bool floatingIslandPassage=supportUpper
|
||||
&&supportUpper->passageCapabilities.hasFloatingIslandLanding;
|
||||
const bool surfaceToTunnelSupport=supportUpper&&supportLower
|
||||
@@ -1223,7 +1248,8 @@ public:
|
||||
+playerVertical*playerVertical)-0.34*qPerMetre);
|
||||
}
|
||||
double cx=ax+t*dx,cy=ay+t*dy,cz=az+t*dz;
|
||||
const bool smoothCarveJoin=index>2&&index+12<lastIndex
|
||||
const bool smoothCarveJoin=index>2
|
||||
&&index+(internalChainPassage?2:12)<lowerMouthIndex
|
||||
&&DeterministicSqrt(lengthSq)<=8.0*qPerMetre
|
||||
&&!IsPassageShortSpanSegment(passageIndex,segmentIndex-1)
|
||||
&&!IsPassageShortSpanSegment(passageIndex,segmentIndex)
|
||||
@@ -1301,8 +1327,9 @@ public:
|
||||
?std::clamp(-(qz-(nearestA.positionSubvoxels[2]+bestT*(
|
||||
static_cast<double>(nearestB.positionSubvoxels[2])-nearestA.positionSubvoxels[2])))
|
||||
/distanceToCenter,0.0,1.0):0.0;
|
||||
const float relief=VF_FieldPassageWallRelief(static_cast<float>(X),static_cast<float>(Y),
|
||||
static_cast<float>(Z),static_cast<std::uint32_t>(Owner?Owner->Seed:0));
|
||||
const float relief=VF_FieldPassageWallRelief(static_cast<float>(X),
|
||||
static_cast<float>(Y),static_cast<float>(Z),
|
||||
static_cast<std::uint32_t>(Owner?Owner->Seed:0));
|
||||
const float mouthSurfaceRelief=VF_FieldSurfaceRelief(static_cast<float>(X)*1.5f,
|
||||
static_cast<float>(Y)*1.5f,static_cast<float>(Z)*1.5f,
|
||||
static_cast<std::uint32_t>(Owner?Owner->Seed:0)^0x4D4F5554u);
|
||||
@@ -1427,7 +1454,8 @@ public:
|
||||
static_cast<double>(mouthTerrainRelief),-1.0,1.0)
|
||||
*0.20*qPerMetre*mouthFloorBlend*floorReliefWeight
|
||||
*(upperMouthFloorFade?1.0:0.0);
|
||||
const double wallReliefWeight=0.05+0.95*(1.0-verticalWallAlignment);
|
||||
const double wallOpening=1.0-verticalWallAlignment;
|
||||
const double wallReliefWeight=0.05+0.95*wallOpening;
|
||||
const double roughSignedDistance=bestSignedDistance
|
||||
+static_cast<double>(std::clamp(relief,-1.0f,1.0f))*0.21*qPerMetre*wallReliefWeight
|
||||
+floorReliefOffset;
|
||||
@@ -1472,6 +1500,7 @@ public:
|
||||
static_cast<float>(-roughSignedDistance/Q));
|
||||
composed=std::lerp(composed,fullyCarvedDensity,
|
||||
static_cast<float>(floorCarveWeight));
|
||||
const float composedAfterCarve=composed;
|
||||
const auto considerSupportSegment=[&](std::ptrdiff_t index)
|
||||
{
|
||||
const auto& segmentA=passage.controlPoints[static_cast<std::size_t>(index)];
|
||||
@@ -1570,15 +1599,25 @@ public:
|
||||
const double minimumSupportQueryDepth=index==selectedSupportSegmentIndex
|
||||
&&sparseRouteSupport?0.65:0.93;
|
||||
if(supportQueryDepth<minimumSupportQueryDepth*qPerMetre
|
||||
||supportQueryDepth>1.30*qPerMetre)return;
|
||||
||supportQueryDepth>1.30*qPerMetre)
|
||||
return;
|
||||
const bool fallbackSupport=qz>=supportPlayerCenterZ-1.50*qPerMetre;
|
||||
const double supportThicknessVoxels=fallbackSupport
|
||||
?qPerMetre/Q:0.5*qPerMetre/Q;
|
||||
// Keep the supported ramp to a comfortable walking width. Extending a
|
||||
// full bore-radius shelf across overlapping helix coils can block the
|
||||
// capsule on the neighbouring turn.
|
||||
const bool lowerMouthBodyOverlap=ResolvedLayout->strates.size()>2
|
||||
&&index>=lowerMouthIndex-2
|
||||
const double lowerMouthDx=centerX
|
||||
-static_cast<double>(passage.lowerMouthSubvoxels[0]);
|
||||
const double lowerMouthDy=centerY
|
||||
-static_cast<double>(passage.lowerMouthSubvoxels[1]);
|
||||
const double lowerMouthDz=centerZ-radius
|
||||
-static_cast<double>(passage.lowerMouthSubvoxels[2]);
|
||||
const double lowerMouthBodyDistance=DeterministicSqrt(
|
||||
lowerMouthDx*lowerMouthDx+lowerMouthDy*lowerMouthDy
|
||||
+lowerMouthDz*lowerMouthDz);
|
||||
const bool lowerMouthBodyOverlap=internalChainPassage
|
||||
&&lowerMouthBodyDistance<=2.0*qPerMetre
|
||||
&&supportQueryDepth<0.98*qPerMetre;
|
||||
// Keep a narrow walk core, but place its top on the native surface
|
||||
// so the support remains a worn ramp instead of a route-height slab.
|
||||
@@ -1731,7 +1770,8 @@ public:
|
||||
bestSupportUsesSignedDistance=signedDistanceSupport;
|
||||
bestSupportMouthBlend=mouthSupportBlend;
|
||||
bestSupportNativeBlend=mouthTread
|
||||
?0.85*mouthSupportBlend*(1.0-supportEdgeSmooth):0.0;
|
||||
?(internalChainPassage?1.0:0.85)
|
||||
*mouthSupportBlend*(1.0-supportEdgeSmooth):0.0;
|
||||
};
|
||||
std::ptrdiff_t supportCenterIndex=bestIndex;
|
||||
if(bestIndex<lowerMouthIndex)
|
||||
@@ -1775,6 +1815,8 @@ public:
|
||||
lowerMouthIndex,mouthSearchBegin,mouthSearchEnd,lastIndex,qx,qy,
|
||||
capsuleQueryZ,qPerMetre,capsuleClearanceMetres)
|
||||
:bestPlayerCapsuleSignedDistance>=capsuleClearanceMetres*qPerMetre;
|
||||
bool supportApplied=false;
|
||||
bool supportNeedsDeferral=false;
|
||||
// Outside bridge and short-span segments, add the tread only where the
|
||||
// native field already provides a solid envelope; the capsule check keeps
|
||||
// that support below the player's walk space.
|
||||
@@ -1782,6 +1824,8 @@ public:
|
||||
||shortSpanFloorSupport)
|
||||
&&(capsuleAllowsSupport||bestSupportDensity>=0.0))
|
||||
{
|
||||
supportNeedsDeferral=laterPassageMayCarve(bestSupportSegmentIndex,
|
||||
qx,qy,qz);
|
||||
if(bestSupportIsMouthTread)
|
||||
{
|
||||
// Preserve the owner's native ground through the shallow mouth
|
||||
@@ -1797,13 +1841,36 @@ public:
|
||||
const double nativeSurfaceDetail=std::clamp(
|
||||
static_cast<double>(mouthTerrainRelief),-1.0,1.0)
|
||||
*0.18*qPerMetre/Q;
|
||||
const float nativeGroundWithRelief=static_cast<float>(density
|
||||
-nativeSurfaceDetail);
|
||||
const float nativeGroundWithRelief=static_cast<float>(
|
||||
(supportNeedsDeferral?std::max(-1.0f,density):density)
|
||||
-nativeSurfaceDetail);
|
||||
composed=std::lerp(composed,nativeGroundWithRelief,
|
||||
static_cast<float>(bestSupportNativeBlend));
|
||||
supportApplied=composed<composedAfterCarve;
|
||||
}
|
||||
}
|
||||
else composed=std::min(composed,static_cast<float>(bestSupportDensity));
|
||||
else
|
||||
{
|
||||
const float densityBeforeSupport=composed;
|
||||
composed=std::min(composed,static_cast<float>(bestSupportDensity));
|
||||
supportApplied=composed<densityBeforeSupport;
|
||||
}
|
||||
}
|
||||
if(supportApplied)
|
||||
{
|
||||
if(PassageOtherCapsuleBlocksSupport(passageIndex,
|
||||
bestSupportSegmentIndex,qx,qy,qz,qPerMetre,capsuleClearanceMetres))
|
||||
{
|
||||
composed=composedAfterCarve;
|
||||
supportApplied=false;
|
||||
}
|
||||
}
|
||||
if(supportApplied&&supportNeedsDeferral)
|
||||
{
|
||||
deferredSupportDensity=std::min(deferredSupportDensity,composed);
|
||||
// Preserve a tread only when a later passage bore reaches this
|
||||
// sample; otherwise retain the ordinary composition order.
|
||||
composed=composedAfterCarve;
|
||||
}
|
||||
}
|
||||
// Restore the surface route where the passage cut crossed otherwise solid
|
||||
@@ -1818,6 +1885,7 @@ public:
|
||||
composeRouteMouthSupport(passage.upperStrateIndex,
|
||||
passage.upperMouthSubvoxels,true);
|
||||
}
|
||||
composed=std::min(composed,deferredSupportDensity);
|
||||
return composed;
|
||||
}
|
||||
|
||||
@@ -3282,6 +3350,17 @@ private:
|
||||
{
|
||||
std::array<double,3> minimum{},maximum{};
|
||||
};
|
||||
struct FPassageLaterBoreCandidate
|
||||
{
|
||||
std::array<double,3> minimum{},maximum{};
|
||||
double reach=0.0;
|
||||
};
|
||||
struct FPassageLaterBoreSource
|
||||
{
|
||||
std::array<double,3> minimum{},maximum{};
|
||||
std::vector<FPassageLaterBoreCandidate> candidates;
|
||||
std::vector<std::size_t> capsuleCandidatePassages;
|
||||
};
|
||||
struct FPassageProofNode
|
||||
{
|
||||
std::array<double,3> minimum{},maximum{};
|
||||
@@ -4237,6 +4316,7 @@ private:
|
||||
PassageNearestNodes.clear();
|
||||
PassageProofBounds.clear();PassageProofNodes.clear();
|
||||
PassageNearestNodes.resize(ResolvedLayout?ResolvedLayout->passages.size():0);
|
||||
PassageLaterBoreSources.clear();
|
||||
PassageColumnSegmentFlags.clear();PassageShortSpanSegmentFlags.clear();
|
||||
PassageHasShortSpanFlags.clear();
|
||||
if(!ResolvedLayout||ResolvedLayout->passages.empty())return;
|
||||
@@ -4336,6 +4416,75 @@ private:
|
||||
}
|
||||
}
|
||||
}
|
||||
// Precompute the later-bore leaves that can intersect each tread segment.
|
||||
// Passage support samples normally stay within this source envelope, so the
|
||||
// hot query can test a short list of physical bounds instead of walking a
|
||||
// full spatial tree for every density sample.
|
||||
PassageLaterBoreSources.resize(ResolvedLayout->passages.size());
|
||||
const double qPerMetre=Q*voxelsPerMetre;
|
||||
for(std::size_t sourcePassageIndex=0;
|
||||
sourcePassageIndex<ResolvedLayout->passages.size();++sourcePassageIndex)
|
||||
{
|
||||
const auto& sourcePassage=ResolvedLayout->passages[sourcePassageIndex];
|
||||
const std::size_t sourceSegmentCount=sourcePassage.controlPoints.size()>1
|
||||
?sourcePassage.controlPoints.size()-1:0;
|
||||
auto& sourceSegments=PassageLaterBoreSources[sourcePassageIndex];
|
||||
sourceSegments.resize(sourceSegmentCount);
|
||||
for(std::size_t sourceSegment=0;sourceSegment<sourceSegmentCount;++sourceSegment)
|
||||
{
|
||||
const auto& sourceA=sourcePassage.controlPoints[sourceSegment];
|
||||
const auto& sourceB=sourcePassage.controlPoints[sourceSegment+1];
|
||||
const double sourceRadius=std::max(sourceA.radiusSubvoxels,
|
||||
sourceB.radiusSubvoxels);
|
||||
auto& source=sourceSegments[sourceSegment];
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
source.minimum[axis]=std::min(sourceA.positionSubvoxels[axis],
|
||||
sourceB.positionSubvoxels[axis])-sourceRadius-3.0*qPerMetre;
|
||||
source.maximum[axis]=std::max(sourceA.positionSubvoxels[axis],
|
||||
sourceB.positionSubvoxels[axis])+sourceRadius+3.0*qPerMetre;
|
||||
}
|
||||
for(std::size_t laterPassageIndex=sourcePassageIndex+1;
|
||||
laterPassageIndex<ResolvedLayout->passages.size();++laterPassageIndex)
|
||||
{
|
||||
for(const auto& node:PassageNearestNodes[laterPassageIndex])
|
||||
{
|
||||
if(node.left>=0)continue;
|
||||
const double reach=node.maximumRadius+0.35*qPerMetre+1.0e-6;
|
||||
bool intersectsSource=true;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
intersectsSource=intersectsSource
|
||||
&&node.minimum[axis]-reach<=source.maximum[axis]
|
||||
&&node.maximum[axis]+reach>=source.minimum[axis];
|
||||
if(!intersectsSource)continue;
|
||||
source.candidates.push_back({node.minimum,node.maximum,reach});
|
||||
}
|
||||
}
|
||||
const double capsuleXYReach=0.39*qPerMetre;
|
||||
for(std::size_t otherPassageIndex=0;
|
||||
otherPassageIndex<ResolvedLayout->passages.size();++otherPassageIndex)
|
||||
{
|
||||
if(otherPassageIndex==sourcePassageIndex
|
||||
||PassageNearestNodes[otherPassageIndex].empty())continue;
|
||||
const auto& otherRoot=PassageNearestNodes[otherPassageIndex].front();
|
||||
const std::array<double,3> blockerMinimum={
|
||||
otherRoot.minimum[0]-capsuleXYReach,
|
||||
otherRoot.minimum[1]-capsuleXYReach,
|
||||
otherRoot.minimum[2]-otherRoot.maximumRadius+0.20*qPerMetre};
|
||||
const std::array<double,3> blockerMaximum={
|
||||
otherRoot.maximum[0]+capsuleXYReach,
|
||||
otherRoot.maximum[1]+capsuleXYReach,
|
||||
otherRoot.maximum[2]+2.06*qPerMetre};
|
||||
bool intersectsSource=true;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
intersectsSource=intersectsSource
|
||||
&&blockerMinimum[axis]<=source.maximum[axis]
|
||||
&&blockerMaximum[axis]>=source.minimum[axis];
|
||||
if(intersectsSource)
|
||||
source.capsuleCandidatePassages.push_back(otherPassageIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
for(const auto& crossing:PassageVoidCrossings)
|
||||
{
|
||||
if(crossing.passageIndex<PassageColumnSegmentFlags.size())
|
||||
@@ -4473,6 +4622,88 @@ private:
|
||||
signedDistanceLimit);
|
||||
}
|
||||
|
||||
bool PassageSupportMayBeCarvedByLaterBore(std::size_t passageIndex,
|
||||
std::ptrdiff_t supportSegment,double qx,double qy,double qz,
|
||||
double signedDistanceLimit) const
|
||||
{
|
||||
if(!ResolvedLayout||passageIndex+1>=ResolvedLayout->passages.size())return false;
|
||||
if(passageIndex<PassageLaterBoreSources.size()&&supportSegment>=0
|
||||
&&static_cast<std::size_t>(supportSegment)
|
||||
<PassageLaterBoreSources[passageIndex].size())
|
||||
{
|
||||
const auto& source=PassageLaterBoreSources[passageIndex][
|
||||
static_cast<std::size_t>(supportSegment)];
|
||||
const bool insideSourceEnvelope=qx>=source.minimum[0]&&qx<=source.maximum[0]
|
||||
&&qy>=source.minimum[1]&&qy<=source.maximum[1]
|
||||
&&qz>=source.minimum[2]&&qz<=source.maximum[2];
|
||||
if(insideSourceEnvelope)
|
||||
{
|
||||
for(const auto& candidate:source.candidates)
|
||||
{
|
||||
double distanceSquared=0.0;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
const double q=axis==0?qx:axis==1?qy:qz;
|
||||
const double distance=q<candidate.minimum[axis]
|
||||
?candidate.minimum[axis]-q
|
||||
:q>candidate.maximum[axis]?q-candidate.maximum[axis]:0.0;
|
||||
distanceSquared+=distance*distance;
|
||||
}
|
||||
const double reach=candidate.reach;
|
||||
if(distanceSquared<=reach*reach)return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for(std::size_t otherPassageIndex=passageIndex+1;
|
||||
otherPassageIndex<ResolvedLayout->passages.size();++otherPassageIndex)
|
||||
{
|
||||
const auto& otherPassage=ResolvedLayout->passages[otherPassageIndex];
|
||||
if(otherPassage.controlPoints.size()<2)continue;
|
||||
if(PassagePotentialWithinNode(otherPassageIndex,0,0,
|
||||
otherPassage.controlPoints.size()-1,qx,qy,qz,signedDistanceLimit))return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool PassageOtherCapsuleBlocksSupport(std::size_t passageIndex,
|
||||
std::ptrdiff_t supportSegment,double qx,double qy,double qz,
|
||||
double qPerMetre,double minimumClearanceMetres) const
|
||||
{
|
||||
if(passageIndex<PassageLaterBoreSources.size()&&supportSegment>=0
|
||||
&&static_cast<std::size_t>(supportSegment)
|
||||
<PassageLaterBoreSources[passageIndex].size())
|
||||
{
|
||||
const auto& source=PassageLaterBoreSources[passageIndex][
|
||||
static_cast<std::size_t>(supportSegment)];
|
||||
const bool insideSourceEnvelope=qx>=source.minimum[0]&&qx<=source.maximum[0]
|
||||
&&qy>=source.minimum[1]&&qy<=source.maximum[1]
|
||||
&&qz>=source.minimum[2]&&qz<=source.maximum[2];
|
||||
if(insideSourceEnvelope)
|
||||
{
|
||||
for(const std::size_t otherPassageIndex:source.capsuleCandidatePassages)
|
||||
{
|
||||
const auto& otherPassage=ResolvedLayout->passages[otherPassageIndex];
|
||||
if(PassagePlayerCapsuleBlocksSupportNode(otherPassageIndex,0,0,
|
||||
otherPassage.controlPoints.size()-1,qx,qy,qz,qPerMetre,
|
||||
minimumClearanceMetres))return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for(std::size_t otherPassageIndex=0;
|
||||
otherPassageIndex<ResolvedLayout->passages.size();++otherPassageIndex)
|
||||
{
|
||||
if(otherPassageIndex==passageIndex)continue;
|
||||
const auto& otherPassage=ResolvedLayout->passages[otherPassageIndex];
|
||||
if(otherPassage.controlPoints.size()<2)continue;
|
||||
if(PassagePlayerCapsuleBlocksSupportNode(otherPassageIndex,0,0,
|
||||
otherPassage.controlPoints.size()-1,qx,qy,qz,qPerMetre,
|
||||
minimumClearanceMetres))return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool PassageMayAffectDensity(std::size_t passageIndex,std::ptrdiff_t carveBegin,
|
||||
std::ptrdiff_t carveEnd,std::ptrdiff_t lowerMouthIndex,
|
||||
std::ptrdiff_t mouthSearchBegin,std::ptrdiff_t mouthSearchEnd,std::ptrdiff_t lastIndex,
|
||||
@@ -4529,7 +4760,10 @@ private:
|
||||
const double vertical=std::max(0.0,std::abs(qz-centerZ)-0.54*qPerMetre);
|
||||
const double distance=DeterministicSqrt(horizontalX*horizontalX+horizontalY*horizontalY
|
||||
+vertical*vertical)-0.34*qPerMetre;
|
||||
if(distance<minimumClearanceMetres*qPerMetre)return true;
|
||||
if(distance<minimumClearanceMetres*qPerMetre)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -4749,8 +4983,12 @@ private:
|
||||
const double irregularity=1.25*qPerMetre
|
||||
+std::clamp(static_cast<double>(broadRelief),-1.0,1.0)*1.10*qPerMetre
|
||||
+std::clamp(static_cast<double>(surfaceRelief),-1.0,1.0)*0.35*qPerMetre;
|
||||
// Add a subtle host-rock root only when the unmodified column lacks the
|
||||
// vertical relief required for walkable passage support.
|
||||
const double rootTaper=crossing.needsRootTaper
|
||||
?0.08*qPerMetre*smoothStep((t-0.35)/0.50):0.0;
|
||||
const double radius=static_cast<double>(crossing.minimumRadiusSubvoxels)
|
||||
+std::max(0.15*qPerMetre,irregularity)+baseFlare+ceilingFlare;
|
||||
+std::max(0.15*qPerMetre,irregularity)+rootTaper+baseFlare+ceilingFlare;
|
||||
const double radialDistanceField=radialDistance-radius;
|
||||
const double topDistance=qz-crossing.topZSubvoxels;
|
||||
const double bottomDistance=crossing.bottomZSubvoxels-qz;
|
||||
@@ -4780,6 +5018,67 @@ private:
|
||||
return ApplyResolvedStrateBoundaryBand(density,X,Y,Z);
|
||||
}
|
||||
|
||||
double PassageColumnNaturalReliefMetres(
|
||||
const FStandalonePassageVoidCrossing& crossing,double qPerMetre) const
|
||||
{
|
||||
constexpr double Q=static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
|
||||
constexpr std::array<double,6> HeightFractions{{0.25,0.35,0.45,0.55,0.65,0.75}};
|
||||
constexpr std::size_t DirectionCount=16;
|
||||
const auto smoothStep=[](double value)
|
||||
{
|
||||
value=std::clamp(value,0.0,1.0);
|
||||
return value*value*(3.0-2.0*value);
|
||||
};
|
||||
std::array<double,HeightFractions.size()> meanRadii{};
|
||||
std::size_t heightCount=0;
|
||||
const auto seed=static_cast<std::uint32_t>(Owner?Owner->Seed:0);
|
||||
const double height=static_cast<double>(crossing.topZSubvoxels
|
||||
-crossing.bottomZSubvoxels);
|
||||
if(height<=0.0)return 0.0;
|
||||
for(const double fraction:HeightFractions)
|
||||
{
|
||||
const double qz=crossing.topZSubvoxels-height*fraction;
|
||||
const float z=static_cast<float>(qz/Q);
|
||||
const float scaledZ=z*0.12f;
|
||||
const float driftX=VF_FieldSurfaceRelief(0.0f,0.0f,scaledZ,seed^0x434f4c58u);
|
||||
const float driftY=VF_FieldSurfaceRelief(0.0f,0.0f,scaledZ,seed^0x434f4c59u);
|
||||
const double axisX=crossing.centerXSubvoxels
|
||||
+std::clamp(static_cast<double>(driftX),-1.0,1.0)*0.35*qPerMetre;
|
||||
const double axisY=crossing.centerYSubvoxels
|
||||
+std::clamp(static_cast<double>(driftY),-1.0,1.0)*0.35*qPerMetre;
|
||||
const double t=(crossing.topZSubvoxels-qz)/height;
|
||||
const double baseFlare=6.0*qPerMetre*smoothStep((t-0.82)/0.18);
|
||||
const double ceilingFlare=2.0*qPerMetre*(1.0-smoothStep(t/0.075));
|
||||
double meanRadius=0.0;
|
||||
for(std::size_t direction=0;direction<DirectionCount;++direction)
|
||||
{
|
||||
const double angle=2.0*3.14159265358979323846*direction/DirectionCount;
|
||||
double radius=crossing.minimumRadiusSubvoxels+1.25*qPerMetre
|
||||
+baseFlare+ceilingFlare;
|
||||
for(int iteration=0;iteration<8;++iteration)
|
||||
{
|
||||
const float sampleX=static_cast<float>((axisX+radius*std::cos(angle))/Q);
|
||||
const float sampleY=static_cast<float>((axisY+radius*std::sin(angle))/Q);
|
||||
const float broadRelief=VF_FieldSurfaceRelief(sampleX*0.12f,
|
||||
sampleY*0.12f,scaledZ,seed^0x434f4c52u);
|
||||
const float surfaceRelief=VF_FieldSurfaceRelief(sampleX,sampleY,z,
|
||||
seed^0x434f4c53u);
|
||||
const double irregularity=1.25*qPerMetre
|
||||
+std::clamp(static_cast<double>(broadRelief),-1.0,1.0)*1.10*qPerMetre
|
||||
+std::clamp(static_cast<double>(surfaceRelief),-1.0,1.0)*0.35*qPerMetre;
|
||||
radius=crossing.minimumRadiusSubvoxels
|
||||
+std::max(0.15*qPerMetre,irregularity)+baseFlare+ceilingFlare;
|
||||
}
|
||||
meanRadius+=radius;
|
||||
}
|
||||
meanRadii[heightCount++]=meanRadius/DirectionCount/qPerMetre;
|
||||
}
|
||||
if(heightCount<2)return 0.0;
|
||||
const auto begin=meanRadii.begin();
|
||||
const auto end=begin+static_cast<std::ptrdiff_t>(heightCount);
|
||||
return *std::max_element(begin,end)-*std::min_element(begin,end);
|
||||
}
|
||||
|
||||
void BuildPassageVoidCrossingPlan()
|
||||
{
|
||||
BuildResolvedBoundaryZIntervals();
|
||||
@@ -4789,7 +5088,8 @@ private:
|
||||
{BuildPassageSpatialIndices();return;}
|
||||
auto& cache=*Owner->PassageVoidCrossingCache;
|
||||
std::lock_guard<std::mutex> lock(cache.mutex);
|
||||
const std::string cacheKey=ResolvedLayout->stableIdentity+"/passage-void-columns-v1";
|
||||
const std::string cacheKey=ResolvedLayout->stableIdentity+"/passage-void-columns-v2-seed-"
|
||||
+std::to_string(Owner->Seed);
|
||||
const auto cached=cache.byLayout.find(cacheKey);
|
||||
if(cached!=cache.byLayout.end())
|
||||
{PassageVoidCrossings=cached->second;BuildPassageSpatialIndices();return;}
|
||||
@@ -4968,6 +5268,12 @@ private:
|
||||
maximumCrossingZ+5.0*qPerMetre));
|
||||
crossing.bottomZSubvoxels=static_cast<std::int64_t>(std::llround(
|
||||
minimumCrossingZ-6.0*qPerMetre));
|
||||
crossing.naturalSurfaceReliefMetres=PassageColumnNaturalReliefMetres(
|
||||
crossing,qPerMetre);
|
||||
// The support-column audit requires 20 cm of vertical relief.
|
||||
// Add a shallow root flare only when the existing field profile
|
||||
// falls short, so naturally varied legacy columns remain intact.
|
||||
crossing.needsRootTaper=crossing.naturalSurfaceReliefMetres<0.20;
|
||||
}
|
||||
PassageVoidCrossings.push_back(crossing);
|
||||
if(crossing.needsColumn)
|
||||
@@ -4975,7 +5281,7 @@ private:
|
||||
const double coreRadius=crossing.nominalRadiusSubvoxels/qPerMetre;
|
||||
const double minimumWall=coreRadius-crossing.maximumRouteOffsetMetres
|
||||
-crossing.maximumBoreRadiusMetres;
|
||||
std::printf("[PassageVoidColumnPlan] id=%s segments=%zu..%zu unsupported_samples=%llu span_m=%.3f threshold_m=%.3f connected=%d/%d axis_m=(%.3f,%.3f) z_m=%.3f..%.3f route_radius_m=%.3f bore_radius_m=%.3f min_radius_m=%.3f nominal_radius_m=%.3f max_radius_m=%.3f nominal_wall_m=%.3f flare_m=6.000 status=%s\n",
|
||||
std::printf("[PassageVoidColumnPlan] id=%s segments=%zu..%zu unsupported_samples=%llu span_m=%.3f threshold_m=%.3f connected=%d/%d axis_m=(%.3f,%.3f) z_m=%.3f..%.3f route_radius_m=%.3f bore_radius_m=%.3f min_radius_m=%.3f nominal_radius_m=%.3f max_radius_m=%.3f nominal_wall_m=%.3f natural_relief_m=%.3f root_taper=%d flare_m=6.000 status=%s\n",
|
||||
passage.stableId.c_str(),crossing.firstSegment,crossing.lastSegment,
|
||||
static_cast<unsigned long long>(crossing.unsupportedSamples),gapLength,
|
||||
shortSpanMaximumMetres,static_cast<int>(crossing.connectedAtStart),
|
||||
@@ -4985,6 +5291,7 @@ private:
|
||||
crossing.maximumRouteOffsetMetres,crossing.maximumBoreRadiusMetres,
|
||||
crossing.minimumRadiusSubvoxels/qPerMetre,coreRadius,
|
||||
crossing.maximumRadiusSubvoxels/qPerMetre,minimumWall,
|
||||
crossing.naturalSurfaceReliefMetres,static_cast<int>(crossing.needsRootTaper),
|
||||
crossing.connectedAtStart&&crossing.connectedAtEnd?"PLANNED":"UNATTACHED");
|
||||
}
|
||||
else
|
||||
@@ -10801,6 +11108,7 @@ private:
|
||||
std::vector<FPassageFeatureNode> PassageFeatureNodes;
|
||||
bool PassageFeatureBoundsFailOpen=false;
|
||||
std::vector<std::vector<FPassageNearestNode>> PassageNearestNodes;
|
||||
std::vector<std::vector<FPassageLaterBoreSource>> PassageLaterBoreSources;
|
||||
std::vector<FPassageProofBound> PassageProofBounds;
|
||||
std::vector<FPassageProofNode> PassageProofNodes;
|
||||
std::vector<std::vector<std::uint8_t>> PassageColumnSegmentFlags,PassageShortSpanSegmentFlags;
|
||||
|
||||
Reference in new issue
Block a user