Files
VoxelForge/Standalone/Research/WorldAssemblerM3/Linux149/l149-one-tube-mouth.patch
T
Fr0zkaandClaude Opus 5.5 b8382eed84 research: one continuous tube through the mouth (L149, 0/16; kept as patch)
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
2026-11-01 15:24:49 +01:00

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}")