Mouth edges matched passage position, radius, tangent (dot 0.99999) and floor slope, but the cubic departure edge still curls ~1 m back alongside the passage within 5 m; shell floor violations remain; composed sweeps 0/16. Sixth mouth-join round (L144-L149): stop and take the design question to the owner. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
630 lines
38 KiB
Diff
630 lines
38 KiB
Diff
diff --git a/Standalone/Core/include/VoxelForgeCore/PassageSpline.h b/Standalone/Core/include/VoxelForgeCore/PassageSpline.h
|
|
index 24c68ab..c8610eb 100644
|
|
--- a/Standalone/Core/include/VoxelForgeCore/PassageSpline.h
|
|
+++ b/Standalone/Core/include/VoxelForgeCore/PassageSpline.h
|
|
@@ -13,6 +13,7 @@ namespace VoxelForgeCore::PassageSpline
|
|
using FPoint = std::array<double,4>;
|
|
inline constexpr std::size_t ArcTableSteps = 32;
|
|
inline constexpr double UpperMouthFloorBlendRadiusMetres = 3.5;
|
|
+ inline constexpr double FlatFloorCornerBlendMetres = 0.5;
|
|
|
|
struct FSegment
|
|
{
|
|
@@ -60,6 +61,36 @@ namespace VoxelForgeCore::PassageSpline
|
|
return result;
|
|
}
|
|
|
|
+ inline FPoint EvaluateRawDerivative(std::span<const FPoint> controls,
|
|
+ std::size_t span,double t,double unitsPerMetre)
|
|
+ {
|
|
+ const auto& p1=controls[span];
|
|
+ const auto& p2=controls[span+1];
|
|
+ const auto& p0=controls[span==0?0:span-1];
|
|
+ const auto& p3=controls[std::min(span+2,controls.size()-1)];
|
|
+ const double dt0=KnotSpan(p0,p1,unitsPerMetre);
|
|
+ const double dt1=KnotSpan(p1,p2,unitsPerMetre);
|
|
+ const double dt2=KnotSpan(p2,p3,unitsPerMetre);
|
|
+ const double t2=t*t;
|
|
+ const double h00=6.0*t2-6.0*t;
|
|
+ const double h01=-h00;
|
|
+ const double h10=3.0*t2-4.0*t+1.0;
|
|
+ const double h11=3.0*t2-2.0*t;
|
|
+ FPoint result{};
|
|
+ for(const std::size_t axis:{0u,1u,3u})
|
|
+ {
|
|
+ const double m1=dt1*((p1[axis]-p0[axis])/dt0
|
|
+ -(p2[axis]-p0[axis])/(dt0+dt1)
|
|
+ +(p2[axis]-p1[axis])/dt1);
|
|
+ const double m2=dt1*((p2[axis]-p1[axis])/dt1
|
|
+ -(p3[axis]-p1[axis])/(dt1+dt2)
|
|
+ +(p3[axis]-p2[axis])/dt2);
|
|
+ result[axis]=h00*p1[axis]+h01*p2[axis]+h10*m1+h11*m2;
|
|
+ }
|
|
+ result[2]=p2[2]-p1[2];
|
|
+ return result;
|
|
+ }
|
|
+
|
|
inline bool ClosestRouteHeightAtXY(std::span<const std::array<double,3>> route,
|
|
double x,double y,double& height,double& distanceSquared)
|
|
{
|
|
diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h
|
|
index 8c44461..e712747 100644
|
|
--- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h
|
|
+++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h
|
|
@@ -511,7 +511,8 @@ namespace VoxelForgeCore
|
|
// the planned graph route, preserves its walk corridor through shell overlaps, and keeps
|
|
// boundary-blended host rock from widening the authored passage wall.
|
|
// v237 adds the shared Core cave-graph shaft primitive and basal terminal route.
|
|
- inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 237;
|
|
+ // v238 joins cave edges to passage mouth tubes with matched geometry and clearance.
|
|
+ inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 238;
|
|
|
|
enum class FieldChannel : std::uint32_t
|
|
{
|
|
diff --git a/Standalone/Core/src/ResolvedLayoutInternal.h b/Standalone/Core/src/ResolvedLayoutInternal.h
|
|
index 7b7520a..095418b 100644
|
|
--- a/Standalone/Core/src/ResolvedLayoutInternal.h
|
|
+++ b/Standalone/Core/src/ResolvedLayoutInternal.h
|
|
@@ -2367,8 +2367,7 @@ bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout,
|
|
constexpr std::int64_t MinimumBoreRadius=1750000;
|
|
constexpr std::int64_t MinimumBoreHeight=2200000;
|
|
constexpr std::int64_t PlannedBoreHeight=4500000;
|
|
- constexpr std::int64_t MouthOverlap=2000000;
|
|
- constexpr std::int64_t PortalLeadLength=6000000;
|
|
+ constexpr std::int64_t PortalLeadLength=1000000;
|
|
constexpr std::int64_t MaximumBoreRadius=10000000;
|
|
constexpr std::int64_t MaximumWalkGradeDenominator=5; // 1:5 = 11.31 degrees.
|
|
const auto integerSqrt=[](std::uint64_t value)
|
|
@@ -2394,171 +2393,77 @@ bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout,
|
|
const auto qToMicrometres=[&](std::int64_t q)
|
|
{return q*static_cast<std::int64_t>(layout.coreVoxelSizeMicrometres)
|
|
/WorldAssemblySubvoxelsPerVoxel;};
|
|
- const std::int64_t mouthBoreRadius=std::clamp<std::int64_t>(
|
|
- std::max(MinimumBoreRadius,settings.minimumTunnelRadiusMicrometres),
|
|
- MinimumBoreRadius,MaximumBoreRadius);
|
|
- const auto chooseMouthRouteTangent=[&](const ResolvedLayoutPassageV1& passage,
|
|
- std::size_t mouthIndex,bool lowerSide,
|
|
- const std::array<std::int64_t,2>& authoredRouteTangentQ16,
|
|
- std::int64_t authoredFloorSlopeQ16,const FPoint& mouthFloorPoint)
|
|
+ std::int64_t mouthBoreRadius=std::max(MinimumBoreRadius,
|
|
+ settings.minimumTunnelRadiusMicrometres);
|
|
+ const auto includePassageMouthRadius=[&](
|
|
+ const ResolvedLayoutPassageV1* passage,bool lowerSide)
|
|
{
|
|
- constexpr std::array<std::array<std::int64_t,2>,16> DirectionsQ16={{
|
|
- {{65536,0}},{{60547,25080}},{{46341,46341}},{{25080,60547}},
|
|
- {{0,65536}},{{-25080,60547}},{{-46341,46341}},{{-60547,25080}},
|
|
- {{-65536,0}},{{-60547,-25080}},{{-46341,-46341}},{{-25080,-60547}},
|
|
- {{0,-65536}},{{25080,-60547}},{{46341,-46341}},{{60547,-25080}}}};
|
|
- const auto scoreDirection=[&](const std::array<std::int64_t,2>& outwardQ16)
|
|
- {
|
|
- const std::size_t segmentCount=passage.controlPoints.size()>1
|
|
- ?passage.controlPoints.size()-1:0;
|
|
- std::int64_t worst=std::numeric_limits<std::int64_t>::max();
|
|
- bool sampled=false;
|
|
- const std::int64_t projectedSlopeQ16=(authoredFloorSlopeQ16
|
|
- *(outwardQ16[0]*authoredRouteTangentQ16[0]
|
|
- +outwardQ16[1]*authoredRouteTangentQ16[1])
|
|
- /(65536ll*65536ll));
|
|
- constexpr std::int64_t MouthClearanceProbeExtent=48000000;
|
|
- for(std::int64_t distance=2000000;
|
|
- distance<=MouthClearanceProbeExtent;distance+=2000000)
|
|
- {
|
|
- const std::int64_t x=mouthFloorPoint[0]
|
|
- +outwardQ16[0]*distance/65536;
|
|
- const std::int64_t y=mouthFloorPoint[1]
|
|
- +outwardQ16[1]*distance/65536;
|
|
- const std::int64_t caveFloor=mouthFloorPoint[2]
|
|
- +projectedSlopeQ16*distance/65536;
|
|
- const std::int64_t caveCeiling=caveFloor+PlannedBoreHeight;
|
|
- for(std::size_t segmentIndex=0;segmentIndex<segmentCount;++segmentIndex)
|
|
- {
|
|
- // The two spans nearest the mouth form the authored joint itself.
|
|
- // Test the candidate against the rest of the passage so a loop
|
|
- // crossing cannot be mistaken for a usable portal tangent.
|
|
- if(lowerSide&&segmentIndex+2>=mouthIndex)continue;
|
|
- if(!lowerSide&&segmentIndex<2)continue;
|
|
- const auto& a=passage.controlPoints[segmentIndex];
|
|
- const auto& b=passage.controlPoints[segmentIndex+1];
|
|
- const std::int64_t ax=qToMicrometres(a.positionSubvoxels[0]);
|
|
- const std::int64_t ay=qToMicrometres(a.positionSubvoxels[1]);
|
|
- const std::int64_t az=qToMicrometres(a.positionSubvoxels[2]);
|
|
- const std::int64_t bx=qToMicrometres(b.positionSubvoxels[0]);
|
|
- const std::int64_t by=qToMicrometres(b.positionSubvoxels[1]);
|
|
- const std::int64_t bz=qToMicrometres(b.positionSubvoxels[2]);
|
|
- const std::int64_t dx=bx-ax,dy=by-ay;
|
|
- const double lengthSquared=static_cast<double>(dx)*dx
|
|
- +static_cast<double>(dy)*dy;
|
|
- const double projection=lengthSquared>0.0
|
|
- ?std::clamp((static_cast<double>(x-ax)*dx
|
|
- +static_cast<double>(y-ay)*dy)/lengthSquared,0.0,1.0):0.0;
|
|
- const std::int64_t tQ16=static_cast<std::int64_t>(std::llround(
|
|
- projection*65536.0));
|
|
- const std::int64_t closestX=ax+dx*tQ16/65536;
|
|
- const std::int64_t closestY=ay+dy*tQ16/65536;
|
|
- const std::int64_t horizontalDistance=static_cast<std::int64_t>(
|
|
- integerSqrt(static_cast<std::uint64_t>((x-closestX)*(x-closestX)
|
|
- +(y-closestY)*(y-closestY))));
|
|
- const std::int64_t passageCenterZ=qToMicrometres(
|
|
- a.positionSubvoxels[2])
|
|
- +(qToMicrometres(b.positionSubvoxels[2])
|
|
- -qToMicrometres(a.positionSubvoxels[2]))*tQ16/65536;
|
|
- const std::int64_t passageRadius=qToMicrometres(a.radiusSubvoxels)
|
|
- +(qToMicrometres(b.radiusSubvoxels)
|
|
- -qToMicrometres(a.radiusSubvoxels))*tQ16/65536;
|
|
- const std::int64_t horizontalGap=horizontalDistance
|
|
- -passageRadius-mouthBoreRadius;
|
|
- const std::int64_t passageFloor=passageCenterZ-passageRadius;
|
|
- const std::int64_t passageCeiling=passageCenterZ+passageRadius;
|
|
- const std::int64_t verticalGap=caveFloor>passageCeiling
|
|
- ?caveFloor-passageCeiling
|
|
- :(passageFloor>caveCeiling?passageFloor-caveCeiling
|
|
- :-std::min(caveCeiling-passageFloor,
|
|
- passageCeiling-caveFloor));
|
|
- const std::int64_t separation=horizontalGap>=0&&verticalGap>=0
|
|
- ?static_cast<std::int64_t>(integerSqrt(static_cast<std::uint64_t>(
|
|
- horizontalGap*horizontalGap+verticalGap*verticalGap)))
|
|
- :std::max(horizontalGap,verticalGap);
|
|
- worst=std::min(worst,separation);sampled=true;
|
|
- }
|
|
- }
|
|
- return sampled?worst:std::numeric_limits<std::int64_t>::max();
|
|
- };
|
|
-
|
|
- // Keep the authored tangent whenever it clears the other passage spans.
|
|
- // If the passage folds back over the mouth, choose the outward direction
|
|
- // with the greatest 3D clearance while preserving deterministic tie order.
|
|
- const std::array<std::int64_t,2> authoredOutwardQ16={
|
|
- lowerSide?authoredRouteTangentQ16[0]:-authoredRouteTangentQ16[0],
|
|
- lowerSide?authoredRouteTangentQ16[1]:-authoredRouteTangentQ16[1]};
|
|
- const std::int64_t authoredScore=scoreDirection(authoredOutwardQ16);
|
|
- if(authoredScore>=0)return authoredRouteTangentQ16;
|
|
- std::array<std::int64_t,2> bestOutwardQ16=authoredOutwardQ16;
|
|
- std::int64_t bestScore=authoredScore;
|
|
- std::int64_t bestAlignment=authoredOutwardQ16[0]*authoredOutwardQ16[0]
|
|
- +authoredOutwardQ16[1]*authoredOutwardQ16[1];
|
|
- for(const auto& candidate:DirectionsQ16)
|
|
- {
|
|
- const std::int64_t score=scoreDirection(candidate);
|
|
- const std::int64_t alignment=candidate[0]*authoredOutwardQ16[0]
|
|
- +candidate[1]*authoredOutwardQ16[1];
|
|
- if(score>bestScore||(score==bestScore&&alignment>bestAlignment))
|
|
- {bestScore=score;bestAlignment=alignment;bestOutwardQ16=candidate;}
|
|
- }
|
|
- std::printf("[CavernMouthClearancePlan] passage=%s side=%s authored_clearance_m=%.3f selected_clearance_m=%.3f outward_q16=(%lld,%lld)\n",
|
|
- passage.stableId.c_str(),lowerSide?"lower":"upper",
|
|
- authoredScore/1.0e6,bestScore/1.0e6,
|
|
- static_cast<long long>(bestOutwardQ16[0]),
|
|
- static_cast<long long>(bestOutwardQ16[1]));
|
|
- return std::array<std::int64_t,2>{
|
|
- lowerSide?bestOutwardQ16[0]:-bestOutwardQ16[0],
|
|
- lowerSide?bestOutwardQ16[1]:-bestOutwardQ16[1]};
|
|
+ if(!passage||passage->controlPoints.empty())return;
|
|
+ const std::size_t index=lowerSide
|
|
+ ?std::min(passage->lowerMouthControlIndex,passage->controlPoints.size()-1):0;
|
|
+ mouthBoreRadius=std::max(mouthBoreRadius,
|
|
+ qToMicrometres(passage->controlPoints[index].radiusSubvoxels));
|
|
};
|
|
+ includePassageMouthRadius(arrivalPassage,true);
|
|
+ includePassageMouthRadius(departurePassage,false);
|
|
+ mouthBoreRadius=std::clamp(mouthBoreRadius,MinimumBoreRadius,MaximumBoreRadius);
|
|
const auto mouthInnerPoint=[&](const ResolvedLayoutPassageV1& passage,
|
|
bool lowerSide,FPoint& point,std::array<std::int64_t,2>& routeTangentQ16,
|
|
std::int64_t& floorSlopeQ16)
|
|
{
|
|
- for(std::size_t axis=0;axis<3;++axis)
|
|
- point[axis]=qToMicrometres(lowerSide?passage.lowerMouthSubvoxels[axis]
|
|
- :passage.upperMouthSubvoxels[axis]);
|
|
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;
|
|
- // The lower arrival continues from the previous passage point through its
|
|
- // mouth. The upper departure approaches its mouth from the cave and then
|
|
- // continues toward control point 1, so the two route tangents have opposite
|
|
- // signs even though both portal offsets lie on the cave side.
|
|
+ if(lowerSide&&mouthIndex==0)return false;
|
|
+ const std::size_t span=lowerSide?mouthIndex-1:0;
|
|
+ const double endpointT=lowerSide?1.0:0.0;
|
|
const std::int64_t direction=lowerSide?1:-1;
|
|
- const std::int64_t tx=direction*(passage.controlPoints[mouthIndex].positionSubvoxels[0]
|
|
- -passage.controlPoints[tangentIndex].positionSubvoxels[0]);
|
|
- const std::int64_t ty=direction*(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;
|
|
- routeTangentQ16={tx*65536/tangentLength,ty*65536/tangentLength};
|
|
+ std::vector<VoxelForgeCore::PassageSpline::FPoint> splineControls;
|
|
+ std::vector<double> floorProfile;
|
|
+ splineControls.reserve(passage.controlPoints.size());
|
|
+ floorProfile.reserve(passage.controlPoints.size());
|
|
+ for(const auto& control:passage.controlPoints)
|
|
+ {
|
|
+ const std::int64_t x=qToMicrometres(control.positionSubvoxels[0]);
|
|
+ const std::int64_t y=qToMicrometres(control.positionSubvoxels[1]);
|
|
+ const std::int64_t z=qToMicrometres(control.positionSubvoxels[2]);
|
|
+ const std::int64_t radius=qToMicrometres(control.radiusSubvoxels);
|
|
+ splineControls.push_back({static_cast<double>(x),static_cast<double>(y),
|
|
+ static_cast<double>(z),static_cast<double>(radius)});
|
|
+ floorProfile.push_back(static_cast<double>(z-radius));
|
|
+ }
|
|
+ auto derivative=VoxelForgeCore::PassageSpline::EvaluateRawDerivative(
|
|
+ splineControls,span,endpointT,MicrometresPerMetre);
|
|
+ if(!lowerSide)
|
|
+ {
|
|
+ derivative[0]=-derivative[0];
|
|
+ derivative[1]=-derivative[1];
|
|
+ }
|
|
+ const double tangentLength=std::hypot(derivative[0],derivative[1]);
|
|
+ if(!(tangentLength>1.0e-9)||!std::isfinite(tangentLength))return false;
|
|
+ routeTangentQ16={
|
|
+ static_cast<std::int64_t>(std::llround(derivative[0]*65536.0/tangentLength)),
|
|
+ static_cast<std::int64_t>(std::llround(derivative[1]*65536.0/tangentLength))};
|
|
+ const auto passageSegments=VoxelForgeCore::PassageSpline::BuildSegments(
|
|
+ splineControls,splineControls.size()-1,2.0,MicrometresPerMetre,
|
|
+ floorProfile);
|
|
+ double spanHorizontalLengthMicrometres=0.0;
|
|
+ for(const auto& segment:passageSegments)
|
|
+ if(segment.controlSpan==span)
|
|
+ spanHorizontalLengthMicrometres+=std::hypot(
|
|
+ segment.b[0]-segment.a[0],segment.b[1]-segment.a[1]);
|
|
+ if(!(spanHorizontalLengthMicrometres>1.0e-9))return false;
|
|
const auto& mouthControl=passage.controlPoints[mouthIndex];
|
|
- const auto& tangentControl=passage.controlPoints[tangentIndex];
|
|
const std::int64_t mouthFloor=mouthControl.positionSubvoxels[2]
|
|
-mouthControl.radiusSubvoxels;
|
|
- const std::int64_t tangentFloor=tangentControl.positionSubvoxels[2]
|
|
- -tangentControl.radiusSubvoxels;
|
|
- // Resolved passage mouths already store the player center at floor + 1.13 m.
|
|
- // Cave edge control points store the floor itself, so start from the passage
|
|
- // control's floor rather than reusing the player-center mouth height.
|
|
+ const std::size_t tangentIndex=lowerSide?mouthIndex-1:1;
|
|
+ const double passageFloorDelta=direction*(floorProfile[mouthIndex]
|
|
+ -floorProfile[tangentIndex]);
|
|
+ const double slope=passageFloorDelta/spanHorizontalLengthMicrometres;
|
|
+ floorSlopeQ16=static_cast<std::int64_t>(std::llround(slope*65536.0));
|
|
+ point[0]=qToMicrometres(mouthControl.positionSubvoxels[0]);
|
|
+ point[1]=qToMicrometres(mouthControl.positionSubvoxels[1]);
|
|
point[2]=qToMicrometres(mouthFloor);
|
|
- const std::int64_t passageFloorDelta=direction*(mouthFloor-tangentFloor);
|
|
- floorSlopeQ16=passageFloorDelta*65536/tangentLength;
|
|
- const auto authoredRouteTangentQ16=routeTangentQ16;
|
|
- routeTangentQ16=chooseMouthRouteTangent(passage,mouthIndex,lowerSide,
|
|
- authoredRouteTangentQ16,floorSlopeQ16,point);
|
|
- const std::int64_t tangentAlignment=routeTangentQ16[0]
|
|
- *authoredRouteTangentQ16[0]+routeTangentQ16[1]
|
|
- *authoredRouteTangentQ16[1];
|
|
- floorSlopeQ16=floorSlopeQ16*tangentAlignment/(65536ll*65536ll);
|
|
- const std::int64_t offsetDirection=lowerSide?1:-1;
|
|
- point[0]+=offsetDirection*routeTangentQ16[0]*MouthOverlap/65536;
|
|
- point[1]+=offsetDirection*routeTangentQ16[1]*MouthOverlap/65536;
|
|
- point[2]+=offsetDirection*floorSlopeQ16*MouthOverlap/65536;
|
|
return true;
|
|
};
|
|
FPoint routeStart{},routeEnd{};
|
|
@@ -2738,7 +2643,7 @@ bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout,
|
|
controls.size()-1,2.0,1000000.0);
|
|
const double totalLength=segments.empty()?0.0:
|
|
segments.back().alongMetres+segments.back().lengthMetres;
|
|
- constexpr double PortalExclusionMetres=10.0;
|
|
+ constexpr double PortalExclusionMetres=2.0;
|
|
for(const auto& segment:segments)
|
|
{
|
|
const double midpoint=segment.alongMetres+segment.lengthMetres*0.5;
|
|
@@ -2815,7 +2720,7 @@ bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout,
|
|
if(routePassageClearanceSpans.empty()||points.size()<2)
|
|
return std::numeric_limits<double>::infinity();
|
|
const double totalLength=routeLength(points)/1.0e6;
|
|
- constexpr double PortalExclusionMetres=10.0;
|
|
+ constexpr double PortalExclusionMetres=2.0;
|
|
double minimumClearance=std::numeric_limits<double>::infinity();
|
|
double along=0.0;
|
|
for(std::size_t index=1;index<points.size();++index)
|
|
@@ -3264,6 +3169,36 @@ bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout,
|
|
}
|
|
return edge;
|
|
};
|
|
+ const auto makePassageMouthEdge=[&](std::string id,std::string startAnchor,
|
|
+ const FPoint& start,const FRoomNode& endNode,
|
|
+ const ResolvedLayoutPassageV1& passage,bool lowerSide,
|
|
+ const std::array<std::int64_t,2>& tangentQ16,std::int64_t floorSlopeQ16,
|
|
+ std::int64_t profileStartDistance,std::int64_t profileEndDistance)
|
|
+ {
|
|
+ const std::size_t mouthIndex=lowerSide
|
|
+ ?std::min(passage.lowerMouthControlIndex,passage.controlPoints.size()-1):0;
|
|
+ const auto& mouth=passage.controlPoints[mouthIndex];
|
|
+ const std::int64_t mouthRadius=qToMicrometres(mouth.radiusSubvoxels);
|
|
+ auto edge=makeEdge(std::move(id),std::move(startAnchor),start,endNode,
|
|
+ mouthRadius,profileStartDistance,profileEndDistance);
|
|
+ edge.boreRadiusMicrometres=mouthRadius;
|
|
+ edge.boreHeightMicrometres=2*mouthRadius;
|
|
+ edge.smoothWidthMicrometres=static_cast<std::int64_t>(std::llround(
|
|
+ VoxelForgeCore::PassageSpline::FlatFloorCornerBlendMetres
|
|
+ *MicrometresPerMetre));
|
|
+ edge.unionWidthMicrometres=smooth;
|
|
+ const FPoint end={endNode.point[0],endNode.point[1],endNode.point[2]};
|
|
+ const std::int64_t horizontalRun=distance2d(start,end);
|
|
+ const std::int64_t controlHandle=std::max<std::int64_t>(1,
|
|
+ std::min(PortalLeadLength,std::max<std::int64_t>(1,horizontalRun))/3);
|
|
+ edge.controlPointsMicrometres[1][0]=start[0]
|
|
+ +tangentQ16[0]*controlHandle/65536;
|
|
+ edge.controlPointsMicrometres[1][1]=start[1]
|
|
+ +tangentQ16[1]*controlHandle/65536;
|
|
+ edge.controlPointsMicrometres[1][2]=start[2]
|
|
+ +floorSlopeQ16*controlHandle/65536;
|
|
+ return edge;
|
|
+ };
|
|
struct FPlannedShaftLanding
|
|
{
|
|
FRoomNode node;
|
|
@@ -3396,9 +3331,13 @@ bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout,
|
|
{
|
|
const auto& node=nodes[nodeIndex];
|
|
const std::string edgeId=routePrefix+"route-edge-"+std::to_string(nodeIndex);
|
|
- auto edge=makeEdge(edgeId,previousAnchor,previousPoint,node,
|
|
- selectedBoreRadius(edgeId),
|
|
- previousProfileDistance,node.floorProfileDistanceMicrometres);
|
|
+ auto edge=nodeIndex==0
|
|
+ ?makePassageMouthEdge(edgeId,previousAnchor,previousPoint,node,
|
|
+ *arrivalPassage,true,arrivalTangentQ16,arrivalFloorSlopeQ16,
|
|
+ previousProfileDistance,node.floorProfileDistanceMicrometres)
|
|
+ :makeEdge(edgeId,previousAnchor,previousPoint,node,
|
|
+ selectedBoreRadius(edgeId),previousProfileDistance,
|
|
+ node.floorProfileDistanceMicrometres);
|
|
edges.push_back(std::move(edge));
|
|
previousPoint={node.point[0],node.point[1],node.point[2]};
|
|
previousProfileDistance=node.floorProfileDistanceMicrometres;
|
|
@@ -3407,8 +3346,9 @@ bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout,
|
|
if(!nodes.empty()&&departurePassage)
|
|
{
|
|
const std::string departureEdgeId=routePrefix+"departure-edge";
|
|
- auto edge=makeEdge(departureEdgeId,departurePassage->stableId+"/upper-mouth",
|
|
- routeEnd,nodes.back(),selectedBoreRadius(departureEdgeId),
|
|
+ auto edge=makePassageMouthEdge(departureEdgeId,
|
|
+ departurePassage->stableId+"/upper-mouth",routeEnd,nodes.back(),
|
|
+ *departurePassage,false,departureTangentQ16,departureFloorSlopeQ16,
|
|
totalRouteLength,nodes.back().floorProfileDistanceMicrometres);
|
|
edges.push_back(std::move(edge));
|
|
}
|
|
diff --git a/Standalone/Core/src/ShapeWorldInternal.h b/Standalone/Core/src/ShapeWorldInternal.h
|
|
index 1a57f85..d17aa02 100644
|
|
--- a/Standalone/Core/src/ShapeWorldInternal.h
|
|
+++ b/Standalone/Core/src/ShapeWorldInternal.h
|
|
@@ -4443,11 +4443,12 @@ private:
|
|
buildSegmentNode(buildSegmentNode,0,runtime.segmentOrder.size());
|
|
const double radius=edge.boreRadiusMicrometres/1000000.0;
|
|
const double height=edge.boreHeightMicrometres/1000000.0;
|
|
+ const bool passageMouthTube=IsPlannerPassageMouthContinuationEdge(edge);
|
|
if(radius>0.0&&height>0.0)
|
|
for(auto& node:runtime.segmentNodes)
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
{
|
|
- const double scale=axis==2?height:radius;
|
|
+ const double scale=axis==2&&!passageMouthTube?height:radius;
|
|
node.minimumNormalized[axis]=node.minimum[axis]/scale;
|
|
node.maximumNormalized[axis]=node.maximum[axis]/scale;
|
|
}
|
|
@@ -5728,6 +5729,8 @@ public:
|
|
}
|
|
else if(feature.edge&&!feature.segments.empty())
|
|
{
|
|
+ if(IsPlannerPassageMouthContinuationEdge(*feature.edge))
|
|
+ return {0.0,0.0,false};
|
|
const double radius=feature.edge->boreRadiusMicrometres/1000000.0;
|
|
const double height=feature.edge->boreHeightMicrometres/1000000.0;
|
|
if(!(radius>0.0&&height>0.0))return {0.0,0.0,false};
|
|
@@ -7832,6 +7835,14 @@ private:
|
|
return composed;
|
|
}
|
|
|
|
+ static bool IsPlannerPassageMouthContinuationEdge(
|
|
+ const VoxelForgeCore::ResolvedCavernEdgeV1& edge)
|
|
+ {
|
|
+ const bool sharedPlannerEdge=edge.stableId.starts_with("tunnel-network/")
|
|
+ ||edge.stableId.starts_with("vertical-shafts/");
|
|
+ return sharedPlannerEdge&&edge.startAnchorStableId.ends_with("-mouth");
|
|
+ }
|
|
+
|
|
static double SmoothMinMetres(double a,double b,double width)
|
|
{
|
|
if(!(width>0.0))return std::min(a,b);
|
|
@@ -7997,9 +8008,11 @@ private:
|
|
double nearestFloor=0.0,nearestGrade=0.0,nearestRadial=0.0;
|
|
const double radius=feature.edge->boreRadiusMicrometres/1000000.0;
|
|
const double height=feature.edge->boreHeightMicrometres/1000000.0;
|
|
+ const bool passageMouthTube=
|
|
+ IsPlannerPassageMouthContinuationEdge(*feature.edge);
|
|
constexpr double SegmentDistanceTieToleranceMetres=1.0e-12;
|
|
if(!(radius>0.0&&height>0.0))continue;
|
|
- const double minScale=std::min(radius,height);
|
|
+ const double minScale=passageMouthTube?radius:std::min(radius,height);
|
|
std::uint32_t nearestSegment=std::numeric_limits<std::uint32_t>::max();
|
|
const auto evaluateSegment=[&](std::uint32_t segmentIndex)
|
|
{
|
|
@@ -8010,7 +8023,9 @@ private:
|
|
{
|
|
const double delta=segment.end[axis]-segment.start[axis];
|
|
lengthSquared+=delta*delta;
|
|
- dot+=(point[axis]-segment.start[axis])*delta;
|
|
+ const double query=axis==2&&passageMouthTube
|
|
+ ?point[axis]-radius:point[axis];
|
|
+ dot+=(query-segment.start[axis])*delta;
|
|
}
|
|
const double t=lengthSquared>0.0
|
|
?std::clamp(dot/lengthSquared,0.0,1.0):0.0;
|
|
@@ -8020,11 +8035,12 @@ private:
|
|
const double delta=point[axis]-(segment.start[axis]
|
|
+t*(segment.end[axis]-segment.start[axis]));
|
|
if(axis<2)radialSquared+=delta*delta;
|
|
- else vertical=delta;
|
|
+ else vertical=delta-(passageMouthTube?radius:0.0);
|
|
}
|
|
+ const double verticalScale=passageMouthTube?radius:height;
|
|
const double normalized=std::sqrt(radialSquared/(radius*radius)
|
|
- +vertical*vertical/(height*height));
|
|
- double distance=(normalized-1.0)*std::min(radius,height);
|
|
+ +vertical*vertical/(verticalScale*verticalScale));
|
|
+ double distance=(normalized-1.0)*minScale;
|
|
if(feature.irregularityAmplitudeVoxels>0.0f)
|
|
{
|
|
const float floorVoxels=static_cast<float>(nearestFloor/metresPerVoxel);
|
|
@@ -8049,8 +8065,10 @@ private:
|
|
?(segment.end[2]-segment.start[2])/horizontal:0.0;
|
|
}
|
|
};
|
|
+ const double verticalScale=passageMouthTube?radius:height;
|
|
const std::array<double,3> normalizedPoint={
|
|
- point[0]/radius,point[1]/radius,point[2]/height};
|
|
+ point[0]/radius,point[1]/radius,
|
|
+ (point[2]-(passageMouthTube?radius:0.0))/verticalScale};
|
|
const auto nodeLowerBound=[&](const FCavernRuntimeFeature::FSegmentNode& node)
|
|
{
|
|
double gapSquared=0.0;
|
|
diff --git a/Standalone/Source/WorldAssemblyPrototype.h b/Standalone/Source/WorldAssemblyPrototype.h
|
|
index 4e20068..0c3cbb7 100644
|
|
--- a/Standalone/Source/WorldAssemblyPrototype.h
|
|
+++ b/Standalone/Source/WorldAssemblyPrototype.h
|
|
@@ -1732,6 +1732,44 @@ namespace StandaloneWorldAssemblyPrototype
|
|
if(std::abs(a[axis]-b[axis])>2)return false;
|
|
return true;
|
|
};
|
|
+ const auto pointAlongEdge=[&](const ResolvedCavernEdgeV1& edge,
|
|
+ double distanceFromStart)
|
|
+ {
|
|
+ const auto evaluate=[&](double t)
|
|
+ {
|
|
+ const double one=1.0-t;
|
|
+ const std::array<double,4> weights={one*one*one,
|
|
+ 3.0*one*one*t,3.0*one*t*t,t*t*t};
|
|
+ std::array<double,3> point{};
|
|
+ for(std::size_t control=0;control<4;++control)
|
|
+ for(std::size_t axis=0;axis<3;++axis)
|
|
+ point[axis]+=weights[control]
|
|
+ *edge.controlPointsMicrometres[control][axis];
|
|
+ return point;
|
|
+ };
|
|
+ auto previous=evaluate(0.0);
|
|
+ double travelled=0.0;
|
|
+ constexpr int Samples=256;
|
|
+ for(int sample=1;sample<=Samples;++sample)
|
|
+ {
|
|
+ const auto next=evaluate(static_cast<double>(sample)/Samples);
|
|
+ const double segmentLength=std::hypot(next[0]-previous[0],
|
|
+ next[1]-previous[1])/1000000.0;
|
|
+ if(travelled+segmentLength>=distanceFromStart
|
|
+ &&segmentLength>1.0e-9)
|
|
+ {
|
|
+ const double t=std::clamp((distanceFromStart-travelled)
|
|
+ /segmentLength,0.0,1.0);
|
|
+ std::array<double,3> point{};
|
|
+ for(std::size_t axis=0;axis<3;++axis)
|
|
+ point[axis]=std::lerp(previous[axis],next[axis],t);
|
|
+ return point;
|
|
+ }
|
|
+ travelled+=segmentLength;
|
|
+ previous=next;
|
|
+ }
|
|
+ return evaluate(1.0);
|
|
+ };
|
|
auto start=arrival->lowerMouthSubvoxels;
|
|
appendRoutePoint("000-arrival",start);
|
|
for(std::size_t edgeIndex=0;edgeIndex<256;++edgeIndex)
|
|
@@ -1755,15 +1793,14 @@ namespace StandaloneWorldAssemblyPrototype
|
|
}
|
|
if(edgeIndex==0)
|
|
{
|
|
- auto inner=edge->controlPointsMicrometres.front();
|
|
- // Keep the exact authored mouth anchor and use the planner
|
|
- // edge's floor height for the first cave-side walking point.
|
|
- // Passage composition uses these same two points to blend the
|
|
- // floor profile into the native cave route.
|
|
- inner[2]+=PlayerCenterAboveFloorMicrometres;
|
|
+ const auto edgeStart=pointAlongEdge(*edge,2.0);
|
|
const std::array<std::int64_t,3> point={
|
|
- toCoreSubvoxels(inner[0]),toCoreSubvoxels(inner[1]),
|
|
- toCoreSubvoxels(inner[2])};
|
|
+ toCoreSubvoxels(static_cast<std::int64_t>(std::llround(edgeStart[0]))),
|
|
+ toCoreSubvoxels(static_cast<std::int64_t>(std::llround(edgeStart[1]))),
|
|
+ toCoreSubvoxels(static_cast<std::int64_t>(std::llround(edgeStart[2]
|
|
+ +PlayerCenterAboveFloorMicrometres)))};
|
|
+ // Keep a walking point on the actual edge curve so audits and
|
|
+ // passage floor blending follow the same mouth continuation.
|
|
if(!sameRoutePoint(point,start))
|
|
appendRoutePoint("000-edge-start",point);
|
|
}
|
|
@@ -1783,11 +1820,12 @@ namespace StandaloneWorldAssemblyPrototype
|
|
&&value.stableId==departureEdgeId;});
|
|
if(departureEdge==layout.cavernEdges.end())
|
|
{error="native cave route lost its planned departure edge";return false;}
|
|
- auto inner=departureEdge->controlPointsMicrometres.front();
|
|
- inner[2]+=PlayerCenterAboveFloorMicrometres;
|
|
+ const auto edgeInner=pointAlongEdge(*departureEdge,2.0);
|
|
const std::array<std::int64_t,3> innerPoint={
|
|
- toCoreSubvoxels(inner[0]),toCoreSubvoxels(inner[1]),
|
|
- toCoreSubvoxels(inner[2])};
|
|
+ toCoreSubvoxels(static_cast<std::int64_t>(std::llround(edgeInner[0]))),
|
|
+ toCoreSubvoxels(static_cast<std::int64_t>(std::llround(edgeInner[1]))),
|
|
+ toCoreSubvoxels(static_cast<std::int64_t>(std::llround(edgeInner[2]
|
|
+ +PlayerCenterAboveFloorMicrometres)))};
|
|
auto end=departure->upperMouthSubvoxels;
|
|
if(!sameRoutePoint(innerPoint,end))
|
|
appendRoutePoint("998-departure-inner",innerPoint);
|
|
diff --git a/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-floating-islands-style2-v238.baseline b/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-floating-islands-style2-v238.baseline
|
|
new file mode 100644
|
|
index 0000000..98d4cc8
|
|
--- /dev/null
|
|
+++ b/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-floating-islands-style2-v238.baseline
|
|
@@ -0,0 +1,12 @@
|
|
+[PipelineFullDetailHash] tiles=960 density=0x91931a57a067fd92 mesh=0xa818654be89790ac payload=0x370ff80451105a39 topology=0x06e507557aa70173
|
|
+[PipelineWholeViewHash] tiles=2370 density=0x7111b4b743e03c95 mesh=0x8c7240939db21b88 payload=0x3981ff5ca7076219 topology=0x07947d2e691b2e55
|
|
+[PipelineLODHash] lod=0 tiles=512 density=0x2fd4c78685a43cfe mesh=0x50478cff1c1f8ca0 payload=0x54e48e57e0af26de topology=0x860c011cfdb9833a
|
|
+[PipelineLODHash] lod=1 tiles=448 density=0x4b8d9a6dcd2942ad mesh=0x17e89b7087bfe711 payload=0x2389c109eb780db6 topology=0x7eccd4cf560d0630
|
|
+[PipelineLODHash] lod=2 tiles=320 density=0x48d4c21a852e6e21 mesh=0x5516a17618d142d5 payload=0x59cda38ab9ca6b7a topology=0x15ae353bd835e6a1
|
|
+[PipelineLODHash] lod=3 tiles=208 density=0x09b4811098a48c8d mesh=0xbac3fb6d54bca8e9 payload=0xca779555d55be411 topology=0xc63e8ee46890a03a
|
|
+[PipelineLODHash] lod=4 tiles=240 density=0xfb11591772a3d18e mesh=0xebf2baee8764591b payload=0xe491d7ac9c0061ad topology=0xd618ac33b95ce494
|
|
+[PipelineLODHash] lod=5 tiles=144 density=0x2f8b7b7fb1e67258 mesh=0x4cd508cbe97344fd payload=0x2b6e96a6050458c9 topology=0x4ff6d0eda8988573
|
|
+[PipelineLODHash] lod=6 tiles=96 density=0x0114a34c8b8d6113 mesh=0xb1fec4d01ae1fa44 payload=0x3e50cfd429b8e950 topology=0x08090c96d15c1618
|
|
+[PipelineLODHash] lod=7 tiles=96 density=0x3836ed8cfce73418 mesh=0x40bc6589c5fc1aac payload=0x677349bcae16790f topology=0x36e3aa53192e6a6b
|
|
+[PipelineLODHash] lod=8 tiles=306 density=0x144f3c1fd722708b mesh=0xef77b28bf7fd9453 payload=0xe3f47b37fcd61489 topology=0x5a3c73e295494e04
|
|
+[PipelineExactQueryHash] probes=9 density=0x7b7c50ac24ccadcc unclamped=0x7b7c50ac24ccadcc detail=0xefcba20c0c70c415 collision=0xe604823a249029bf path=direct-Core-queries
|
|
diff --git a/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-surface-world-style1-v238.baseline b/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-surface-world-style1-v238.baseline
|
|
new file mode 100644
|
|
index 0000000..e43aaf0
|
|
--- /dev/null
|
|
+++ b/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-surface-world-style1-v238.baseline
|
|
@@ -0,0 +1,12 @@
|
|
+[PipelineFullDetailHash] tiles=960 density=0xace56c8f205238ad mesh=0xa9c603a1248de6b8 payload=0xea55f1f4fcc6a894 topology=0x5ea7bc78a1b678ae
|
|
+[PipelineWholeViewHash] tiles=2370 density=0x826704417f556bb8 mesh=0x3dc31324d910ad2b payload=0x32cd1847a8e05d0a topology=0xb800610aa85336d7
|
|
+[PipelineLODHash] lod=0 tiles=512 density=0xcd6ab28cfa504aca mesh=0xf03dc80a669fca31 payload=0x397aded1c3d2d297 topology=0xb4a1dc84fabc1fe3
|
|
+[PipelineLODHash] lod=1 tiles=448 density=0xce6e713828b2a20a mesh=0x1cf408c25d67d18c payload=0x8a2b5d20571c997e topology=0x51cd985fe8e7d100
|
|
+[PipelineLODHash] lod=2 tiles=320 density=0xd7f003d75d73e5b1 mesh=0x4e9c56918da506ae payload=0x3867c5915b409d6d topology=0x02a2a7c0ee9ac809
|
|
+[PipelineLODHash] lod=3 tiles=208 density=0xce02d09814028b15 mesh=0xe4c2addc4c54c439 payload=0x7e25d9eee6469c5a topology=0xe603e61c9b9bb733
|
|
+[PipelineLODHash] lod=4 tiles=240 density=0x0be8798407724eaf mesh=0x131c303cbbe51434 payload=0x2a90ecf0a2411ca0 topology=0xe3e054a371152e08
|
|
+[PipelineLODHash] lod=5 tiles=144 density=0xf20897ad272103a9 mesh=0x13d00b6253ae1f72 payload=0x0db3880693a962fa topology=0x462bd87a80f15e89
|
|
+[PipelineLODHash] lod=6 tiles=96 density=0xc040fcac457d6313 mesh=0x1ed132eeb718fb62 payload=0xcd0214e35577c6e3 topology=0x2c56c69e6fdcfd64
|
|
+[PipelineLODHash] lod=7 tiles=96 density=0xc2d419446355fdb8 mesh=0x4ea44452574fc84b payload=0xfd9779bedfc9ae0f topology=0x8948546e770c9b72
|
|
+[PipelineLODHash] lod=8 tiles=306 density=0x62db31696f2da831 mesh=0xd373f9e9481532fe payload=0x540a60526e0e3e46 topology=0xc725cb901e6dc335
|
|
+[PipelineExactQueryHash] probes=9 density=0xc949c4df2ce6f9d9 unclamped=0xc949c4df2ce6f9d9 detail=0x114c501e2d830d94 collision=0xe6011d3a248d4849 path=direct-Core-queries
|
|
diff --git a/Standalone/Tools/verify/verify.py b/Standalone/Tools/verify/verify.py
|
|
index e97c250..12f4405 100644
|
|
--- a/Standalone/Tools/verify/verify.py
|
|
+++ b/Standalone/Tools/verify/verify.py
|
|
@@ -125,11 +125,11 @@ RECIPE_RE = re.compile(
|
|
|
|
def owner_hash_baseline(recipe: str, style: int) -> tuple[Path, str]:
|
|
if recipe == "surface-world" and style == 1:
|
|
- return (BASELINES / "owner-hash/canonical/owner-hash-surface-world-style1-v237.baseline",
|
|
- "L139-v237-vertical-shafts-core-planner")
|
|
+ return (BASELINES / "owner-hash/canonical/owner-hash-surface-world-style1-v238.baseline",
|
|
+ "L149-v238-passage-mouth-tube-joins")
|
|
if recipe == "floating-islands" and style == 2:
|
|
- return (BASELINES / "owner-hash/canonical/owner-hash-floating-islands-style2-v237.baseline",
|
|
- "L139-v237-vertical-shafts-core-planner")
|
|
+ return (BASELINES / "owner-hash/canonical/owner-hash-floating-islands-style2-v238.baseline",
|
|
+ "L149-v238-passage-mouth-tube-joins")
|
|
raise VerificationFailure(f"no pinned owner hash baseline for {recipe} style {style}")
|
|
|
|
|