wip: Tunnel Network cave planner on Core caves (L134, route support still failing)

Seed + 21/47 legacy fields -> 30 compound rooms (72 primitives), 3 branches, destination chamber; mouths fixed;
8.67 deg max grade. Passage composer no longer skips native cave layers (incoming passage 21,425/21,425 open).
Open: route-support audit has 380 unsupported samples near the arrival mouth. 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 11:42:04 +01:00
1 parent 0065ee68e6
commit 75c6ad251d
9 files changed
+1008 -47

No files matched your search

@@ -496,8 +496,9 @@ namespace VoxelForgeCore
// continuous floor while relaxing the spline chord tolerance and bounding shell work.
// Version 226 unifies the passage influence reach in metres and converts it to core
// voxels for every broadphase and proof bound. Version 227 uses the exact smooth-max
// support width/6 when deriving that reach.
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 227;
// support width/6 when deriving that reach. Version 228 composes deterministic cave
// graphs from fixed passage mouths and the owner Tunnel Network settings.
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 228;
enum class FieldChannel : std::uint32_t
{
@@ -991,6 +992,34 @@ namespace VoxelForgeCore
std::array<std::int64_t, 3> boundsMaximumMicrometres{};
};
// Inputs to the reusable, plan-time cave graph composer. All distances are
// integer micrometres and unit-interval values are Q16 so the resolved graph
// does not depend on worker count or floating-point random distributions.
struct ResolvedCavernPlannerSettingsV1
{
std::int64_t roomSpacingMicrometres = 16000000;
std::int64_t minimumRoomRadiusMicrometres = 2000000;
std::int64_t maximumRoomRadiusMicrometres = 5000000;
std::int64_t destinationRadiusMicrometres = 6000000;
std::int64_t minimumTunnelRadiusMicrometres = 750000;
std::int64_t maximumTunnelRadiusMicrometres = 1000000;
std::int64_t maximumTunnelLengthMicrometres = 45000000;
std::int64_t tunnelWarpMicrometres = 0;
std::int64_t caveWarpMicrometres = 0;
std::int64_t smoothWidthMicrometres = 500000;
std::int64_t irregularityAmplitudeMicrometres = 0;
std::int64_t irregularityWavelengthMicrometres = 0;
std::int32_t roomDensityQ16 = 22938;
std::int32_t tunnelDensityQ16 = 26214;
std::int32_t roomShapeVarietyQ16 = 32768;
std::int32_t roomHeightRatioQ16 = 55706;
std::int32_t roomFloorCutMinimumQ16 = 45875;
std::int32_t roomFloorCutMaximumQ16 = 58982;
std::int32_t tunnelHorizontalBiasQ16 = 32768;
std::int32_t originRoomMaximumConnections = 4;
bool tunnelsFlowTowardOrigin = true;
};
// Immutable material-only deposits. The capsule and its conservative bounds are in
// micrometres; bounds include irregularity and the blend envelope.
struct ResolvedLayoutOreVeinRequestV1
@@ -1095,7 +1124,7 @@ namespace VoxelForgeCore
inline bool IsNativeCaveFieldStratum(const ResolvedLayoutStratumV2& stratum) noexcept
{
return stratum.generatorKind=="Caves"
return (stratum.generatorKind=="Caves"||stratum.generatorKind=="TunnelNetwork")
&&stratum.recipeId=="Core.Caves.PrimitiveField"
&&stratum.recipeVersion==1;
}
@@ -1256,6 +1285,17 @@ namespace VoxelForgeCore
std::span<const ResolvedCavernRoomV1> rooms,
std::span<const ResolvedCavernEdgeV1> edges,
std::string& error);
// Build a deterministic, walkable room-and-tunnel graph between the two
// passage mouths that terminate in strateIndex. Compound rooms are emitted
// as overlapping room primitives; the route is planned below a 15 degree
// walking grade and every corridor carries a 4.5 m vertical clearance.
VFCORE_API bool PlanResolvedCavernNetworkV1(
const ResolvedLayoutSnapshotV3& layout, std::int32_t seed,
std::int32_t strateIndex,
const ResolvedCavernPlannerSettingsV1& settings,
std::vector<ResolvedCavernRoomV1>& rooms,
std::vector<ResolvedCavernEdgeV1>& edges,
std::string& error);
VFCORE_API bool ResolveResolvedLayoutOreVeinsV1(
ResolvedLayoutSnapshotV3& layout, std::int32_t seed,
std::span<const ResolvedLayoutOreVeinRequestV1> requests,
@@ -2031,6 +2031,458 @@ bool ResolveResolvedCavernGraphV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
layout=std::move(candidate);return true;
}
bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout,
std::int32_t seed,std::int32_t strateIndex,
const ResolvedCavernPlannerSettingsV1& settings,
std::vector<ResolvedCavernRoomV1>& rooms,
std::vector<ResolvedCavernEdgeV1>& edges,std::string& error)
{
rooms.clear();edges.clear();error.clear();
const auto stratum=std::find_if(layout.strates.begin(),layout.strates.end(),
[strateIndex](const auto& value){return value.strateIndex==strateIndex;});
if(stratum==layout.strates.end()||!IsNativeCaveFieldStratum(*stratum))
{error="cavern planner target is not a resolved native cave stratum";return false;}
if(settings.roomSpacingMicrometres<=0
||settings.minimumRoomRadiusMicrometres<500000
||settings.maximumRoomRadiusMicrometres<settings.minimumRoomRadiusMicrometres
||settings.maximumRoomRadiusMicrometres>100000000
||settings.destinationRadiusMicrometres<settings.maximumRoomRadiusMicrometres
||settings.destinationRadiusMicrometres>100000000
||settings.maximumTunnelLengthMicrometres<=0
||settings.minimumTunnelRadiusMicrometres<0
||settings.maximumTunnelRadiusMicrometres<settings.minimumTunnelRadiusMicrometres
||settings.smoothWidthMicrometres<0
||settings.smoothWidthMicrometres>10000000
||settings.irregularityAmplitudeMicrometres<0
||settings.irregularityAmplitudeMicrometres>10000000
||(settings.irregularityAmplitudeMicrometres>0
&&settings.irregularityWavelengthMicrometres<=0))
{error="cavern planner settings are outside the bounded room, tunnel or relief ranges";return false;}
const ResolvedLayoutPassageV1* arrivalPassage=nullptr;
const ResolvedLayoutPassageV1* departurePassage=nullptr;
for(const auto& passage:layout.passages)
{
if(passage.lowerStrateIndex==strateIndex&&passage.upperStrateIndex<strateIndex
&&(!arrivalPassage||passage.stableId<arrivalPassage->stableId))
arrivalPassage=&passage;
if(passage.upperStrateIndex==strateIndex&&passage.lowerStrateIndex>strateIndex
&&(!departurePassage||passage.stableId<departurePassage->stableId))
departurePassage=&passage;
}
if(!arrivalPassage||!departurePassage)
{error="cavern planner needs an incoming lower mouth and an outgoing upper mouth in the target strate";return false;}
if(layout.coreVoxelSizeMicrometres==0)
{error="cavern planner requires a nonzero Core voxel size";return false;}
using FPoint=std::array<std::int64_t,3>;
constexpr std::int64_t MicrometresPerMetre=1000000;
constexpr std::int64_t MinimumBoreRadius=1750000;
constexpr std::int64_t MinimumBoreHeight=2200000;
constexpr std::int64_t PlannedBoreHeight=4500000;
constexpr std::int64_t MouthOverlap=3000000;
constexpr std::int64_t MouthFloorOffset=750000;
constexpr std::int64_t MaximumBoreRadius=10000000;
constexpr std::int64_t MaximumWalkGradeDenominator=5; // 1:5 = 11.31 degrees.
const auto integerSqrt=[](std::uint64_t value)
{
std::uint64_t result=0;
std::uint64_t bit=std::uint64_t{1}<<62;
while(bit>value)bit>>=2;
while(bit!=0)
{
if(value>=result+bit)
{value-=result+bit;result=(result>>1)+bit;}
else result>>=1;
bit>>=2;
}
return result;
};
const auto distance2d=[&](const FPoint& a,const FPoint& b)
{
const std::int64_t dx=b[0]-a[0],dy=b[1]-a[1];
const auto squared=static_cast<std::uint64_t>(dx*dx+dy*dy);
return static_cast<std::int64_t>(integerSqrt(squared));
};
const auto qToMicrometres=[&](std::int64_t q)
{return q*static_cast<std::int64_t>(layout.coreVoxelSizeMicrometres)
/WorldAssemblySubvoxelsPerVoxel;};
const auto mouthInnerPoint=[&](const ResolvedLayoutPassageV1& passage,
bool lowerSide,FPoint& point)
{
for(std::size_t axis=0;axis<3;++axis)
point[axis]=qToMicrometres(lowerSide?passage.lowerMouthSubvoxels[axis]
:passage.upperMouthSubvoxels[axis]);
point[2]-=MouthFloorOffset;
if(passage.controlPoints.size()<2)return false;
const std::size_t mouthIndex=lowerSide
?std::min(passage.lowerMouthControlIndex,passage.controlPoints.size()-1):0;
const std::size_t tangentIndex=lowerSide
?(mouthIndex>0?mouthIndex-1:1):1;
const std::int64_t tx=passage.controlPoints[mouthIndex].positionSubvoxels[0]
-passage.controlPoints[tangentIndex].positionSubvoxels[0];
const std::int64_t ty=passage.controlPoints[mouthIndex].positionSubvoxels[1]
-passage.controlPoints[tangentIndex].positionSubvoxels[1];
const auto tangentLength=static_cast<std::int64_t>(integerSqrt(
static_cast<std::uint64_t>(tx*tx+ty*ty)));
if(tangentLength<=0)return false;
point[0]+=tx*MouthOverlap/tangentLength;
point[1]+=ty*MouthOverlap/tangentLength;
return true;
};
FPoint routeStart{},routeEnd{};
if(!mouthInnerPoint(*arrivalPassage,true,routeStart)
||!mouthInnerPoint(*departurePassage,false,routeEnd))
{error="cavern passage mouth is missing a usable horizontal route tangent";return false;}
const std::int64_t dx=routeEnd[0]-routeStart[0];
const std::int64_t dy=routeEnd[1]-routeStart[1];
const std::int64_t dz=routeEnd[2]-routeStart[2];
const std::int64_t straightLength=distance2d(routeStart,routeEnd);
const std::int64_t horizontalBias=std::clamp<std::int64_t>(
settings.tunnelHorizontalBiasQ16,0,65536);
const std::int64_t planGradeDenominator=MaximumWalkGradeDenominator
+horizontalBias/32768;
const std::int64_t requiredLength=std::abs(dz)*planGradeDenominator+1000;
std::int64_t lateral=std::max<std::int64_t>(1000000,
std::max(settings.tunnelWarpMicrometres,settings.caveWarpMicrometres)/2);
const std::uint64_t routeHash=LayoutStampSeed(seed,strateIndex,
"tunnel-network/main-route");
if((routeHash&1u)!=0)lateral=-lateral;
std::int64_t pathAxisX=dx,pathAxisY=dy;
const auto makeRoute=[&](std::int64_t lateralOffset)
{
std::array<FPoint,4> points{};
points[0]=routeStart;points[3]=routeEnd;
std::int64_t axisX=dx,axisY=dy;
std::int64_t axisLength=straightLength;
if(axisLength<=0)
{
axisX=1;axisY=0;axisLength=1;
}
pathAxisX=axisX;pathAxisY=axisY;
const std::int64_t perpX=-axisY*lateralOffset/axisLength;
const std::int64_t perpY= axisX*lateralOffset/axisLength;
for(std::size_t axis=0;axis<3;++axis)
{
const std::int64_t delta=routeEnd[axis]-routeStart[axis];
points[1][axis]=routeStart[axis]+delta/3;
points[2][axis]=routeStart[axis]+(delta*2)/3;
}
points[1][0]+=perpX;points[1][1]+=perpY;
points[2][0]-=perpX;points[2][1]-=perpY;
return points;
};
const auto routeLength=[&](const std::array<FPoint,4>& points)
{
std::int64_t length=0;
for(std::size_t i=1;i<points.size();++i)length+=distance2d(points[i-1],points[i]);
return length;
};
auto routePoints=makeRoute(lateral);
for(int attempt=0;routeLength(routePoints)<requiredLength&&attempt<32;++attempt)
{
if(lateral>0)lateral=std::min<std::int64_t>(lateral*2,1000000000ll);
else lateral=std::max<std::int64_t>(lateral*2,-1000000000ll);
routePoints=makeRoute(lateral);
}
const std::int64_t totalRouteLength=routeLength(routePoints);
if(totalRouteLength<=0||totalRouteLength<requiredLength)
{error="cavern planner cannot fit the fixed mouth floor difference under the 1:5 grade budget";return false;}
std::array<std::int64_t,4> cumulative{};
for(std::size_t i=1;i<routePoints.size();++i)
cumulative[i]=cumulative[i-1]+distance2d(routePoints[i-1],routePoints[i]);
if(cumulative.back()!=totalRouteLength)
{error="cavern planner route length accounting failed";return false;}
const auto pointAt=[&](std::int64_t distance)
{
distance=std::clamp<std::int64_t>(distance,0,totalRouteLength);
std::size_t segment=1;
while(segment<cumulative.size()-1&&distance>cumulative[segment])++segment;
const std::int64_t startDistance=cumulative[segment-1];
const std::int64_t segmentLength=std::max<std::int64_t>(1,
cumulative[segment]-startDistance);
const std::int64_t local=distance-startDistance;
FPoint point{};
for(std::size_t axis=0;axis<2;++axis)
{
const std::int64_t delta=routePoints[segment][axis]
-routePoints[segment-1][axis];
point[axis]=routePoints[segment-1][axis]+delta*local/segmentLength;
}
point[2]=routeStart[2]+dz*distance/totalRouteLength;
return point;
};
std::int64_t spacing=std::min(std::max<std::int64_t>(settings.roomSpacingMicrometres,
4000000),settings.maximumTunnelLengthMicrometres);
spacing=std::max<std::int64_t>(4000000,spacing);
if(spacing>settings.maximumTunnelLengthMicrometres)
spacing=settings.maximumTunnelLengthMicrometres;
if(spacing<=0)
{error="cavern planner has no positive route spacing after applying tunnel length limits";return false;}
std::vector<std::int64_t> nodeDistances;
for(std::size_t leg=1;leg<routePoints.size();++leg)
{
const std::int64_t legStart=cumulative[leg-1];
const std::int64_t legLength=cumulative[leg]-legStart;
const std::int64_t segments=std::max<std::int64_t>(1,
(legLength+spacing-1)/spacing);
if(segments>256)
{error="cavern planner would exceed the 256-node bounded backbone";return false;}
for(std::int64_t part=1;part<=segments;++part)
{
const std::int64_t distance=legStart+legLength*part/segments;
if(distance>0&&distance<totalRouteLength)nodeDistances.push_back(distance);
}
}
nodeDistances.push_back(totalRouteLength/2);
std::sort(nodeDistances.begin(),nodeDistances.end());
nodeDistances.erase(std::unique(nodeDistances.begin(),nodeDistances.end()),nodeDistances.end());
if(nodeDistances.empty())nodeDistances.push_back(totalRouteLength/2);
if(nodeDistances.size()>256)
{error="cavern planner would exceed the 256-node bounded backbone";return false;}
const auto clampQ16=[](std::int32_t value)
{return std::clamp<std::int64_t>(value,0,65536);};
const std::int64_t smooth=std::clamp<std::int64_t>(
std::max<std::int64_t>(1500000,settings.smoothWidthMicrometres),
50000,10000000);
const std::int64_t irregularity=std::clamp<std::int64_t>(
settings.irregularityAmplitudeMicrometres,0,500000);
const std::int64_t routeBoreRadius=std::clamp<std::int64_t>(
std::max(MinimumBoreRadius,settings.minimumTunnelRadiusMicrometres),
MinimumBoreRadius,MaximumBoreRadius);
const std::int64_t maximumRouteBoreRadius=std::clamp<std::int64_t>(
std::max(routeBoreRadius,settings.maximumTunnelRadiusMicrometres),
routeBoreRadius,MaximumBoreRadius);
const auto selectedBoreRadius=[&](const std::string& stableId)
{
const std::uint64_t span=static_cast<std::uint64_t>(
maximumRouteBoreRadius-routeBoreRadius);
return routeBoreRadius+(span==0?0:static_cast<std::int64_t>(
LayoutStampSeed(seed,strateIndex,stableId)%(span+1)));
};
const auto passageBoreRadius=[&](const ResolvedLayoutPassageV1& passage,bool lowerSide)
{
std::int64_t radius=qToMicrometres(passage.mouthRadiusSubvoxels);
if(!passage.controlPoints.empty())
{
const auto control=lowerSide?std::min(passage.lowerMouthControlIndex,
passage.controlPoints.size()-1):std::size_t{0};
radius=std::max(radius,qToMicrometres(passage.controlPoints[control].radiusSubvoxels));
}
return std::clamp<std::int64_t>(std::max(routeBoreRadius,radius),
routeBoreRadius,MaximumBoreRadius);
};
const auto fractional=[](std::int64_t value,std::int64_t q16)
{return value*std::clamp<std::int64_t>(q16,0,65536)/65536;};
struct FRoomNode
{
std::string id;
FPoint point{};
std::int64_t radiusMicrometres=0;
bool destination=false;
};
std::vector<FRoomNode> nodes;nodes.reserve(nodeDistances.size());
const std::int64_t floorCutMinimum=clampQ16(settings.roomFloorCutMinimumQ16);
const std::int64_t floorCutMaximum=std::max(floorCutMinimum,
clampQ16(settings.roomFloorCutMaximumQ16));
const std::int64_t floorRange=floorCutMaximum-floorCutMinimum;
const std::int64_t heightRatio=std::clamp<std::int64_t>(
settings.roomHeightRatioQ16,6554,65536);
const std::int64_t baseRoomSpan=settings.maximumRoomRadiusMicrometres
-settings.minimumRoomRadiusMicrometres;
const std::int64_t mainRouteMidpoint=totalRouteLength/2;
for(std::size_t nodeIndex=0;nodeIndex<nodeDistances.size();++nodeIndex)
{
const std::int64_t distance=nodeDistances[nodeIndex];
FRoomNode node;node.point=pointAt(distance);
node.destination=distance==mainRouteMidpoint;
node.id="tunnel-network/strate-"+std::to_string(strateIndex)+"/room-"
+std::to_string(nodeIndex);
const std::uint64_t hash=LayoutStampSeed(seed,strateIndex,node.id);
node.radiusMicrometres=node.destination?settings.destinationRadiusMicrometres
:settings.minimumRoomRadiusMicrometres
+static_cast<std::int64_t>(hash%static_cast<std::uint64_t>(baseRoomSpan+1));
nodes.push_back(std::move(node));
}
const auto makeCompound=[&](const FRoomNode& node,bool destination,
std::vector<ResolvedCavernRoomV1>& outRooms)
{
const std::uint64_t hash=LayoutStampSeed(seed,strateIndex,node.id+"/compound");
const std::int64_t variety=clampQ16(settings.roomShapeVarietyQ16);
const std::size_t additionalPrimitiveCount=static_cast<std::size_t>(
variety*2/65536);
const std::size_t primitiveCount=2+static_cast<std::size_t>(
hash%(additionalPrimitiveCount+1));
std::uint64_t state=hash;
const std::int64_t radius=node.radiusMicrometres;
const std::int64_t floorCutBase=clampQ16(settings.roomFloorCutMinimumQ16);
const std::int64_t componentCount=static_cast<std::int64_t>(primitiveCount);
for(std::int64_t component=0;component<componentCount;++component)
{
const std::string stableId=node.id+"/primitive-"+std::to_string(component);
const std::uint64_t componentHash=LayoutStampNext(state);
const std::int64_t componentScaleQ16=component==0?65536
:42000+static_cast<std::int64_t>(componentHash%22001);
const std::int64_t componentRadius=std::max<std::int64_t>(500000,
fractional(radius,componentScaleQ16));
const std::int64_t axisScaleQ16=component==0?65536
:47000+static_cast<std::int64_t>((componentHash>>8)%19001);
const bool swapAxes=((componentHash>>24)&1u)!=0;
const std::int64_t halfX=swapAxes?fractional(componentRadius,axisScaleQ16)
:componentRadius;
const std::int64_t halfY=swapAxes?componentRadius
:fractional(componentRadius,axisScaleQ16);
const std::int64_t halfZ=std::max<std::int64_t>(500000,
fractional(componentRadius,heightRatio));
const std::int64_t floorCut=floorCutBase
+(floorRange==0?0:static_cast<std::int64_t>(componentHash%(
static_cast<std::uint64_t>(floorRange+1))));
const std::int64_t offsetMagnitude=component==0?0
:fractional(radius,12000+static_cast<std::int64_t>(componentHash%11001));
const std::int64_t routeLengthXY=std::max<std::int64_t>(1,
static_cast<std::int64_t>(integerSqrt(static_cast<std::uint64_t>(
pathAxisX*pathAxisX+pathAxisY*pathAxisY))));
const std::int64_t centerOffset=(component%2==0?1:-1)*offsetMagnitude;
ResolvedCavernRoomV1 room;room.stableId=stableId;
room.strateIndex=strateIndex;room.floorMicrometres=node.point[2];
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)};
if(component>0)
{
const std::int64_t headroom=std::max<std::int64_t>(0,
halfZ-fractional(halfZ,floorCut));
const std::int64_t jitterRange=std::min(headroom,halfZ/5);
const std::int64_t ceilingJitter=jitterRange>0
?static_cast<std::int64_t>((componentHash>>32)
%static_cast<std::uint64_t>(jitterRange+1)):0;
room.centerMicrometres[2]+=ceilingJitter;
}
room.smoothWidthMicrometres=smooth;
room.irregularityAmplitudeMicrometres=irregularity;
room.irregularityWavelengthMicrometres=settings.irregularityWavelengthMicrometres;
outRooms.push_back(std::move(room));
}
(void)destination;
};
for(const auto& node:nodes)makeCompound(node,node.destination,rooms);
const auto componentRoomId=[](const FRoomNode& node)
{return node.id+"/primitive-0";};
const auto makeEdge=[&](std::string id,std::string startAnchor,
const FPoint& start,const FRoomNode& endNode,std::int64_t boreRadius)
{
ResolvedCavernEdgeV1 edge;edge.stableId=std::move(id);
edge.startAnchorStableId=std::move(startAnchor);
edge.endRoomStableId=componentRoomId(endNode);edge.strateIndex=strateIndex;
edge.boreRadiusMicrometres=boreRadius;
edge.boreHeightMicrometres=std::max(PlannedBoreHeight,MinimumBoreHeight);
edge.floorSupportDepthMicrometres=0;
edge.smoothWidthMicrometres=smooth;
edge.irregularityAmplitudeMicrometres=irregularity;
edge.irregularityWavelengthMicrometres=settings.irregularityWavelengthMicrometres;
const FPoint end={endNode.point[0],endNode.point[1],endNode.point[2]};
for(std::size_t axis=0;axis<3;++axis)
{
const std::int64_t delta=end[axis]-start[axis];
edge.controlPointsMicrometres[0][axis]=start[axis];
edge.controlPointsMicrometres[1][axis]=start[axis]+delta/3;
edge.controlPointsMicrometres[2][axis]=start[axis]+(delta*2)/3;
edge.controlPointsMicrometres[3][axis]=end[axis];
}
return edge;
};
const auto arrivalPoint=routeStart;
FPoint previousPoint=arrivalPoint;
std::string previousAnchor=arrivalPassage->stableId+"/lower-mouth";
for(std::size_t nodeIndex=0;nodeIndex<nodes.size();++nodeIndex)
{
const auto& node=nodes[nodeIndex];
const bool portal=nodeIndex==0;
const std::string edgeId="tunnel-network/strate-"+std::to_string(strateIndex)
+"/route-edge-"+std::to_string(nodeIndex);
edges.push_back(makeEdge(edgeId,previousAnchor,previousPoint,node,
portal?passageBoreRadius(*arrivalPassage,true):selectedBoreRadius(edgeId)));
previousPoint={node.point[0],node.point[1],node.point[2]};
previousAnchor=componentRoomId(node);
}
if(!nodes.empty())
{
const std::string departureEdgeId="tunnel-network/strate-"
+std::to_string(strateIndex)+"/departure-edge";
edges.push_back(makeEdge(departureEdgeId,departurePassage->stableId+"/upper-mouth",
routeEnd,nodes.back(),passageBoreRadius(*departurePassage,false)));
}
const std::int64_t roomDensity=clampQ16(settings.roomDensityQ16);
const std::int64_t tunnelDensity=clampQ16(settings.tunnelDensityQ16);
const std::int64_t branchThreshold=roomDensity*tunnelDensity/65536;
const std::size_t branchLimit=settings.originRoomMaximumConnections<=0
?32u:static_cast<std::size_t>(std::clamp(settings.originRoomMaximumConnections,0,32));
std::size_t branchCount=0;
for(std::size_t nodeIndex=1;nodeIndex+1<nodes.size()
&&branchCount<branchLimit&&rooms.size()<2048;++nodeIndex)
{
const auto& parent=nodes[nodeIndex];
const std::uint64_t branchHash=LayoutStampSeed(seed,strateIndex,
parent.id+"/side-branch");
if(static_cast<std::int64_t>(branchHash&0xffffu)>=branchThreshold)continue;
const std::int64_t branchLength=std::max<std::int64_t>(
routeBoreRadius*3,std::min(spacing/2,settings.maximumTunnelLengthMicrometres/2));
const std::int64_t direction=(branchHash&0x10000u)?1:-1;
FRoomNode branch;branch.id=parent.id+"/side-"+std::to_string(branchCount);
branch.radiusMicrometres=settings.minimumRoomRadiusMicrometres
+static_cast<std::int64_t>((branchHash>>17)
%static_cast<std::uint64_t>(baseRoomSpan+1));
const std::int64_t pathAxisLength=std::max<std::int64_t>(1,
static_cast<std::int64_t>(integerSqrt(static_cast<std::uint64_t>(
pathAxisX*pathAxisX+pathAxisY*pathAxisY))));
branch.point={parent.point[0]+(-pathAxisY)*branchLength/pathAxisLength*direction,
parent.point[1]+pathAxisX*branchLength/pathAxisLength*direction,
parent.point[2]};
makeCompound(branch,false,rooms);
const std::string branchEdgeId="tunnel-network/strate-"
+std::to_string(strateIndex)+"/side-edge-"+std::to_string(branchCount);
if(settings.tunnelsFlowTowardOrigin)
edges.push_back(makeEdge(branchEdgeId,componentRoomId(branch),
branch.point,parent,selectedBoreRadius(branchEdgeId)));
else
edges.push_back(makeEdge(branchEdgeId,componentRoomId(parent),
parent.point,branch,selectedBoreRadius(branchEdgeId)));
++branchCount;
}
if(nodes.empty()||rooms.empty()||edges.empty())
{error="cavern planner emitted an empty arrival-to-departure graph";return false;}
double maximumGradeDegrees=0.0;
for(const auto& edge:edges)
{
const auto& a=edge.controlPointsMicrometres.front();
const auto& b=edge.controlPointsMicrometres.back();
const std::int64_t run=static_cast<std::int64_t>(integerSqrt(
static_cast<std::uint64_t>((b[0]-a[0])*(b[0]-a[0])
+(b[1]-a[1])*(b[1]-a[1]))));
const std::int64_t rise=std::abs(b[2]-a[2]);
if(run<=0&&rise>0)
{error="cavern planner produced a vertical route edge with no walking run";return false;}
if(run>0&&rise*MaximumWalkGradeDenominator>run)
{error="cavern planner emitted an edge beyond its 1:5 route grade budget";return false;}
if(run>0)maximumGradeDegrees=std::max(maximumGradeDegrees,
std::atan2(static_cast<double>(rise),static_cast<double>(run))
*180.0/3.14159265358979323846);
}
std::printf("[CavernPlannerV1] strate=%d seed=%d mouths=%s->%s route_nodes=%zu room_primitives=%zu edges=%zu branches=%zu route_horizontal_m=%.3f floor_delta_m=%.3f max_grade_deg=%.3f grade_limit_deg=11.310 clearance_radius_m=%.3f clearance_height_m=%.3f destination_radius_m=%.3f status=PASS\n",
strateIndex,seed,arrivalPassage->stableId.c_str(),departurePassage->stableId.c_str(),
nodes.size(),rooms.size(),edges.size(),branchCount,totalRouteLength/1.0e6,
std::abs(dz)/1.0e6,maximumGradeDegrees,routeBoreRadius/1.0e6,PlannedBoreHeight/1.0e6,
settings.destinationRadiusMicrometres/1.0e6);
return true;
}
bool ResolveResolvedLayoutOreVeinsV1(ResolvedLayoutSnapshotV3& layout,std::int32_t seed,
std::span<const ResolvedLayoutOreVeinRequestV1> requests,std::string& error)
{
+25 -7
View File
@@ -1221,16 +1221,15 @@ 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;
if(queryStratum&&queryStratum->Resolved
&&queryStratum->Resolved->passageCapabilities.hasNativeCaveField
&&passage.lowerStrateIndex==queryStratum->StrateIndex)
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.
if(passageProfile)++passageProfile->passageIterations;
if(passageTileScopeMatches&&!passageTileQueries->hasRouteFeature(passageIndex))
continue;
@@ -7488,7 +7487,16 @@ private:
const double delta=(point[axis]-feature.centerMetres[axis])/radius;
normalizedSquared+=delta*delta;meanRadius+=radius/3.0;
}
const double ellipsoid=(std::sqrt(normalizedSquared)-1.0)*meanRadius;
double ellipsoid=(std::sqrt(normalizedSquared)-1.0)*meanRadius;
if(feature.irregularityAmplitudeVoxels>0.0f)
{
const float fade=SmoothUnit((Z-feature.floorVoxels)/0.8f);
ellipsoid+=StampValueNoise(
X-feature.centerVoxels[0],Y-feature.centerVoxels[1],
Z-feature.centerVoxels[2],
feature.irregularityWavelengthVoxels,feature.irregularitySeed)
*feature.irregularityAmplitudeVoxels*metresPerVoxel*fade;
}
const double floor=feature.room->floorMicrometres/1000000.0-point[2];
const double width=feature.room->smoothWidthMicrometres/1000000.0;
featureVoid=SmoothMaxMetres(ellipsoid,floor,width);
@@ -7526,7 +7534,17 @@ private:
}
const double normalized=std::sqrt(radialSquared/(radius*radius)
+vertical*vertical/(height*height));
const double distance=(normalized-1.0)*std::min(radius,height);
double distance=(normalized-1.0)*std::min(radius,height);
if(feature.irregularityAmplitudeVoxels>0.0f)
{
const float floorVoxels=static_cast<float>(nearestFloor/metresPerVoxel);
const float fade=SmoothUnit((Z-floorVoxels)/0.8f);
distance+=StampValueNoise(
X-feature.controlPointsVoxels[0][0],
Y-feature.controlPointsVoxels[0][1],Z-floorVoxels,
feature.irregularityWavelengthVoxels,feature.irregularitySeed)
*feature.irregularityAmplitudeVoxels*metresPerVoxel*fade;
}
const bool clearlyCloser=distance<tube-SegmentDistanceTieToleranceMetres;
const bool stableTie=std::abs(distance-tube)
<=SegmentDistanceTieToleranceMetres&&segmentIndex<nearestSegment;