From 4a97023b24253d325374fb480bf23df2a6b926ab Mon Sep 17 00:00:00 2001 From: Fr0zka Date: Sat, 31 Oct 2026 12:03:48 +0100 Subject: [PATCH] wip: continuous backbone floor profile for the cave planner (L134 design 4, untested) Written by Luna before the Codex usage limit cut the round: one shared distance/floor profile across backbone rooms and edges, edge cubic floors derived from it, sloped route-room floors, lower-floor backing cap at overlaps, and validator checks for joint height/slope continuity. Not yet built or audited. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1 --- .../include/VoxelForgeCore/VoxelForgeCore.h | 16 +- Standalone/Core/src/ResolvedLayoutInternal.h | 242 +++++++++++++++++- Standalone/Core/src/ShapeWorldInternal.h | 57 ++++- 3 files changed, 296 insertions(+), 19 deletions(-) diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h index df0ab7c..ade7a4e 100644 --- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h @@ -497,8 +497,9 @@ namespace VoxelForgeCore // 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. Version 228 composes deterministic cave - // graphs from fixed passage mouths and the owner Tunnel Network settings. - inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 229; + // graphs from fixed passage mouths and the owner Tunnel Network settings. Version 229 + // budgets cave floor support; 230 records a shared backbone floor profile and joint cap. + inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 230; enum class FieldChannel : std::uint32_t { @@ -893,6 +894,7 @@ namespace VoxelForgeCore inline constexpr std::uint32_t ResolvedLayoutStampVocabularyVersionV2 = 2; inline constexpr std::uint32_t ResolvedLayoutStampVocabularyVersionV3 = 3; inline constexpr std::uint32_t ResolvedLayoutCavernGraphVocabularyVersionV1 = 1; + inline constexpr std::uint32_t ResolvedLayoutCavernGraphVocabularyVersionV2 = 2; inline constexpr std::uint32_t ResolvedLayoutOreVeinVocabularyVersionV1 = 1; enum class ResolvedLayoutValidationModeV3 : std::uint8_t { compatibility, strict }; @@ -957,6 +959,11 @@ namespace VoxelForgeCore std::array centerMicrometres{}; std::array halfExtentsMicrometres{}; std::int64_t floorMicrometres = 0; + // Optional shared backbone profile reference. The room floor plane is + // floorMicrometres plus this Q16 XY gradient from centerMicrometres. + std::string floorProfileStableId; + std::int64_t floorProfileDistanceMicrometres = -1; + std::array floorGradientQ16{}; std::int64_t smoothWidthMicrometres = 0; // Optional feature-to-feature union width. Zero preserves the historical // behavior where smoothWidthMicrometres also controls the graph union. @@ -976,6 +983,11 @@ namespace VoxelForgeCore std::int32_t strateIndex = -1; // A single cubic Bezier in XY. Its four Z values are the cubic floor profile. std::array, 4> controlPointsMicrometres{}; + // Edges with the same profile ID are adjacent pieces of one shared h(s). + // Distances are signed along that profile, so a terminal edge may be reversed. + std::string floorProfileStableId; + std::int64_t floorProfileStartDistanceMicrometres = -1; + std::int64_t floorProfileEndDistanceMicrometres = -1; // Resolve-time, error-bounded approximation of the cubic centerline. Each // consecutive pair is a physical segment used by the native cave field. std::vector> polylineMicrometres; diff --git a/Standalone/Core/src/ResolvedLayoutInternal.h b/Standalone/Core/src/ResolvedLayoutInternal.h index 2b0449f..76b1d41 100644 --- a/Standalone/Core/src/ResolvedLayoutInternal.h +++ b/Standalone/Core/src/ResolvedLayoutInternal.h @@ -621,6 +621,10 @@ namespace <<"],\"half_extents_um\":["<0) @@ -647,6 +651,11 @@ namespace out<<'['<0 &&room.irregularityWavelengthMicrometres<=0)) add("cavern_room_parameters",room.stableId+" has invalid or unbounded parameters"); + const std::int64_t gradientSquared=static_cast(room.floorGradientQ16[0]) + *room.floorGradientQ16[0]+static_cast(room.floorGradientQ16[1]) + *room.floorGradientQ16[1]; + if((room.floorProfileStableId.empty() + &&room.floorProfileDistanceMicrometres!=-1) + ||(!room.floorProfileStableId.empty() + &&room.floorProfileDistanceMicrometres<0) + ||gradientSquared>static_cast(13108)*13108) + add("cavern_room_floor_profile",room.stableId+ + " has an invalid shared profile reference or floor grade"); const std::int64_t pad=room.smoothWidthMicrometres +room.unionWidthMicrometres+room.irregularityAmplitudeMicrometres; for(std::size_t axis=0;axis<3;++axis) @@ -1308,6 +1328,15 @@ bool ResolvedLayoutSnapshotV3::validate(ResolvedLayoutValidationModeV3 mode, ||(edge.irregularityAmplitudeMicrometres>0 &&edge.irregularityWavelengthMicrometres<=0)) add("cavern_edge_parameters",edge.stableId+" has invalid or unbounded parameters"); + const bool hasFloorProfile=!edge.floorProfileStableId.empty(); + if((hasFloorProfile&&(edge.floorProfileStartDistanceMicrometres<0 + ||edge.floorProfileEndDistanceMicrometres<0 + ||edge.floorProfileStartDistanceMicrometres + ==edge.floorProfileEndDistanceMicrometres)) + ||(!hasFloorProfile&&(edge.floorProfileStartDistanceMicrometres!=-1 + ||edge.floorProfileEndDistanceMicrometres!=-1))) + add("cavern_edge_floor_profile",edge.stableId+ + " has an invalid shared profile distance interval"); std::array low=edge.controlPointsMicrometres[0]; std::array high=low; if(nativeLattice&&!edge.polylineMicrometres.empty()) @@ -1363,6 +1392,114 @@ bool ResolvedLayoutSnapshotV3::validate(ResolvedLayoutValidationModeV3 mode, add("cavern_edge_polyline",edge.stableId+ " has no bounded resolve-time polyline with matching endpoints"); } + std::vector floorProfileIds; + for(const auto& edge:cavernEdges) + if(!edge.floorProfileStableId.empty() + &&std::find(floorProfileIds.begin(),floorProfileIds.end(), + edge.floorProfileStableId)==floorProfileIds.end()) + floorProfileIds.push_back(edge.floorProfileStableId); + for(const std::string& profileId:floorProfileIds) + { + std::vector profileEdges; + for(const auto& edge:cavernEdges) + if(edge.floorProfileStableId==profileId)profileEdges.push_back(&edge); + std::sort(profileEdges.begin(),profileEdges.end(),[](const auto* a,const auto* b) + { + return std::min(a->floorProfileStartDistanceMicrometres, + a->floorProfileEndDistanceMicrometres) + floorProfileStartDistanceMicrometres, + b->floorProfileEndDistanceMicrometres); + }); + const auto endpointZ=[](const ResolvedCavernEdgeV1& edge,bool atMinimum) + { + const bool forward=edge.floorProfileStartDistanceMicrometres + (edge.floorProfileEndDistanceMicrometres + -edge.floorProfileStartDistanceMicrometres); + if(span==0.0)return std::numeric_limits::infinity(); + const bool forward=edge.floorProfileStartDistanceMicrometres + floorProfileStartDistanceMicrometres, + edge->floorProfileEndDistanceMicrometres); + const std::int64_t high=std::max(edge->floorProfileStartDistanceMicrometres, + edge->floorProfileEndDistanceMicrometres); + const double slope0=endpointSlope(*edge,true); + const double slope1=endpointSlope(*edge,false); + if(std::max(std::abs(slope0),std::abs(slope1))>0.200001) + add("cavern_floor_profile_grade",edge->stableId+ + " exceeds the shared backbone 1:5 grade limit"); + if(previous) + { + const std::int64_t previousHigh=std::max( + previous->floorProfileStartDistanceMicrometres, + previous->floorProfileEndDistanceMicrometres); + if(low!=previousHigh + ||std::abs(endpointZ(*previous,false)-endpointZ(*edge,true))>1000 + ||std::abs(endpointSlope(*previous,false)-slope0)>0.000001) + add("cavern_floor_profile_joint",profileId+ + " has a gap, height step, or slope step between adjacent edges"); + } + previous=edge; + (void)high; + } + for(const auto& room:cavernRooms) + { + if(room.floorProfileStableId!=profileId)continue; + const ResolvedCavernEdgeV1* containing=nullptr; + for(const auto* edge:profileEdges) + { + const auto low=std::min(edge->floorProfileStartDistanceMicrometres, + edge->floorProfileEndDistanceMicrometres); + const auto high=std::max(edge->floorProfileStartDistanceMicrometres, + edge->floorProfileEndDistanceMicrometres); + if(room.floorProfileDistanceMicrometres>=low + &&room.floorProfileDistanceMicrometres<=high) + {containing=edge;break;} + } + if(!containing) + { + add("cavern_room_floor_profile",room.stableId+ + " does not lie on its referenced continuous floor profile"); + continue; + } + const auto& edge=*containing; + const double t=static_cast(room.floorProfileDistanceMicrometres + -edge.floorProfileStartDistanceMicrometres) + /(edge.floorProfileEndDistanceMicrometres + -edge.floorProfileStartDistanceMicrometres); + const double one=1.0-t; + const double expected=one*one*one*edge.controlPointsMicrometres[0][2] + +3.0*one*one*t*edge.controlPointsMicrometres[1][2] + +3.0*one*t*t*edge.controlPointsMicrometres[2][2] + +t*t*t*edge.controlPointsMicrometres[3][2]; + std::array routeXY{}; + for(std::size_t axis=0;axis<2;++axis) + routeXY[axis]=one*one*one*edge.controlPointsMicrometres[0][axis] + +3.0*one*one*t*edge.controlPointsMicrometres[1][axis] + +3.0*one*t*t*edge.controlPointsMicrometres[2][axis] + +t*t*t*edge.controlPointsMicrometres[3][axis]; + const double roomFloorAtProfile=room.floorMicrometres + +room.floorGradientQ16[0]/65536.0 + *(routeXY[0]-room.centerMicrometres[0]) + +room.floorGradientQ16[1]/65536.0 + *(routeXY[1]-room.centerMicrometres[1]); + if(std::abs(expected-roomFloorAtProfile)>1000.0) + add("cavern_room_floor_profile",room.stableId+ + " floor does not match its referenced h(s)"); + } + } } if(oreVeins.empty()) { @@ -1526,6 +1663,12 @@ std::optional ResolvedLayoutSnapshotV3::FromJson( readTriplet("bounds_min_um",room.boundsMinimumMicrometres); readTriplet("bounds_max_um",room.boundsMaximumMicrometres); room.floorMicrometres=LayoutInt(v,"floor_um"); + room.floorProfileStableId=LayoutString(v,"floor_profile_id"); + if(v.Get("floor_profile_distance_um").IsNumber()) + room.floorProfileDistanceMicrometres=LayoutInt(v,"floor_profile_distance_um"); + const auto& floorGradient=v.Get("floor_gradient_q16").AsArray(); + for(std::size_t axis=0;axis<2&&axis(floorGradient[axis].Integer()); room.smoothWidthMicrometres=LayoutInt(v,"smooth_width_um"); room.unionWidthMicrometres=LayoutInt(v,"union_width_um"); room.irregularityAmplitudeMicrometres=LayoutInt(v,"irregularity_amplitude_um"); @@ -1539,6 +1682,13 @@ std::optional ResolvedLayoutSnapshotV3::FromJson( edge.startAnchorStableId=LayoutString(v,"start_anchor_stable_id"); edge.endRoomStableId=LayoutString(v,"end_room_stable_id"); edge.strateIndex=static_cast(LayoutInt(v,"strate_index")); + edge.floorProfileStableId=LayoutString(v,"floor_profile_id"); + if(v.Get("floor_profile_start_distance_um").IsNumber()) + edge.floorProfileStartDistanceMicrometres= + LayoutInt(v,"floor_profile_start_distance_um"); + if(v.Get("floor_profile_end_distance_um").IsNumber()) + edge.floorProfileEndDistanceMicrometres= + LayoutInt(v,"floor_profile_end_distance_um"); const auto& points=v.Get("control_points_um").AsArray(); for(std::size_t i=0;i(distance,0,totalRouteLength); + return routeStart[2]+dz*distance/totalRouteLength; + }; + const auto routeFloorGradientAt=[&](std::int64_t distance) + { + distance=std::clamp(distance,0,totalRouteLength); + std::size_t segment=1; + while(segmentcumulative[segment])++segment; + const auto unitTangent=[&](std::size_t index) + { + const double tx=static_cast(routePoints[index][0] + -routePoints[index-1][0]); + const double ty=static_cast(routePoints[index][1] + -routePoints[index-1][1]); + const double length=std::hypot(tx,ty); + return length>0.0?std::array{tx/length,ty/length} + :std::array{0.0,0.0}; + }; + auto tangent=unitTangent(segment); + if(segment+11.0e-12){tangent[0]/=length;tangent[1]/=length;} + } + constexpr double Q16=65536.0; + const double profileSlope=static_cast(dz)/totalRouteLength; + return std::array{ + static_cast(std::llround(tangent[0]*profileSlope*Q16)), + static_cast(std::llround(tangent[1]*profileSlope*Q16))}; + }; const auto pointAt=[&](std::int64_t distance) { distance=std::clamp(distance,0,totalRouteLength); @@ -2211,7 +2397,7 @@ bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout, -routePoints[segment-1][axis]; point[axis]=routePoints[segment-1][axis]+delta*local/segmentLength; } - point[2]=routeStart[2]+dz*distance/totalRouteLength; + point[2]=routeFloorAt(distance); return point; }; std::int64_t spacing=std::min(std::max(settings.roomSpacingMicrometres, @@ -2281,6 +2467,9 @@ bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout, { std::string id; FPoint point{}; + std::string floorProfileStableId; + std::int64_t floorProfileDistanceMicrometres=-1; + std::array floorGradientQ16{}; std::int64_t radiusMicrometres=0; bool destination=false; }; @@ -2298,6 +2487,9 @@ bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout, { const std::int64_t distance=nodeDistances[nodeIndex]; FRoomNode node;node.point=pointAt(distance); + node.floorProfileStableId=backboneFloorProfileId; + node.floorProfileDistanceMicrometres=distance; + node.floorGradientQ16=routeFloorGradientAt(distance); node.destination=distance==mainRouteMidpoint; node.id="tunnel-network/strate-"+std::to_string(strateIndex)+"/room-" +std::to_string(nodeIndex); @@ -2347,7 +2539,10 @@ bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout, 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.strateIndex=strateIndex; + room.floorProfileStableId=node.floorProfileStableId; + room.floorProfileDistanceMicrometres=node.floorProfileDistanceMicrometres; + room.floorGradientQ16=node.floorGradientQ16; room.halfExtentsMicrometres={halfX,halfY,halfZ}; room.centerMicrometres={node.point[0]+(-pathAxisY)*centerOffset/routeLengthXY, node.point[1]+pathAxisX*centerOffset/routeLengthXY, @@ -2362,6 +2557,11 @@ bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout, %static_cast(jitterRange+1)):0; room.centerMicrometres[2]+=ceilingJitter; } + room.floorMicrometres=node.point[2] + -(static_cast(room.floorGradientQ16[0]) + *(room.centerMicrometres[0]-node.point[0]) + +static_cast(room.floorGradientQ16[1]) + *(room.centerMicrometres[1]-node.point[1]))/65536; room.smoothWidthMicrometres=smooth; room.irregularityAmplitudeMicrometres=irregularity; room.irregularityWavelengthMicrometres=settings.irregularityWavelengthMicrometres; @@ -2374,7 +2574,8 @@ bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout, 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) + const FPoint& start,const FRoomNode& endNode,std::int64_t boreRadius, + std::int64_t profileStartDistance,std::int64_t profileEndDistance) { ResolvedCavernEdgeV1 edge;edge.stableId=std::move(id); edge.startAnchorStableId=std::move(startAnchor); @@ -2382,6 +2583,12 @@ bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout, edge.boreRadiusMicrometres=boreRadius; edge.boreHeightMicrometres=std::max(PlannedBoreHeight,MinimumBoreHeight); edge.floorSupportDepthMicrometres=settings.floorSupportDepthMicrometres; + if(profileStartDistance>=0&&profileEndDistance>=0) + { + edge.floorProfileStableId=backboneFloorProfileId; + edge.floorProfileStartDistanceMicrometres=profileStartDistance; + edge.floorProfileEndDistanceMicrometres=profileEndDistance; + } edge.smoothWidthMicrometres=smooth; edge.irregularityAmplitudeMicrometres=irregularity; edge.irregularityWavelengthMicrometres=settings.irregularityWavelengthMicrometres; @@ -2394,10 +2601,21 @@ bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout, edge.controlPointsMicrometres[2][axis]=start[axis]+(delta*2)/3; edge.controlPointsMicrometres[3][axis]=end[axis]; } + if(profileStartDistance>=0&&profileEndDistance>=0) + { + const std::int64_t floorStart=routeFloorAt(profileStartDistance); + const std::int64_t floorEnd=routeFloorAt(profileEndDistance); + const std::int64_t floorDelta=floorEnd-floorStart; + edge.controlPointsMicrometres[0][2]=floorStart; + edge.controlPointsMicrometres[1][2]=floorStart+floorDelta/3; + edge.controlPointsMicrometres[2][2]=floorStart+(floorDelta*2)/3; + edge.controlPointsMicrometres[3][2]=floorEnd; + } return edge; }; const auto arrivalPoint=routeStart; FPoint previousPoint=arrivalPoint; + std::int64_t previousProfileDistance=0; std::string previousAnchor=arrivalPassage->stableId+"/lower-mouth"; for(std::size_t nodeIndex=0;nodeIndexstableId+"/upper-mouth", - routeEnd,nodes.back(),passageBoreRadius(*departurePassage,false))); + routeEnd,nodes.back(),passageBoreRadius(*departurePassage,false), + totalRouteLength,nodes.back().floorProfileDistanceMicrometres)); } const std::int64_t roomDensity=clampQ16(settings.roomDensityQ16); @@ -2444,16 +2665,19 @@ bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout, branch.point={parent.point[0]+(-pathAxisY)*branchLength/pathAxisLength*direction, parent.point[1]+pathAxisX*branchLength/pathAxisLength*direction, parent.point[2]}; + branch.floorProfileStableId=parent.floorProfileStableId; + branch.floorProfileDistanceMicrometres=parent.floorProfileDistanceMicrometres; + branch.floorGradientQ16=parent.floorGradientQ16; makeCompound(branch,false,rooms); const std::string branchEdgeId="tunnel-network/strate-" +std::to_string(strateIndex)+"/side-edge-"+std::to_string(branchCount); ResolvedCavernEdgeV1 branchEdge; if(settings.tunnelsFlowTowardOrigin) branchEdge=makeEdge(branchEdgeId,componentRoomId(branch), - branch.point,parent,selectedBoreRadius(branchEdgeId)); + branch.point,parent,selectedBoreRadius(branchEdgeId),-1,-1); else branchEdge=makeEdge(branchEdgeId,componentRoomId(parent), - parent.point,branch,selectedBoreRadius(branchEdgeId)); + parent.point,branch,selectedBoreRadius(branchEdgeId),-1,-1); // Side branches retain their cave floor SDF, but they do not cap the // descending backbone when their tubes overlap it near a junction. branchEdge.floorSupportDepthMicrometres=0; diff --git a/Standalone/Core/src/ShapeWorldInternal.h b/Standalone/Core/src/ShapeWorldInternal.h index 41a3fea..02f74b6 100644 --- a/Standalone/Core/src/ShapeWorldInternal.h +++ b/Standalone/Core/src/ShapeWorldInternal.h @@ -3583,6 +3583,7 @@ private: double minimumFloorMetres=std::numeric_limits::infinity(); double maximumFloorMetres=-std::numeric_limits::infinity(); double maximumSegmentGrade=0.0; + std::array floorGradient{}; std::array centerVoxels{},halfExtentsVoxels{}; std::array,4> controlPointsVoxels{}; float floorVoxels=0.0f; @@ -4011,8 +4012,10 @@ private: { CavernRuntime.clear();CavernSpatialNodes.clear();CavernSpatialOrder.clear(); if(!ResolvedLayout||!Owner||Owner->CoreVoxelSizeMicrons==0 - ||ResolvedLayout->cavernGraphVocabularyVersion - !=VoxelForgeCore::ResolvedLayoutCavernGraphVocabularyVersionV1)return; + ||(ResolvedLayout->cavernGraphVocabularyVersion + !=VoxelForgeCore::ResolvedLayoutCavernGraphVocabularyVersionV1 + &&ResolvedLayout->cavernGraphVocabularyVersion + !=VoxelForgeCore::ResolvedLayoutCavernGraphVocabularyVersionV2))return; const auto isNativeGraphStrate=[this](std::int32_t strateIndex) { const auto found=std::find_if(ResolvedLayout->strates.begin(), @@ -4042,6 +4045,8 @@ private: } runtime.floorVoxels=static_cast( static_cast(room.floorMicrometres)/micronsPerVoxel); + runtime.floorGradient={room.floorGradientQ16[0]/65536.0, + room.floorGradientQ16[1]/65536.0}; runtime.smoothWidthVoxels=static_cast( static_cast(room.smoothWidthMicrometres)/micronsPerVoxel); runtime.irregularityAmplitudeVoxels=static_cast( @@ -5314,7 +5319,18 @@ public: (std::sqrt(normalizedMinimumSquared)-1.0)*meanRadius, (std::sqrt(normalizedMaximumSquared)-1.0)*meanRadius,true}; const double floorZ=feature.room->floorMicrometres/1000000.0; - const FMetricInterval floor={floorZ-hi[2],floorZ-lo[2],true}; + const double centerX=feature.room->centerMicrometres[0]/1000000.0; + const double centerY=feature.room->centerMicrometres[1]/1000000.0; + const double gx=feature.room->floorGradientQ16[0]/65536.0; + const double gy=feature.room->floorGradientQ16[1]/65536.0; + const double x0=lo[0]-centerX,x1=hi[0]-centerX; + const double y0=lo[1]-centerY,y1=hi[1]-centerY; + const double gradientMinimum=(gx>=0.0?gx*x0:gx*x1) + +(gy>=0.0?gy*y0:gy*y1); + const double gradientMaximum=(gx>=0.0?gx*x1:gx*x0) + +(gy>=0.0?gy*y1:gy*y0); + const FMetricInterval floor={floorZ-hi[2]+gradientMinimum, + floorZ-lo[2]+gradientMaximum,true}; const double width=feature.room->smoothWidthMicrometres/1000000.0; featureVoid={SmoothMaxMetres(ellipsoid.minimum,floor.minimum,width), SmoothMaxMetres(ellipsoid.maximum,floor.maximum,width),true}; @@ -7463,6 +7479,7 @@ private: double floorSupportScore=std::numeric_limits::infinity(); double floorSupportDepth=0.0,floorSupportFloor=0.0,floorSupportGrade=0.0; const std::string* floorSupportEdgeId=nullptr; + const std::string* floorSupportProfileId=nullptr; for(const std::uint32_t index:*candidates) { if(index>=CavernRuntime.size())continue; @@ -7491,7 +7508,9 @@ private: feature.irregularityWavelengthVoxels,feature.irregularitySeed) *feature.irregularityAmplitudeVoxels*metresPerVoxel*fade; } - const double floor=feature.room->floorMicrometres/1000000.0-point[2]; + const double floor=feature.room->floorMicrometres/1000000.0-point[2] + +feature.floorGradient[0]*(point[0]-feature.centerMetres[0]) + +feature.floorGradient[1]*(point[1]-feature.centerMetres[1]); const double width=feature.room->smoothWidthMicrometres/1000000.0; featureVoid=SmoothMaxMetres(ellipsoid,floor,width); } @@ -7599,10 +7618,32 @@ private: { const double deltaZ=point[2]-nearestFloor; const double candidateScore=nearestRadial*nearestRadial+deltaZ*deltaZ; - if(candidateScorestableId<*floorSupportEdgeId))) + if(!feature.edge->floorProfileStableId.empty()) + { + const bool sameProfile=floorSupportProfileId + &&*floorSupportProfileId==feature.edge->floorProfileStableId; + if(!floorSupportProfileId||sameProfile) + { + // Backbone edges share h(s). At a geometric joint their + // cave volumes overlap, so the backing top is the lower + // of the local floors, never the closer higher segment. + if(!floorSupportProfileId||nearestFloorstableId<*floorSupportEdgeId)) + { + floorSupportDepth=candidateSupportDepth; + floorSupportFloor=nearestFloor; + floorSupportGrade=nearestGrade; + floorSupportEdgeId=&feature.edge->stableId; + } + floorSupportProfileId=&feature.edge->floorProfileStableId; + } + } + else if(!floorSupportProfileId + &&(candidateScorestableId<*floorSupportEdgeId)))) { floorSupportScore=candidateScore; floorSupportDepth=candidateSupportDepth;