diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h index 8c44461..7ddad4e 100644 --- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h @@ -510,8 +510,8 @@ namespace VoxelForgeCore // preserves open cave lanes through passage shell overlaps; 236 binds passage mouths to // 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 plans one resolved passage spline with explicit landing chambers. + inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 238; enum class FieldChannel : std::uint32_t { @@ -1209,6 +1209,16 @@ namespace VoxelForgeCore std::int64_t radiusSubvoxels = 0; }; + struct ResolvedLayoutPassageLandingChamberV1 + { + // The chamber floor plane passes through this point. Gradient is dz/dx, + // dz/dy in Q16 so the chamber continues the passage's authored h(s) grade. + std::array floorCenterSubvoxels{}; + std::array floorGradientQ16{}; + std::int64_t radiusSubvoxels = 0; + std::int64_t clearHeightSubvoxels = 0; + }; + struct ResolvedLayoutPassageV1 { std::string stableId; @@ -1223,6 +1233,8 @@ namespace VoxelForgeCore std::int64_t midRadiusSubvoxels = 0; // Derived from the mouth binding; the main descent is monotone through this point. std::size_t lowerMouthControlIndex = 0; + ResolvedLayoutPassageLandingChamberV1 upperLandingChamber; + ResolvedLayoutPassageLandingChamberV1 lowerLandingChamber; std::vector controlPoints; }; diff --git a/Standalone/Core/src/ResolvedLayoutInternal.h b/Standalone/Core/src/ResolvedLayoutInternal.h index 7b7520a..a244131 100644 --- a/Standalone/Core/src/ResolvedLayoutInternal.h +++ b/Standalone/Core/src/ResolvedLayoutInternal.h @@ -565,7 +565,21 @@ namespace <<"],\"lower_mouth_q\":["<& mouth,const char* side) + { + const double playerCenterHeight=(side==std::string_view("upper") + &&host.generatorKind=="SurfaceWorld")?0.98:1.13; + const std::int64_t playerCenterHeightQ=static_cast( + std::llround(playerCenterHeight*WorldAssemblySubvoxelsPerVoxel + *1000000.0/std::max(1,coreVoxelSizeMicrometres))); + if(chamber.floorCenterSubvoxels[0]!=mouth[0] + ||chamber.floorCenterSubvoxels[1]!=mouth[1] + ||chamber.floorCenterSubvoxels[2]+playerCenterHeightQ!=mouth[2] + ||chamber.radiusSubvoxels<=0 + ||chamber.clearHeightSubvoxels<=0 + ||chamber.radiusSubvoxels( + std::llround(2.2*WorldAssemblySubvoxelsPerVoxel*1000000.0 + /std::max(1,coreVoxelSizeMicrometres)))) + { + add("passage_landing_chamber",passage.stableId+" "+side + +" landing chamber has invalid floor, radius, or capsule clearance"); + return; + } + const std::uint64_t gradientSquared=static_cast( + chamber.floorGradientQ16[0]*chamber.floorGradientQ16[0] + +chamber.floorGradientQ16[1]*chamber.floorGradientQ16[1]); + const std::int64_t gradientMagnitude=static_cast( + PassageIsqrt(gradientSquared)); + if(gradientMagnitude>17560) + add("passage_landing_grade",passage.stableId+" "+side + +" landing chamber floor exceeds 15 degrees"); + const std::int64_t planeReachQ=static_cast( + (chamber.radiusSubvoxels*gradientMagnitude+65535)/65536); + const std::int64_t shellQ=static_cast(std::llround( + 0.75*WorldAssemblySubvoxelsPerVoxel*1000000.0 + /std::max(1,coreVoxelSizeMicrometres))); + const auto qToUm=[&](std::int64_t q) + {return q*static_cast(coreVoxelSizeMicrometres) + /WorldAssemblySubvoxelsPerVoxel;}; + if(!host.content.presentInCurrentField + ||qToUm(chamber.floorCenterSubvoxels[2]-planeReachQ-shellQ) + host.content.topMicrometres) + add("passage_landing_content",passage.stableId+" "+side + +" landing chamber or plain shell leaves its host content band"); + }; + if(upper&&lower) + { + validateLandingChamber(passage.upperLandingChamber,*upper, + passage.upperMouthSubvoxels,"upper"); + validateLandingChamber(passage.lowerLandingChamber,*lower, + passage.lowerMouthSubvoxels,"lower"); + } if(passage.controlPoints.size()<2)continue; const std::int64_t PlayerCenterAboveFloorQ=coreVoxelSizeMicrometres>0 ?static_cast(std::llround(1.13*WorldAssemblySubvoxelsPerVoxel @@ -1730,6 +1800,21 @@ std::optional ResolvedLayoutSnapshotV3::FromJson( const auto& lower=v.Get("lower_mouth_q").AsArray(); for(std::size_t axis=0;axis<3&&axis(layout.coreVoxelSizeMicrometres) /WorldAssemblySubvoxelsPerVoxel;}; - const std::int64_t mouthBoreRadius=std::clamp( - std::max(MinimumBoreRadius,settings.minimumTunnelRadiusMicrometres), - MinimumBoreRadius,MaximumBoreRadius); + constexpr std::int64_t LandingConnectorBoreRadius=2000000; + const std::int64_t mouthBoreRadius=LandingConnectorBoreRadius; const auto chooseMouthRouteTangent=[&](const ResolvedLayoutPassageV1& passage, std::size_t mouthIndex,bool lowerSide, const std::array& authoredRouteTangentQ16, - std::int64_t authoredFloorSlopeQ16,const FPoint& mouthFloorPoint) + std::int64_t authoredFloorSlopeQ16,const FPoint& mouthFloorPoint, + std::int64_t chamberRadiusMicrometres) { constexpr std::array,16> DirectionsQ16={{ {{65536,0}},{{60547,25080}},{{46341,46341}},{{25080,60547}}, @@ -2418,7 +2502,9 @@ bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout, +outwardQ16[1]*authoredRouteTangentQ16[1]) /(65536ll*65536ll)); constexpr std::int64_t MouthClearanceProbeExtent=48000000; - for(std::int64_t distance=2000000; + const std::int64_t firstClearanceProbe=std::max( + 2000000,chamberRadiusMicrometres+1000000); + for(std::int64_t distance=firstClearanceProbe; distance<=MouthClearanceProbeExtent;distance+=2000000) { const std::int64_t x=mouthFloorPoint[0] @@ -2515,9 +2601,10 @@ bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout, bool lowerSide,FPoint& point,std::array& routeTangentQ16, std::int64_t& floorSlopeQ16) { + const auto& chamber=lowerSide?passage.lowerLandingChamber + :passage.upperLandingChamber; for(std::size_t axis=0;axis<3;++axis) - point[axis]=qToMicrometres(lowerSide?passage.lowerMouthSubvoxels[axis] - :passage.upperMouthSubvoxels[axis]); + point[axis]=qToMicrometres(chamber.floorCenterSubvoxels[axis]); if(passage.controlPoints.size()<2)return false; const std::size_t mouthIndex=lowerSide ?std::min(passage.lowerMouthControlIndex,passage.controlPoints.size()-1):0; @@ -2537,28 +2624,26 @@ bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout, if(tangentLength<=0)return false; routeTangentQ16={tx*65536/tangentLength,ty*65536/tangentLength}; 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::int64_t mouthFloor=chamber.floorCenterSubvoxels[2]; point[2]=qToMicrometres(mouthFloor); - const std::int64_t passageFloorDelta=direction*(mouthFloor-tangentFloor); - floorSlopeQ16=passageFloorDelta*65536/tangentLength; + floorSlopeQ16=(chamber.floorGradientQ16[0]*routeTangentQ16[0] + +chamber.floorGradientQ16[1]*routeTangentQ16[1])/65536; 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); + authoredRouteTangentQ16,floorSlopeQ16,point, + qToMicrometres(chamber.radiusSubvoxels)); + floorSlopeQ16=(chamber.floorGradientQ16[0]*routeTangentQ16[0] + +chamber.floorGradientQ16[1]*routeTangentQ16[1])/65536; 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; + const std::int64_t portOffset=std::max(0, + qToMicrometres(chamber.radiusSubvoxels)-LandingConnectorBoreRadius); + point[0]+=offsetDirection*routeTangentQ16[0]*portOffset/65536; + point[1]+=offsetDirection*routeTangentQ16[1]*portOffset/65536; + point[2]=qToMicrometres(mouthFloor) + +(chamber.floorGradientQ16[0]*(point[0] + -qToMicrometres(chamber.floorCenterSubvoxels[0])) + +chamber.floorGradientQ16[1]*(point[1] + -qToMicrometres(chamber.floorCenterSubvoxels[1])))/65536; return true; }; FPoint routeStart{},routeEnd{}; @@ -3391,13 +3476,14 @@ bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout, const auto arrivalPoint=routeStart; FPoint previousPoint=arrivalPoint; std::int64_t previousProfileDistance=0; - std::string previousAnchor=arrivalPassage->stableId+"/lower-mouth"; + std::string previousAnchor=arrivalPassage->stableId+"/lower-landing-chamber"; for(std::size_t nodeIndex=0;nodeIndexstableId+"/upper-mouth", - routeEnd,nodes.back(),selectedBoreRadius(departureEdgeId), + auto edge=makeEdge(departureEdgeId, + departurePassage->stableId+"/upper-landing-chamber", + routeEnd,nodes.back(), + std::min(selectedBoreRadius(departureEdgeId),LandingConnectorBoreRadius), totalRouteLength,nodes.back().floorProfileDistanceMicrometres); edges.push_back(std::move(edge)); } @@ -3873,7 +3961,6 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t {error=passage.stableId+" route mouth is outside the resolved PassageConfig.DistanceMax radius";return false;} passage.upperMouthSubvoxels=upperMouth;passage.lowerMouthSubvoxels=lowerMouth; - constexpr std::int64_t LandingExtensionQ=8; std::array upperTangent{},lowerTangent{}; const bool hasUpperTangent=hasUpperRoute &&routeTangent(upper,true,upperTangent); @@ -4643,39 +4730,6 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t } pushXY({lowerMouth[0],lowerMouth[1]});totalHorizontal=measurePath(); std::size_t lowerMouthIndex=xy.size()-1; - std::vector> lowerLandingPoints; - if(hasLowerTangent) - { - std::array previousRoutePoint=lowerMouth; - std::int64_t remainingQ=LandingExtensionQ*qPerMetre; - bool skippedMouth=false; - for(const auto& anchor:lower.anchors) - if(anchor.kind=="route_point"&&anchor.values.size()>=3) - { - const std::array routePoint{{anchor.values[0], - anchor.values[1],anchor.values[2]}}; - if(!skippedMouth){skippedMouth=true;continue;} - if(remainingQ<=0)break; - const std::int64_t dx=routePoint[0]-previousRoutePoint[0]; - const std::int64_t dy=routePoint[1]-previousRoutePoint[1]; - const std::uint64_t segmentLength=distanceQ(dx,dy); - if(segmentLength==0)continue; - const std::int64_t advance=std::min(remainingQ, - static_cast(segmentLength)); - std::array landingPoint{}; - for(std::size_t axis=0;axis<3;++axis) - landingPoint[axis]=previousRoutePoint[axis]+PassageRoundRatio( - routePoint[axis]-previousRoutePoint[axis],advance, - static_cast(segmentLength)); - if(!radialWithin(landingPoint,placementRadius))break; - lowerLandingPoints.push_back(landingPoint); - pushXY({landingPoint[0],landingPoint[1]}); - previousRoutePoint=landingPoint;remainingQ-=advance; - if(advance(segmentLength))break; - } - if(lowerLandingPoints.empty()) - {error=passage.stableId+" lower route has no landing length inside PassageConfig.DistanceMax";return false;} - } if(VoxelForgeCore::WorldAssemblyEvaluatorVersion>=188 &&style==0&&anchoredIslandPassage&&!anchoredSpiral&&xy.size()>4) { @@ -4718,8 +4772,7 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t std::max(8*qPerMetre,2*mouthRadius)); for(std::size_t i=0;ilowerMouthIndex&&i+1>=xy.size()-1))?mouthRadius:midRadius; + std::int64_t radius=(i==0||i==lowerMouthIndex)?mouthRadius:midRadius; if(surfaceGroundDeparture&&i(cumulative[i]) @@ -4761,9 +4814,7 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t } } std::int64_t playerZ=0; - if(i>lowerMouthIndex) - playerZ=lowerLandingPoints[i-lowerMouthIndex-1][2]; - else if(hasAnchorHelix&&i<=helixEndIndex) + if(hasAnchorHelix&&i<=helixEndIndex) playerZ=upperMouth[2]+PassageRoundRatio(helixBedrockZ-upperMouth[2], static_cast(cumulative[i]), static_cast(helixHorizontal)); @@ -4815,6 +4866,201 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t passage.controlPoints[lowerMouthIndex].positionSubvoxels[2]=lowerMouth[2] +passage.controlPoints[lowerMouthIndex].radiusSubvoxels -playerCenterAboveFloorQ; + std::vector chamberFloorControls; + std::vector chamberFloorProfile; + chamberFloorControls.reserve(passage.controlPoints.size()); + chamberFloorProfile.reserve(passage.controlPoints.size()); + for(const auto& point:passage.controlPoints) + { + chamberFloorControls.push_back({ + static_cast(point.positionSubvoxels[0]), + static_cast(point.positionSubvoxels[1]), + static_cast(point.positionSubvoxels[2]), + static_cast(point.radiusSubvoxels)}); + chamberFloorProfile.push_back(static_cast( + point.positionSubvoxels[2]-point.radiusSubvoxels)); + } + const auto routePoints=[&](const ResolvedLayoutStratumV2& host) + { + std::vector> route; + for(const auto& anchor:host.anchors) + if(anchor.kind=="route_point"&&anchor.values.size()>=3) + route.push_back({static_cast(anchor.values[0]), + static_cast(anchor.values[1]), + static_cast(anchor.values[2])}); + return route; + }; + if(upper.generatorKind=="TunnelNetwork" + ||upper.generatorKind=="SurfaceWorld") + { + auto route=routePoints(upper); + if(upper.generatorKind=="TunnelNetwork"&&route.size()>2) + route.erase(route.begin(),route.end()-2); + double routeHeight=0.0,distanceSquared=0.0; + if(VoxelForgeCore::PassageSpline::ClosestRouteHeightAtXY(route, + chamberFloorControls.front()[0],chamberFloorControls.front()[1], + routeHeight,distanceSquared) + &&distanceSquared<=std::pow(chamberFloorControls.front()[3] + +2.0*qPerMetre,2.0)) + VoxelForgeCore::PassageSpline::BlendEndpointFloorToRoute( + chamberFloorControls,chamberFloorProfile,0,route,qPerMetre, + upper.generatorKind=="SurfaceWorld"?0.98:1.13); + } + if(lower.generatorKind=="TunnelNetwork" + ||lower.generatorKind=="VerticalShafts") + { + auto route=routePoints(lower); + if(route.size()>2)route.resize(2); + double routeHeight=0.0,distanceSquared=0.0; + if(VoxelForgeCore::PassageSpline::ClosestRouteHeightAtXY(route, + chamberFloorControls[lowerMouthIndex][0], + chamberFloorControls[lowerMouthIndex][1], + routeHeight,distanceSquared) + &&distanceSquared<=std::pow( + chamberFloorControls[lowerMouthIndex][3] + +2.0*qPerMetre,2.0)) + VoxelForgeCore::PassageSpline::BlendEndpointFloorToRoute( + chamberFloorControls,chamberFloorProfile, + lowerMouthIndex,route,qPerMetre); + } + const auto resolveLandingChamber=[&]( + ResolvedLayoutPassageLandingChamberV1& chamber, + const std::array& mouth,std::size_t endpointIndex, + std::size_t neighborIndex,const ResolvedLayoutStratumV2& host, + const char* side) + { + chamber.floorCenterSubvoxels=mouth; + chamber.radiusSubvoxels=mouthRadius+3*qPerMetre; + chamber.clearHeightSubvoxels=static_cast( + std::llround(4.5*qPerMetre)); + const auto& endpoint=passage.controlPoints[endpointIndex]; + const auto& neighbor=passage.controlPoints[neighborIndex]; + const std::int64_t endpointFloor=static_cast( + std::llround(chamberFloorProfile[endpointIndex])); + const std::int64_t neighborFloor=static_cast( + std::llround(chamberFloorProfile[neighborIndex])); + chamber.floorCenterSubvoxels[2]=endpointFloor; + const std::int64_t dx=endpoint.positionSubvoxels[0] + -neighbor.positionSubvoxels[0]; + const std::int64_t dy=endpoint.positionSubvoxels[1] + -neighbor.positionSubvoxels[1]; + const std::int64_t dz=endpointFloor-neighborFloor; + const std::int64_t horizontalSquared=dx*dx+dy*dy; + if(horizontalSquared<=0) + {error=passage.stableId+" "+side+" landing chamber has no endpoint tangent";return false;} + chamber.floorGradientQ16={ + PassageRoundRatio(dx<0?-dz:dz,std::abs(dx)*65536, + horizontalSquared), + PassageRoundRatio(dy<0?-dz:dz,std::abs(dy)*65536, + horizontalSquared)}; + if(host.generatorKind=="SurfaceWorld") + { + const auto surfaceRoute=routePoints(host); + std::vector> nearbyRoute; + // Fit the landing floor only to route anchors inside its + // chamber footprint; samples beyond the chamber can pull + // the floor away from the route at the joint. + const double supportRadius=static_cast( + chamber.radiusSubvoxels); + for(const auto& point:surfaceRoute) + { + const double routeDx=point[0]-endpoint.positionSubvoxels[0]; + const double routeDy=point[1]-endpoint.positionSubvoxels[1]; + if(routeDx*routeDx+routeDy*routeDy<=supportRadius*supportRadius) + nearbyRoute.push_back(point); + } + if(nearbyRoute.size()>=2) + { + std::array mean{}; + for(const auto& point:nearbyRoute) + for(std::size_t axis=0;axis<3;++axis) + mean[axis]+=point[axis]/nearbyRoute.size(); + double xx=0.0,xy=0.0,yy=0.0,xz=0.0,yz=0.0; + for(const auto& point:nearbyRoute) + { + const double routeDx=point[0]-mean[0]; + const double routeDy=point[1]-mean[1]; + const double routeDz=point[2]-mean[2]; + xx+=routeDx*routeDx;xy+=routeDx*routeDy; + yy+=routeDy*routeDy;xz+=routeDx*routeDz; + yz+=routeDy*routeDz; + } + const double determinant=xx*yy-xy*xy; + double gradientX=0.0,gradientY=0.0; + if(determinant>1.0e-9) + { + gradientX=(xz*yy-yz*xy)/determinant; + gradientY=(yz*xx-xz*xy)/determinant; + } + else + { + // Route anchors can be collinear inside the chamber. + // Fit their dominant horizontal direction by least + // squares instead of extrapolating from one far point. + const double trace=xx+yy; + const double discriminant=std::sqrt(std::max(0.0, + (xx-yy)*(xx-yy)+4.0*xy*xy)); + const double variance=0.5*(trace+discriminant); + if(variance>1.0e-9) + { + double axisX=1.0,axisY=0.0; + if(std::abs(xy)>1.0e-12) + {axisX=xy;axisY=variance-xx;} + else if(yy>xx) + {axisX=0.0;axisY=1.0;} + const double axisLength=std::hypot(axisX,axisY); + axisX/=axisLength;axisY/=axisLength; + const double routeSlope=(axisX*xz+axisY*yz)/variance; + gradientX=routeSlope*axisX; + gradientY=routeSlope*axisY; + } + } + chamber.floorGradientQ16={ + static_cast(std::llround(gradientX*65536.0)), + static_cast(std::llround(gradientY*65536.0))}; + } + } + const std::uint64_t gradientSquared=static_cast( + chamber.floorGradientQ16[0]*chamber.floorGradientQ16[0] + +chamber.floorGradientQ16[1]*chamber.floorGradientQ16[1]); + const std::int64_t gradientMagnitude=static_cast( + PassageIsqrt(gradientSquared)); + constexpr std::int64_t MaximumLandingGradeQ16=17560; + if(gradientMagnitude>MaximumLandingGradeQ16) + {error=passage.stableId+" "+side+" landing chamber floor exceeds 15 degrees";return false;} + const std::int64_t planeReachQ=PassageRoundRatio( + chamber.radiusSubvoxels,gradientMagnitude,65536); + const std::int64_t shellQ=static_cast( + std::llround(0.75*qPerMetre)); + const auto qToUm=[&](std::int64_t q) + {return q*static_cast(layout.coreVoxelSizeMicrometres) + /WorldAssemblySubvoxelsPerVoxel;}; + if(!host.content.presentInCurrentField + ||qToUm(chamber.floorCenterSubvoxels[2]-planeReachQ-shellQ) + host.content.topMicrometres) + { + error=passage.stableId+" "+side + +" landing chamber does not fit inside its host content band"; + return false; + } + return true; + }; + if(!resolveLandingChamber(passage.upperLandingChamber,upperMouth,0,1, + upper,"upper") + ||!resolveLandingChamber(passage.lowerLandingChamber,lowerMouth, + lowerMouthIndex,lowerMouthIndex-1,lower,"lower")) + return false; + std::printf("[PassageLandingChamberPlan] id=%s upper_radius_m=%.3f lower_radius_m=%.3f clear_height_m=4.500 upper_floor_grade_q16=(%lld,%lld) lower_floor_grade_q16=(%lld,%lld) status=PASS\n", + passage.stableId.c_str(), + passage.upperLandingChamber.radiusSubvoxels/static_cast(qPerMetre), + passage.lowerLandingChamber.radiusSubvoxels/static_cast(qPerMetre), + static_cast(passage.upperLandingChamber.floorGradientQ16[0]), + static_cast(passage.upperLandingChamber.floorGradientQ16[1]), + static_cast(passage.lowerLandingChamber.floorGradientQ16[0]), + static_cast(passage.lowerLandingChamber.floorGradientQ16[1])); layout.passages.push_back(std::move(passage)); } } @@ -4842,17 +5088,22 @@ ResolvedLayoutValidatorAuditV3 AuditResolvedLayoutValidatorV3() else result.failures.push_back({"negative_control_accepted",std::move(name)+" was accepted by strict validator"}); }; auto base=ResolvedLayoutSnapshotV3{};base.worldIdentity="synthetic";base.ownerVoxelSizeMicrometres=125000; - base.coreVoxelSizeMicrometres=250000;base.strates={make("a",0,10000000,0),make("b",1,-2000000,-12000000)}; + base.coreVoxelSizeMicrometres=250000;base.strates={make("a",0,30000000,0),make("b",1,-2000000,-40000000)}; ResolvedLayoutPassageConfigV1 passageConfig;passageConfig.upperStrateIndex=0;passageConfig.connections=1; passageConfig.mouthRadiusSubvoxels=1280;passageConfig.midRadiusSubvoxels=1280; - passageConfig.reachMinimumSubvoxels=2048;passageConfig.reachMaximumSubvoxels=2048; + passageConfig.reachMinimumSubvoxels=9216;passageConfig.reachMaximumSubvoxels=9216; passageConfig.distanceMinimumSubvoxels=1024;passageConfig.distanceMaximumSubvoxels=10240; std::string planError; if(!PlanResolvedLayoutPassagesV1(base,19,{passageConfig},planError)) result.failures.push_back({"synthetic_passage_plan_failed",planError}); {std::vector issues;std::string error; if(!base.validate(ResolvedLayoutValidationModeV3::strict,issues,error)) - result.failures.push_back({"positive_control_rejected","valid ceiling/content/bedrock layout was rejected"});} + { + for(const auto& issue:issues) + std::printf("[ResolvedLayoutValidatorPositiveControlIssue] code=%s detail=%s\n", + issue.code.c_str(),issue.detail.c_str()); + result.failures.push_back({"positive_control_rejected","valid ceiling/content/bedrock layout was rejected"}); + }} auto missingPassage=base;missingPassage.passages.clear();expectReject("missing interface passage",missingPassage); auto outsidePassage=base; if(!outsidePassage.passages.empty()) diff --git a/Standalone/Core/src/ShapeWorldInternal.h b/Standalone/Core/src/ShapeWorldInternal.h index 1a57f85..df853c5 100644 --- a/Standalone/Core/src/ShapeWorldInternal.h +++ b/Standalone/Core/src/ShapeWorldInternal.h @@ -1280,6 +1280,8 @@ public: double unionDistance=std::numeric_limits::infinity(); double unionShellDistance=std::numeric_limits::infinity(); double unionFloorShellDistance=std::numeric_limits::infinity(); + double unionLandingChamberFloorShellDistance= + std::numeric_limits::infinity(); double plannedTunnelPassageShellDistance=std::numeric_limits::infinity(); const std::uint32_t reliefSeed=static_cast(Owner?Owner->Seed:0); const auto* queryStratum=FindBoundStratumForManagerZ(Z); @@ -1317,14 +1319,56 @@ public: const bool passageTargetsPlannedTunnelNetwork=plannedTunnelStratum; const double mouthShellBlendMetres=passageTargetsPlannedTunnelNetwork ?2.0:MouthShellBlendMetres; - // Legacy native cave fields own the floor and mouth carve on their - // lower-strate boundary. The planner's TunnelNetwork field instead - // needs the shared passage composer to carry its incoming mouth open. - if(passageTargetsNativeCaveField&&!passageTargetsPlannedTunnelNetwork) - continue; + // Legacy native cave fields own the tunnel bore on their lower-strate + // boundary. The shared passage composer still owns its landing chamber. if(passageProfile)++passageProfile->passageIterations; const auto& constants=PassageComposerConstants[passageIndex]; + const VoxelForgeCore::ResolvedLayoutPassageLandingChamberV1* landingChamber=nullptr; + if(queryStratum&&queryStratum->Resolved) + { + if(queryStratum->StrateIndex==passage.upperStrateIndex) + landingChamber=&passage.upperLandingChamber; + else if(queryStratum->StrateIndex==passage.lowerStrateIndex) + landingChamber=&passage.lowerLandingChamber; + } + if(landingChamber&&landingChamber->radiusSubvoxels>0 + &&landingChamber->clearHeightSubvoxels>0) + { + const double centerX=static_cast( + landingChamber->floorCenterSubvoxels[0]); + const double centerY=static_cast( + landingChamber->floorCenterSubvoxels[1]); + const double floorCenterZ=static_cast( + landingChamber->floorCenterSubvoxels[2]) + +(landingChamber->floorGradientQ16[0]*(qx-centerX) + +landingChamber->floorGradientQ16[1]*(qy-centerY))/65536.0; + const double radialDistance=DeterministicSqrt( + (qx-centerX)*(qx-centerX)+(qy-centerY)*(qy-centerY)) + -landingChamber->radiusSubvoxels; + const double floorDistance=floorCenterZ-qz; + const double ceilingDistance=qz-(floorCenterZ + +landingChamber->clearHeightSubvoxels); + const double chamberDistance=std::max({radialDistance, + floorDistance,ceilingDistance}); + const double chamberShellDistance=std::max(-chamberDistance, + chamberDistance-shellThickness); + const double chamberFloorShellDistance=std::max( + std::max(-floorDistance,floorDistance-shellThickness), + radialDistance); + unionDistance=std::min(unionDistance,chamberDistance); + unionShellDistance=std::min(unionShellDistance,chamberShellDistance); + unionFloorShellDistance=std::min(unionFloorShellDistance, + chamberFloorShellDistance); + unionLandingChamberFloorShellDistance=std::min( + unionLandingChamberFloorShellDistance,chamberFloorShellDistance); + if(passageTargetsPlannedTunnelNetwork) + plannedTunnelPassageShellDistance=std::min( + plannedTunnelPassageShellDistance,chamberShellDistance); + } + if(passageTargetsNativeCaveField&&!passageTargetsPlannedTunnelNetwork) + continue; + FNearestTubeSample nearest; const bool usePreparedSweep=passageTileScopeMatches &&passageTileQueries->hasSweptTubeBase(); @@ -1615,6 +1659,11 @@ public: if(!std::isfinite(unionDistance))return density; const float passageAirDistance=static_cast(-unionDistance/Q); + // Keep the landing floor shell where it is exposed to the chamber. At + // the tunnel joint, the passage void must be able to open through that + // floor shell so its own continuous floor and shell define the throat. + if(unionLandingChamberFloorShellDistance<0.0&&unionDistance>0.0) + return static_cast(unionLandingChamberFloorShellDistance/Q); const auto plannedPortalWalkCorridorContains=[&]() { if(!queryStratum||!queryStratum->Resolved @@ -1678,7 +1727,11 @@ public: const double routeFloor=std::lerp(a[2],b[2],t)-1.13; return queryZ>=routeFloor-0.30&&queryZ<=routeFloor+2.25; }; - constexpr double PortalWalkLeadMetres=8.0; + // The connected layer route begins inside the landing chamber. Let + // its air open the chamber's plain side shell through the chamber + // radius plus the shell thickness; the condition below remains + // limited to samples where the tunnel/chamber shell is present. + constexpr double PortalWalkLeadMetres=9.0; const auto arrival=position(*first),arrivalInner=position(*second); const double arrivalInnerDistance=std::hypot( arrivalInner[0]-arrival[0],arrivalInner[1]-arrival[1]); @@ -1693,6 +1746,90 @@ public: departureTailDistance) ||nearRouteSegment(*lastTwo,*last,PortalWalkLeadMetres,true,0.0); }; + const auto connectedSurfaceLandingShellContains=[&]() + { + if(!queryStratum||!queryStratum->Resolved + ||queryStratum->Resolved->generatorKind!="SurfaceWorld")return false; + const double queryX=qx/qPerMetre,queryY=qy/qPerMetre,queryZ=qz/qPerMetre; + constexpr double ChamberShellThicknessMetres=0.75; + constexpr double RouteClearanceMetres=1.25; + constexpr double RouteHeightClearanceMetres=1.5; + constexpr double PortalWalkLeadMetres=9.0; + for(const auto& passage:ResolvedLayout->passages) + { + if(passage.upperStrateIndex!=queryStratum->StrateIndex)continue; + const auto& chamber=passage.upperLandingChamber; + if(chamber.radiusSubvoxels<=0||chamber.clearHeightSubvoxels<=0)continue; + const double cx=static_cast(chamber.floorCenterSubvoxels[0])/qPerMetre; + const double cy=static_cast(chamber.floorCenterSubvoxels[1])/qPerMetre; + const double floorZ=static_cast(chamber.floorCenterSubvoxels[2])/qPerMetre + +chamber.floorGradientQ16[0]/65536.0*(queryX-cx) + +chamber.floorGradientQ16[1]/65536.0*(queryY-cy); + const double radialDistance=std::hypot(queryX-cx,queryY-cy) + -static_cast(chamber.radiusSubvoxels)/qPerMetre; + const double floorDistance=floorZ-queryZ; + const double ceilingDistance=queryZ-(floorZ + +static_cast(chamber.clearHeightSubvoxels)/qPerMetre); + const double chamberDistance=std::max({radialDistance, + floorDistance,ceilingDistance}); + const double chamberShellDistance=std::max(-chamberDistance, + chamberDistance-ChamberShellThicknessMetres); + if(chamberDistance<=0.0||chamberShellDistance>=0.0)continue; + + std::vector route; + for(const auto& anchor:queryStratum->Resolved->anchors) + if(anchor.kind=="route_point"&&anchor.values.size()>=3) + route.push_back(&anchor); + if(route.size()<2)continue; + const double mouthZ=static_cast(passage.upperMouthSubvoxels[2]) + /qPerMetre; + std::size_t endpoint=route.size(); + double endpointDistanceSquared=1.0; + for(std::size_t index=0;indexvalues[0]/qPerMetre; + const double y=route[index]->values[1]/qPerMetre; + const double z=route[index]->values[2]/qPerMetre; + const double dx=x-cx,dy=y-cy; + const double distanceSquared=dx*dx+dy*dy; + if(distanceSquared routeDistance(route.size(),0.0); + for(std::size_t index=1;indexvalues[0]-route[index-1]->values[0]) + /qPerMetre; + const double dy=(route[index]->values[1]-route[index-1]->values[1]) + /qPerMetre; + routeDistance[index]=routeDistance[index-1]+std::hypot(dx,dy); + } + for(std::size_t index=1;indexPortalWalkLeadMetres)continue; + const double routeX=ax+t*dx,routeY=ay+t*dy; + const double routeZ=az+t*(b.values[2]-a.values[2])/qPerMetre; + if(std::hypot(queryX-routeX,queryY-routeY)<=RouteClearanceMetres + &&std::abs(queryZ-routeZ)<=RouteHeightClearanceMetres)return true; + } + } + return false; + }; if(plannedTunnelStratum &&nativeTunnelFieldDensity!=std::numeric_limits::lowest() &&nativeTunnelFieldDensity<0.0f) @@ -1755,7 +1892,11 @@ public: if(nativeTunnelFieldDensity!=std::numeric_limits::lowest()) return std::max(density,passageAirDistance); if(unionDistance>0.0&&unionShellDistance<0.0&&density>=0.0f) + { + if(connectedSurfaceLandingShellContains()) + return std::max(density,passageAirDistance); return static_cast(unionShellDistance/Q); + } return std::max(density,passageAirDistance); } @@ -1892,6 +2033,26 @@ public: } if(overlaps(low,high))return true; } + const auto chamberOverlaps=[&]( + const VoxelForgeCore::ResolvedLayoutPassageLandingChamberV1& chamber) + { + const double cx=static_cast(chamber.floorCenterSubvoxels[0])/Q; + const double cy=static_cast(chamber.floorCenterSubvoxels[1])/Q; + const double cz=static_cast(chamber.floorCenterSubvoxels[2])/Q; + const double radius=static_cast(chamber.radiusSubvoxels)/Q; + const double floorExtent=radius*std::hypot( + static_cast(chamber.floorGradientQ16[0])/65536.0, + static_cast(chamber.floorGradientQ16[1])/65536.0); + const double extent=reachCoreVoxels+0.01; + const double low[3]={cx-radius-extent,cy-radius-extent, + cz-floorExtent-extent}; + const double high[3]={cx+radius+extent,cy+radius+extent, + cz+floorExtent+static_cast( + chamber.clearHeightSubvoxels)/Q+extent}; + return overlaps(low,high); + }; + if(chamberOverlaps(passage.upperLandingChamber) + ||chamberOverlaps(passage.lowerLandingChamber))return true; } return false; } @@ -3668,6 +3829,12 @@ private: double lowerMouthPlannedFloor=0.0; double lowerMouthTangentX=0.0,lowerMouthTangentY=0.0; std::vector> lowerMouthReceivingRoute; + std::array upperLandingFloorCenterSubvoxels{}; + std::array upperLandingFloorGradient{}; + double upperLandingRadiusSubvoxels=0.0; + std::array lowerLandingFloorCenterSubvoxels{}; + std::array lowerLandingFloorGradient{}; + double lowerLandingRadiusSubvoxels=0.0; std::size_t routeEndControlIndex=0; }; struct FPassageSegmentData @@ -3692,6 +3859,49 @@ private: std::max(0.0,radiusSubvoxels-0.23*qPerMetre)); } + static double BlendPassageFloorToLandingChambers( + const FPassageComposerConstants& constants, + const FPassageSegmentData& segment,double floorT, + double queryXSubvoxels,double queryYSubvoxels,double plannedFloor, + double qPerMetre) + { + if(qPerMetre<=0.0||!(constants.totalPathLengthMetres>0.0)) + return plannedFloor; + const double alongMetres=segment.authoredAlongMetres + +floorT*segment.authoredLengthMetres; + constexpr double LandingFloorBlendMetres=12.0; + const auto floorWeight=[&](double distanceMetres,double radiusSubvoxels) + { + if(!(radiusSubvoxels>0.0))return 0.0; + const double radiusMetres=radiusSubvoxels/qPerMetre; + if(distanceMetres<=radiusMetres)return 1.0; + if(distanceMetres>=radiusMetres+LandingFloorBlendMetres)return 0.0; + const double t=(distanceMetres-radiusMetres)/LandingFloorBlendMetres; + return 1.0-t*t*(3.0-2.0*t); + }; + const double upperWeight=floorWeight(std::max(0.0,alongMetres), + constants.upperLandingRadiusSubvoxels); + const double lowerWeight=floorWeight(std::max(0.0, + constants.totalPathLengthMetres-alongMetres), + constants.lowerLandingRadiusSubvoxels); + if(upperWeight<=0.0&&lowerWeight<=0.0)return plannedFloor; + const auto floorAt=[&](const std::array& center, + const std::array& gradient) + { + return center[2]+gradient[0]*(queryXSubvoxels-center[0]) + +gradient[1]*(queryYSubvoxels-center[1]); + }; + const double upperFloor=floorAt(constants.upperLandingFloorCenterSubvoxels, + constants.upperLandingFloorGradient); + const double lowerFloor=floorAt(constants.lowerLandingFloorCenterSubvoxels, + constants.lowerLandingFloorGradient); + const double totalWeight=upperWeight+lowerWeight; + if(totalWeight<=1.0) + return plannedFloor*(1.0-totalWeight) + +upperFloor*upperWeight+lowerFloor*lowerWeight; + return (upperFloor*upperWeight+lowerFloor*lowerWeight)/totalWeight; + } + static double PassageFlatFloorCenterZSubvoxels( const FPassageComposerConstants& constants, const std::vector& segments, @@ -3742,7 +3952,9 @@ private: -constants.lowerMouthTangentX,-constants.lowerMouthTangentY, VoxelForgeCore::PassageSpline::UpperMouthFloorBlendRadiusMetres,1.13, qPerMetre); - return lowerBlendFloor+floorDepthSubvoxels; + const double landingFloor=BlendPassageFloorToLandingChambers(constants, + segment,floorT,queryXSubvoxels,queryYSubvoxels,lowerBlendFloor,qPerMetre); + return landingFloor+floorDepthSubvoxels; } static float PassageFlatBottomDistance(float roundDistanceSubvoxels, @@ -4900,6 +5112,23 @@ private: passage.lowerMouthControlIndex,1,passage.controlPoints.size()-1); auto& constants=PassageComposerConstants[passageIndex]; constants.routeEndControlIndex=routeEnd; + for(std::size_t axis=0;axis<3;++axis) + { + constants.upperLandingFloorCenterSubvoxels[axis]=static_cast( + passage.upperLandingChamber.floorCenterSubvoxels[axis]); + constants.lowerLandingFloorCenterSubvoxels[axis]=static_cast( + passage.lowerLandingChamber.floorCenterSubvoxels[axis]); + } + constants.upperLandingFloorGradient={ + passage.upperLandingChamber.floorGradientQ16[0]/65536.0, + passage.upperLandingChamber.floorGradientQ16[1]/65536.0}; + constants.lowerLandingFloorGradient={ + passage.lowerLandingChamber.floorGradientQ16[0]/65536.0, + passage.lowerLandingChamber.floorGradientQ16[1]/65536.0}; + constants.upperLandingRadiusSubvoxels=static_cast( + passage.upperLandingChamber.radiusSubvoxels); + constants.lowerLandingRadiusSubvoxels=static_cast( + passage.lowerLandingChamber.radiusSubvoxels); const auto upperStratum=std::find_if(ResolvedLayout->strates.begin(), ResolvedLayout->strates.end(),[&](const auto& stratum) {return stratum.strateIndex==passage.upperStrateIndex;}); @@ -4928,7 +5157,8 @@ private: double upperMouthFloorDepth=authoredUpperMouthFloorDepth; if(upperStratum!=ResolvedLayout->strates.end() &&(upperStratum->generatorKind=="FloatingIslands" - ||upperStratum->generatorKind=="TunnelNetwork")) + ||upperStratum->generatorKind=="TunnelNetwork" + ||upperStratum->generatorKind=="SurfaceWorld")) { std::vector> route; for(const auto& anchor:upperStratum->anchors) @@ -4947,16 +5177,19 @@ private: upperMouthFloorDepth=std::max(upperMouthFloorDepth, std::max(0.0,(controls.front()[2]-routeHeight)/qPerMetre +constants.upperMouthPlayerCenterHeightMetres)); - if(upperStratum->generatorKind=="TunnelNetwork") + if(upperStratum->generatorKind=="TunnelNetwork" + ||upperStratum->generatorKind=="SurfaceWorld") VoxelForgeCore::PassageSpline::BlendEndpointFloorToRoute( - controls,plannedFloorProfile,0,route,qPerMetre); + controls,plannedFloorProfile,0,route,qPerMetre, + constants.upperMouthPlayerCenterHeightMetres); } } controls.front()[3]=std::max(controls.front()[3], upperMouthFloorDepth*qPerMetre); if(upperStratum!=ResolvedLayout->strates.end() &&(upperStratum->generatorKind=="FloatingIslands" - ||upperStratum->generatorKind=="TunnelNetwork")) + ||upperStratum->generatorKind=="TunnelNetwork" + ||upperStratum->generatorKind=="SurfaceWorld")) for(const auto& anchor:upperStratum->anchors) if(anchor.kind=="route_point"&&anchor.values.size()>=3) constants.upperMouthReceivingRoute.push_back({ @@ -5044,8 +5277,8 @@ private: constants.upperMouthTangentY=sampledSegments.front().b[1] -sampledSegments.front().a[1]; } - const std::string lowerMouthAnchor=passage.stableId+"/lower-mouth"; - const std::string upperMouthAnchor=passage.stableId+"/upper-mouth"; + const std::string lowerMouthAnchor=passage.stableId+"/lower-landing-chamber"; + const std::string upperMouthAnchor=passage.stableId+"/upper-landing-chamber"; for(const auto& edge:ResolvedLayout->cavernEdges) { if(edge.controlPointsMicrometres.size()<2)continue; @@ -5230,6 +5463,40 @@ private: std::ceil((localMaximum[axis]+localExtent)*Q)); } } + const auto includeLandingChamber=[&]( + const VoxelForgeCore::ResolvedLayoutPassageLandingChamberV1& chamber) + { + const double cx=static_cast(chamber.floorCenterSubvoxels[0])/Q; + const double cy=static_cast(chamber.floorCenterSubvoxels[1])/Q; + const double cz=static_cast(chamber.floorCenterSubvoxels[2])/Q; + const double radius=static_cast(chamber.radiusSubvoxels)/Q; + const double gx=static_cast(chamber.floorGradientQ16[0])/65536.0; + const double gy=static_cast(chamber.floorGradientQ16[1])/65536.0; + const double floorExtent=radius*std::hypot(gx,gy); + const double height=static_cast( + chamber.clearHeightSubvoxels)/Q; + const double extent=requiredReachCoreVoxels+0.01; + const std::array minimum{ + cx-radius-extent,cy-radius-extent,cz-floorExtent-extent}; + const std::array maximum{ + cx+radius+extent,cy+radius+extent, + cz+floorExtent+height+extent}; + FPassageProofBound proof; + for(std::size_t axis=0;axis<3;++axis) + { + routeMinimum[axis]=std::min(routeMinimum[axis],minimum[axis]); + routeMaximum[axis]=std::max(routeMaximum[axis],maximum[axis]); + proof.minimum[axis]=minimum[axis]; + proof.maximum[axis]=maximum[axis]; + } + PassageProofBounds.push_back(proof); + }; + if(segmentIndex==0) + includeLandingChamber( + ResolvedLayout->passages[passageIndex].upperLandingChamber); + if(segmentEndIndex==segmentCount) + includeLandingChamber( + ResolvedLayout->passages[passageIndex].lowerLandingChamber); const double routeExtent=maximumRadiusSubvoxels/Q +requiredReachCoreVoxels+0.01; const double tileExtent=routeExtent; diff --git a/Standalone/Research/WorldAssemblerM3/Linux151/PassageEnvelopeAudit.py b/Standalone/Research/WorldAssemblerM3/Linux151/PassageEnvelopeAudit.py new file mode 100644 index 0000000..14f3b62 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux151/PassageEnvelopeAudit.py @@ -0,0 +1,318 @@ +#!/usr/bin/env python3 +"""Audit cave primitive bounds against passage envelopes, allowing chamber joints only.""" +from __future__ import annotations + +import math +from typing import Any + + +INFLUENCE_REACH_M = 1.053334 +SHELL_THICKNESS_M = 0.75 + + +def _point(control: dict[str, Any], scale: float) -> tuple[float, float, float, float]: + x, y, z = control["position_q"] + return (x * scale, y * scale, z * scale, control["radius_q"] * scale) + + +def _knot_span(a, b) -> float: + return max(1.0e-6, math.sqrt(math.sqrt((b[0] - a[0]) ** 2 + (b[1] - a[1]) ** 2))) + + +def _evaluate(controls, span: int, t: float): + p1, p2 = controls[span], controls[span + 1] + p0 = controls[span - 1] if span else controls[0] + p3 = controls[min(span + 2, len(controls) - 1)] + dt0, dt1, dt2 = _knot_span(p0, p1), _knot_span(p1, p2), _knot_span(p2, p3) + t2, t3 = t * t, t * t * t + result = [] + for axis in (0, 1, 3): + m1 = dt1 * ((p1[axis] - p0[axis]) / dt0 + - (p2[axis] - p0[axis]) / (dt0 + dt1) + + (p2[axis] - p1[axis]) / dt1) + m2 = dt1 * ((p2[axis] - p1[axis]) / dt1 + - (p3[axis] - p1[axis]) / (dt1 + dt2) + + (p3[axis] - p2[axis]) / dt2) + value = ((2 * t3 - 3 * t2 + 1) * p1[axis] + + (-2 * t3 + 3 * t2) * p2[axis] + + (t3 - 2 * t2 + t) * m1 + (t3 - t2) * m2) + result.append(value) + radius = min(max(result[2], min(p1[3], p2[3])), max(p1[3], p2[3])) + return result[0], result[1], p1[2] + (p2[2] - p1[2]) * t, radius + + +def _segments(passage: dict[str, Any], scale: float): + controls = [_point(row, scale) for row in passage["control_points"]] + route_end = min(max(1, int(passage.get("lower_mouth_control_index", len(controls) - 1))), + len(controls) - 1) + along = 0.0 + for span in range(route_end): + a, b = controls[span], controls[span + 1] + chord = math.hypot(b[0] - a[0], b[1] - a[1]) + steps = max(1, math.ceil(chord / 2.0)) + prior = _evaluate(controls, span, 0.0) + for step in range(1, steps + 1): + current = _evaluate(controls, span, step / steps) + length = math.dist(prior[:3], current[:3]) + yield prior, current, along, max(prior[3], current[3]) + INFLUENCE_REACH_M + along += length + prior = current + + +def _distance_squared(a, b, bounds, t: float) -> float: + total = 0.0 + for axis in range(3): + pos = a[axis] + (b[axis] - a[axis]) * t + lo, hi = bounds[axis] + gap = lo - pos if pos < lo else pos - hi if pos > hi else 0.0 + total += gap * gap + return total + + +def _overlap_interval(a, b, bounds, radius: float): + cuts = {0.0, 1.0} + for axis in range(3): + delta = b[axis] - a[axis] + if abs(delta) < 1.0e-15: + continue + for edge in bounds[axis]: + t = (edge - a[axis]) / delta + if 0.0 < t < 1.0: + cuts.add(t) + cuts = sorted(cuts) + best_t, best_d2 = 0.0, math.inf + for lo, hi in zip(cuts, cuts[1:]): + mid = (lo + hi) * 0.5 + quadratic = linear = 0.0 + for axis in range(3): + delta = b[axis] - a[axis] + pos = a[axis] + delta * mid + low, high = bounds[axis] + edge = low if pos < low else high if pos > high else None + if edge is not None: + quadratic += delta * delta + linear += delta * (a[axis] - edge) + candidate = min(hi, max(lo, -linear / quadratic)) if quadratic else lo + d2 = _distance_squared(a, b, bounds, candidate) + if d2 < best_d2: + best_t, best_d2 = candidate, d2 + for t in (0.0, 1.0): + d2 = _distance_squared(a, b, bounds, t) + if d2 < best_d2: + best_t, best_d2 = t, d2 + if best_d2 > radius * radius: + return None + + radius2 = radius * radius + if _distance_squared(a, b, bounds, 0.0) <= radius2: + left = 0.0 + else: + low, high = 0.0, best_t + for _ in range(42): + mid = (low + high) * 0.5 + if _distance_squared(a, b, bounds, mid) <= radius2: + high = mid + else: + low = mid + left = high + if _distance_squared(a, b, bounds, 1.0) <= radius2: + right = 1.0 + else: + low, high = best_t, 1.0 + for _ in range(42): + mid = (low + high) * 0.5 + if _distance_squared(a, b, bounds, mid) <= radius2: + low = mid + else: + high = mid + right = low + return left, right, best_t + + +def _inside_chamber(point, chamber: dict[str, Any], scale: float) -> bool: + cx, cy, cz = (value * scale for value in chamber["floor_center_q"]) + radius = chamber["radius_q"] * scale + clear = chamber["clear_height_q"] * scale + gx, gy = (value / 65536.0 for value in chamber["floor_gradient_q16"]) + floor = cz + gx * (point[0] - cx) + gy * (point[1] - cy) + chamber_distance = max(math.hypot(point[0] - cx, point[1] - cy) - radius, + floor - point[2], point[2] - (floor + clear)) + return chamber_distance <= SHELL_THICKNESS_M + + +def _primitive_bounds(row: dict[str, Any]): + return tuple((row["bounds_min_um"][i] / 1.0e6, + row["bounds_max_um"][i] / 1.0e6) for i in range(3)) + + +def _bezier(points, t: float): + u = 1.0 - t + return tuple(u**3 * points[0][axis] + 3*u*u*t * points[1][axis] + + 3*u*t*t * points[2][axis] + t**3 * points[3][axis] + for axis in range(3)) + + +def _distance_to_ellipse(radial: float, vertical: float, + radius: float, height: float) -> float: + """Euclidean distance to the edge bore's horizontal/vertical ellipse.""" + radial = abs(radial) + vertical = abs(vertical) + if (radial / radius) ** 2 + (vertical / height) ** 2 <= 1.0: + return 0.0 + + lo, hi = 0.0, math.pi / 2.0 + ratio = (math.sqrt(5.0) - 1.0) / 2.0 + + def squared_distance(angle: float) -> float: + return ((radial - radius * math.cos(angle)) ** 2 + + (vertical - height * math.sin(angle)) ** 2) + + left = hi - ratio * (hi - lo) + right = lo + ratio * (hi - lo) + left_value, right_value = squared_distance(left), squared_distance(right) + for _ in range(48): + if left_value < right_value: + hi, right, right_value = right, left, left_value + left = hi - ratio * (hi - lo) + left_value = squared_distance(left) + else: + lo, left, left_value = left, right, right_value + right = lo + ratio * (hi - lo) + right_value = squared_distance(right) + return math.sqrt(min(left_value, right_value)) + + +def _actual_edge_clearance(passage, edge, scale: float, + chamber: dict[str, Any]) -> float: + """Return minimum true tube clearance outside the landing chamber. + + The route edge is a swept ellipse, with its recorded polyline as the axis. + Comparing the tunnel tube to that ellipse avoids the false clashes caused by + intersecting two enclosing boxes. + """ + controls = [tuple(value / 1.0e6 for value in point) + for point in edge["control_points_um"]] + axis = [tuple(value / 1.0e6 for value in point) + for point in edge.get("polyline_um", [])] + if len(axis) < 2: + axis = [_bezier(controls, index / 64.0) for index in range(65)] + edge_radius = (edge["bore_radius_um"] + + edge["irregularity_amplitude_um"]) / 1.0e6 + edge_height = (edge["bore_height_um"] + + edge["irregularity_amplitude_um"]) / 1.0e6 + passage_segments = passage.get("_audit_segments") or tuple(_segments(passage, scale)) + minimum_clearance = math.inf + for a, b, _, passage_radius in passage_segments: + length = math.dist(a[:3], b[:3]) + steps = max(1, math.ceil(length / 0.05)) + for step in range(steps + 1): + t = step / steps + point = tuple(a[i] + (b[i] - a[i]) * t for i in range(3)) + if _inside_chamber(point, chamber, scale): + continue + tunnel_radius = (a[3] + (b[3] - a[3]) * t + + INFLUENCE_REACH_M) + for start, end in zip(axis, axis[1:]): + delta = tuple(end[i] - start[i] for i in range(3)) + length_squared = sum(value * value for value in delta) + projection = (sum((point[i] - start[i]) * delta[i] + for i in range(3)) / length_squared + if length_squared else 0.0) + projection = min(1.0, max(0.0, projection)) + center = tuple(start[i] + projection * delta[i] for i in range(3)) + radial = math.hypot(point[0] - center[0], point[1] - center[1]) + vertical = abs(point[2] - center[2]) + clearance = (_distance_to_ellipse(radial, vertical, + edge_radius, edge_height) + - tunnel_radius) + minimum_clearance = min(minimum_clearance, clearance) + return minimum_clearance + + +def _test_bounds(bounds, strate_index: int, passages, scale: float, + *, allow_chamber: bool = True): + for passage in passages: + if strate_index not in (passage["upper_strate_index"], passage["lower_strate_index"]): + continue + chamber_key = ("upper_landing_chamber" if strate_index == passage["upper_strate_index"] + else "lower_landing_chamber") + chamber = passage[chamber_key] + segments = passage.get("_audit_segments") + if segments is None: + segments = tuple(_segments(passage, scale)) + for a, b, along, radius in segments: + expanded = [(a[i] - radius, b[i] + radius) if a[i] <= b[i] + else (b[i] - radius, a[i] + radius) for i in range(3)] + if any(expanded[i][1] < bounds[i][0] or expanded[i][0] > bounds[i][1] + for i in range(3)): + continue + overlap = _overlap_interval(a, b, bounds, radius) + if overlap is None: + continue + first, last, closest = overlap + if allow_chamber: + points = [tuple(a[i] + (b[i] - a[i]) * t for i in range(3)) + for t in (first, closest, last)] + if all(_inside_chamber(point, chamber, scale) for point in points): + continue + point = tuple(a[i] + (b[i] - a[i]) * closest for i in range(3)) + return passage["stable_id"], along + closest * math.dist(a[:3], b[:3]), point + return None + + +def audit_layout(layout: dict[str, Any]) -> dict[str, Any]: + scale = layout["core_voxel_size_um"] / (256.0 * 1.0e6) + passages = [dict(row, _audit_segments=tuple(_segments(row, scale))) + for row in layout.get("passages", [])] + primitives = [*layout.get("cavern_rooms", []), *layout.get("cavern_edges", [])] + checked = conflicts = chamber_joints = bounds_candidates = refined_false_positives = 0 + first_conflict = None + for primitive in primitives: + bounds = _primitive_bounds(primitive) + checked += 1 + primitive_conflicted = False + for passage in passages: + found = _test_bounds(bounds, primitive["strate_index"], [passage], scale) + if not found: + continue + bounds_candidates += 1 + passage_id, station, point = found + if "control_points_um" in primitive: + side = ("upper_landing_chamber" + if primitive["strate_index"] == passage["upper_strate_index"] + else "lower_landing_chamber") + clearance = _actual_edge_clearance(passage, primitive, scale, + passage[side]) + if clearance >= 0.0: + refined_false_positives += 1 + continue + primitive_conflicted = True + conflicts += 1 + if first_conflict is None: + first_conflict = {"primitive": primitive["stable_id"], + "passage": passage_id, "station_m": round(station, 3), + "center_m": [round(value, 3) for value in point]} + break + if not primitive_conflicted: + # Count the edge's named landing association as a confirmed joint. + anchor = primitive.get("start_anchor_stable_id", "") + if anchor.endswith(("/upper-landing-chamber", "/lower-landing-chamber")): + chamber_joints += 1 + + negative_controls = 0 + long_passage = max(passages, key=lambda row: len(row["control_points"]), default=None) + if long_passage: + controls = [_point(row, scale) for row in long_passage["control_points"]] + index = min(max(1, len(controls) // 2), len(controls) - 2) + center = controls[index] + bounds = tuple((center[axis] - 0.25, center[axis] + 0.25) for axis in range(3)) + # Select the side whose chamber is farther from this forced tunnel overlap. + strate = long_passage["lower_strate_index"] + if _test_bounds(bounds, strate, [long_passage], scale, allow_chamber=False): + negative_controls = 1 + return {"primitives_checked": checked, "bounds_candidates": bounds_candidates, + "refined_edge_false_positives": refined_false_positives, + "outside_chamber_conflicts": conflicts, + "chamber_joints": chamber_joints, "negative_controls": negative_controls, + "negative_controls_expected": 1, "first_conflict": first_conflict, + "passed": conflicts == 0 and negative_controls == 1} diff --git a/Standalone/Research/WorldAssemblerM3/Linux151/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux151/REPORT.md new file mode 100644 index 0000000..74bb059 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux151/REPORT.md @@ -0,0 +1,39 @@ +# L151 — One tunnel for two strata + +| Landing chamber | Before (L150) | After (L151) | +|---|---|---| +| passage/0-1/0 · upper | ![L150 upper landing, passage 0-1](captures/before-l150-0-1-0-upper-landing.jpg) | ![L151 upper landing, passage 0-1](captures/after-l151-0-1-0-upper-landing.jpg) | +| passage/0-1/0 · lower | ![L150 lower landing, passage 0-1](captures/before-l150-0-1-0-lower-landing.jpg) | ![L151 lower landing, passage 0-1](captures/after-l151-0-1-0-lower-landing.jpg) | +| passage/1-2/0 · upper | ![L150 upper landing, passage 1-2](captures/before-l150-1-2-0-upper-landing.jpg) | ![L151 upper landing, passage 1-2](captures/after-l151-1-2-0-upper-landing.jpg) | +| passage/1-2/0 · lower | ![L150 lower landing, passage 1-2](captures/before-l150-1-2-0-lower-landing.jpg) | ![L151 lower landing, passage 1-2](captures/after-l151-1-2-0-lower-landing.jpg) | +| passage/2-3/0 · upper | ![L150 upper landing, passage 2-3](captures/before-l150-2-3-0-upper-landing.jpg) | ![L151 upper landing, passage 2-3](captures/after-l151-2-3-0-upper-landing.jpg) | +| passage/2-3/0 · lower | ![L150 lower landing, passage 2-3](captures/before-l150-2-3-0-lower-landing.jpg) | ![L151 lower landing, passage 2-3](captures/after-l151-2-3-0-lower-landing.jpg) | + +Seed 0 camera poses are matched at eye height. All 12 images passed the capture quality check. The L150 build stamp is `c6b8a100355e271ddc40eb5973569d2ff4e2ba8d9c628c72f8e0af2493f0609f`; L151 is `1ca3b84f6d35f7f355ec0f72dabd3b87a455ea02e5789fea611916962e5db8e5`. The [capture index](captures/INDEX.md) and [manifest](captures/manifest.json) record the poses and files. + +## Result + +**L151 is not promotion-ready.** The resolved layout now records one passage spline with an upper and lower landing chamber, and the evaluator version is 238. The best tested floor blend still leaves two walk segments above the 15° limit: 19.077° on passage 0-1 and 19.033° on passage 2-3. The three-variant stop rule was reached; I stopped without a fourth geometry attempt. No owner baselines were promoted. + +## What changed + +- Added serialized and validated landing chamber records with floor center, floor gradient, radius, and 4.5 m clear height. The shared passage field composes each chamber with the tunnel shell and floor. +- Cave route endpoints now bind to the passage's landing chamber anchors. The passage floor blends to chamber floors over the tested 12 m transition. +- Extended passage audits for chamber capsule clearance, floor and wall shell, and blocked-world negative controls. Added a geometric cave-primitive envelope audit that permits the named chamber joints and refines conservative edge-box candidates against the swept bore. +- Retained the L135 mouth profile, L147 interior-air gate, and L148 envelope allowance. + +## Measurements and checks + +- **Focused owner-world run:** one seed-2 W8 run, source stamp above. Three interfaces were audited. Passage and chamber interiors together had 0 solid samples across 319,579 capsule samples; all six chambers had 0 solid samples. Passage floor, wall, and interior shell violations were all zero. Naturalness, mouth profile, ripple, and all three layer route audits passed. Tunnel-network, vertical-shaft, and blocked-passage negative controls were detected (1/1, 1/1, and 3/3). The combined passage summary failed because only one of three interfaces met walkability. +- **Chamber envelope audit:** 549 primitives checked; 2 conservative bounds candidates were refined as false positives; 0 conflicts remained outside chambers; 3 named chamber joints and the negative control passed (1/1). Result: [JSON](../../../../Saved/Linux/l151/passage-envelope-audit.json). +- **Blend attempts:** 12 m retained two grades at 19.077° and 19.033°. Extending the blend to 24 m produced 436 solid interior capsule samples and one wall violation, as well as grades of 15.282° and 17.406° on passage 0-1. Restricting the external blend to 0 m kept interior and shell checks clear but left a 22.226° grade on passage 0-1. The 12 m variant is the final source state. +- **Build and snapshots:** Clang 19 build passed for stamp `1ca3b84f…`; the explicit seed-0 resolved-layout snapshot round trip passed. Python capture and envelope audit scripts ran successfully. `git diff --check` passed for the tracked diff; the report's whitespace and image references were checked separately. +- **Seams:** the saved seed-2 log has no `[PassageTileSeamAudit]` result, so seam status is unverified. + +## Not run + +The three-failure stop was reached before the fast tier, the 16-seed Tunnel Network and Vertical Shafts sweeps, Clang/GCC W1/W8/W12, and L80 timing for all four layers. No timing claim is made. No owner-world baseline files were updated because the composed walkability gate failed. The only modified `Source/` file is `Standalone/Source/StreamBenchmarkPipeline.h`; no Unreal `Source/` files changed. + +## Remaining risk + +The tunnel-to-chamber floor transition still creates local grades above 15° in two interfaces. The envelope audit and geometry evidence cover seed 2 only; multi-seed composed behavior, seams, compiler/worker determinism, the fast tier, and L80 performance remain unverified. diff --git a/Standalone/Research/WorldAssemblerM3/Linux151/RunL151Captures.py b/Standalone/Research/WorldAssemblerM3/Linux151/RunL151Captures.py new file mode 100644 index 0000000..1f41c85 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux151/RunL151Captures.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 +"""Capture matched eye-height views from both ends of every owner tunnel.""" +from __future__ import annotations + +import json +import math +import os +from pathlib import Path +import re +import subprocess +import sys + + +ROOT = Path(__file__).resolve().parents[4] +DATA = ROOT / "Standalone/data/owner_world.json" +OUT = ROOT / "Saved/Linux/l151/captures" +LAYOUT = OUT / "after-resolved-layout.json" +ARTIFACTS = ROOT / "Standalone/Research/WorldAssemblerM3/Linux151/captures" +LIMIT = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--"] +VERSIONS = { + "before-l150": { + "headless": ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-l150-head-clang", + "renderer": ROOT / "Standalone/bin/linux/VoxelForgeOffscreenCapture-l150-head-clang", + "stamp": "c6b8a100355e271ddc40eb5973569d2ff4e2ba8d9c628c72f8e0af2493f0609f", + }, + "after-l151": { + "headless": ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-l151-clang", + "renderer": ROOT / "Standalone/bin/linux/VoxelForgeOffscreenCapture-l151-clang", + "stamp": "1ca3b84f6d35f7f355ec0f72dabd3b87a455ea02e5789fea611916962e5db8e5", + }, +} +STAMP_RE = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]{64})$", re.M) + + +def fmt(values) -> str: + return ",".join(f"{value:.6f}" for value in values) + + +def angles(camera, target) -> tuple[float, float]: + dx, dy, dz = (target[index] - camera[index] for index in range(3)) + return math.degrees(math.atan2(dx, dy)), math.degrees( + math.atan2(dz, math.hypot(dx, dy))) + + +def point_q(layout: dict, values) -> tuple[float, float, float]: + scale = layout["core_voxel_size_um"] / (256.0 * 1_000_000.0) + return tuple(value * scale for value in values) + + +def chamber_views(layout: dict) -> list[dict]: + result = [] + for passage in layout["passages"]: + controls = passage["control_points"] + for side, endpoint, neighbor in ( + ("upper", controls[0], controls[1]), + ("lower", controls[-1], controls[-2])): + chamber = passage[f"{side}_landing_chamber"] + center = point_q(layout, chamber["floor_center_q"]) + a = point_q(layout, neighbor["position_q"]) + b = point_q(layout, endpoint["position_q"]) + dx, dy = a[0] - b[0], a[1] - b[1] + length = math.hypot(dx, dy) + if length <= 1.0e-9: + raise ValueError(f"degenerate {side} tunnel tangent: {passage['stable_id']}") + dx, dy = dx / length, dy / length + eye_z = center[2] + 1.45 + distance = 9.0 + target = (center[0] + dx * distance, center[1] + dy * distance, + eye_z + (chamber["floor_gradient_q16"][0] / 65536.0 * dx + + chamber["floor_gradient_q16"][1] / 65536.0 * dy) + * distance) + camera = (center[0], center[1], eye_z) + slug = passage["stable_id"].replace("passage/", "").replace("/", "-") + result.append({"name": f"{slug}-{side}-landing", "passage": passage["stable_id"], + "side": side, "camera": camera, "target": target, + "focus": (center[0], center[1], eye_z, 18.0)}) + return result + + +def stamped_run(name: str, command: list[object], log: Path, expected_stamp: str, + env: dict[str, str]) -> str: + binary = Path(str(command[0])) + probe = subprocess.run([*LIMIT, "timeout", "15s", str(binary), "--print-source-stamp"], + cwd=ROOT, env=env, text=True, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, check=False, timeout=20) + match = STAMP_RE.search(probe.stdout or "") + if probe.returncode or not match or match.group(1) != expected_stamp: + raise RuntimeError(f"{name}: source stamp mismatch for {binary}") + log.parent.mkdir(parents=True, exist_ok=True) + with log.open("w", encoding="utf-8") as stream: + result = subprocess.run([*LIMIT, "timeout", "1740s", *map(str, command)], + cwd=ROOT, env=env, stdout=stream, stderr=subprocess.STDOUT, + timeout=1745, check=False) + output = log.read_text(encoding="utf-8", errors="replace") + emitted = STAMP_RE.search(output) + if result.returncode or not emitted or emitted.group(1) != expected_stamp: + raise RuntimeError(f"{name}: export or render failed; see {log}") + return output + + +def resolve_layout(env: dict[str, str]) -> dict: + config = VERSIONS["after-l151"] + command = [config["headless"], DATA, "--world-assembler-prototype", + "--stream-benchmark", "--stream-pipeline", "--workers=12", + "--clip-radius=6", "--max-clip-level=0", "--max-level=0", + "--force-generate", "--seed=0", "--recipe=surface-world", "--resolved-layout-only", + f"--resolved-layout-output={LAYOUT}"] + stamped_run("after-l151-resolve-layout", list(map(str, command)), + OUT / "after-l151-resolved-layout.log", config["stamp"], env) + layout = json.loads(LAYOUT.read_text(encoding="utf-8")) + if len(layout.get("passages", [])) != 3: + raise RuntimeError("owner layout did not resolve the expected three tunnels") + return layout + + +def main() -> int: + if not DATA.is_file(): + raise SystemExit("missing owner fixture") + OUT.mkdir(parents=True, exist_ok=True) + ARTIFACTS.mkdir(parents=True, exist_ok=True) + env = os.environ.copy() + env["LP_NUM_THREADS"] = "12" + env["XDG_CACHE_HOME"] = str(OUT / "xdg-cache") + layout = resolve_layout(env) + views = chamber_views(layout) + quality_tool = ROOT / "Standalone/Tools/verify/capture_quality.py" + manifest = [] + failures = [] + for version, config in VERSIONS.items(): + for view in views: + camera, target = view["camera"], view["target"] + yaw, pitch = angles(camera, target) + mesh_dir = OUT / "meshes" / version + mesh_dir.mkdir(parents=True, exist_ok=True) + mesh = mesh_dir / f"{view['name']}.vfmesh" + capture_layout = mesh_dir / f"{view['name']}-layout.json" + export = [config["headless"], DATA, "--world-assembler-prototype", + "--stream-benchmark", "--stream-pipeline", "--workers=12", + "--clip-radius=6", "--max-clip-level=0", "--max-level=0", + "--force-generate", "--seed=0", "--recipe=surface-world", + "--render-distance-chunks=0", + f"--camera={fmt((*camera, yaw, pitch))}", + f"--resolved-layout-output={capture_layout}", f"--mesh-export={mesh}"] + stamped_run(f"{version}-{view['name']}-export", list(map(str, export)), + OUT / f"{version}-{view['name']}-export.log", config["stamp"], env) + if not mesh.is_file() or mesh.stat().st_size == 0: + raise RuntimeError(f"{version}-{view['name']}: empty mesh export") + png = OUT / f"{version}-{view['name']}.png" + jpg = ARTIFACTS / f"{version}-{view['name']}.jpg" + render = [config["renderer"], mesh, png, "--size=1280x720", + "--fov-deg=78.0", f"--camera-m={fmt(camera)}", + f"--yaw-deg={yaw:.6f}", f"--pitch-deg={pitch:.6f}", + "--tile-skirts=off", "--shade=readable", "--headlamp=on", + "--height-tint=on", "--height-center-m=-950", "--height-range-m=60", + "--tile-edges=off", f"--focus-region={fmt(view['focus'])}"] + stamped_run(f"{version}-{view['name']}-render", list(map(str, render)), + OUT / f"{version}-{view['name']}-render.log", config["stamp"], env) + subprocess.run([*LIMIT, "timeout", "120s", "ffmpeg", "-nostdin", + "-hide_banner", "-loglevel", "error", "-y", "-i", png, + "-frames:v", "1", "-q:v", "4", jpg], cwd=ROOT, env=env, + check=True, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT, + timeout=125) + check = subprocess.run([*LIMIT, "timeout", "120s", sys.executable, + quality_tool, jpg], cwd=ROOT, env=env, text=True, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + check=False, timeout=125) + quality_ok = check.returncode == 0 and "status=PASS" in check.stdout + if not quality_ok: + failures.append(f"{version}-{view['name']}") + quality_line = (check.stdout.strip().splitlines()[-1] + if check.stdout.strip() else f"quality exit={check.returncode}") + manifest.append({"version": version, "view": view["name"], + "passage": view["passage"], "side": view["side"], + "file": str(jpg.relative_to(ROOT)), "camera_m": camera, + "target_m": target, "yaw_deg": yaw, "pitch_deg": pitch, + "source_stamp": config["stamp"], + "pose_layout_identity": layout.get("stable_identity"), + "quality_status": "PASS" if quality_ok else "FAIL", + "quality": quality_line}) + print(f"[L151Capture] version={version} view={view['name']} " + f"quality={'PASS' if quality_ok else 'FAIL'} " + f"file={jpg.relative_to(ROOT)}", flush=True) + (ARTIFACTS / "manifest.json").write_text( + json.dumps(manifest, indent=2) + "\n", encoding="utf-8") + with (ARTIFACTS / "INDEX.md").open("w", encoding="utf-8") as index: + index.write("# L151 matched owner tunnel landing captures\n\n" + "Seed 0 camera poses resolved from the L151 owner layout and reused for both " + "versions. Each view is at eye height at one of the three tunnel landing " + "chambers. Before uses the L150 head build; after uses L151 source stamp " + f"`{VERSIONS['after-l151']['stamp']}`.\n\n") + for view in views: + index.write(f"## {view['passage']} / {view['side']} landing\n\n" + f"- Before: [image](before-l150-{view['name']}.jpg)\n" + f"- After: [image](after-l151-{view['name']}.jpg)\n\n") + index.write(f"Capture quality failures: {len(failures)}/{len(manifest)}.\n") + print(f"[L151CaptureSummary] chamber_views={len(views)} " + f"images={len(manifest)} quality_failures={len(failures)}", flush=True) + return 0 if not failures else 1 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except Exception as error: + print(f"[L151CaptureFailure] {error}", file=sys.stderr, flush=True) + raise SystemExit(1) diff --git a/Standalone/Research/WorldAssemblerM3/Linux151/captures/INDEX.md b/Standalone/Research/WorldAssemblerM3/Linux151/captures/INDEX.md new file mode 100644 index 0000000..40f36c3 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux151/captures/INDEX.md @@ -0,0 +1,35 @@ +# L151 matched owner tunnel landing captures + +Seed 0 camera poses resolved from the L151 owner layout and reused for both versions. Each view is at eye height at one of the three tunnel landing chambers. Before uses the L150 head build; after uses L151 source stamp `1ca3b84f6d35f7f355ec0f72dabd3b87a455ea02e5789fea611916962e5db8e5`. + +## passage/0-1/0 / upper landing + +- Before: [image](before-l150-0-1-0-upper-landing.jpg) +- After: [image](after-l151-0-1-0-upper-landing.jpg) + +## passage/0-1/0 / lower landing + +- Before: [image](before-l150-0-1-0-lower-landing.jpg) +- After: [image](after-l151-0-1-0-lower-landing.jpg) + +## passage/1-2/0 / upper landing + +- Before: [image](before-l150-1-2-0-upper-landing.jpg) +- After: [image](after-l151-1-2-0-upper-landing.jpg) + +## passage/1-2/0 / lower landing + +- Before: [image](before-l150-1-2-0-lower-landing.jpg) +- After: [image](after-l151-1-2-0-lower-landing.jpg) + +## passage/2-3/0 / upper landing + +- Before: [image](before-l150-2-3-0-upper-landing.jpg) +- After: [image](after-l151-2-3-0-upper-landing.jpg) + +## passage/2-3/0 / lower landing + +- Before: [image](before-l150-2-3-0-lower-landing.jpg) +- After: [image](after-l151-2-3-0-lower-landing.jpg) + +Capture quality failures: 0/12. diff --git a/Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-0-1-0-lower-landing.jpg b/Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-0-1-0-lower-landing.jpg new file mode 100644 index 0000000..c4013c0 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-0-1-0-lower-landing.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-0-1-0-upper-landing.jpg b/Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-0-1-0-upper-landing.jpg new file mode 100644 index 0000000..c4444f0 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-0-1-0-upper-landing.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-1-2-0-lower-landing.jpg b/Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-1-2-0-lower-landing.jpg new file mode 100644 index 0000000..aff38d6 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-1-2-0-lower-landing.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-1-2-0-upper-landing.jpg b/Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-1-2-0-upper-landing.jpg new file mode 100644 index 0000000..5763300 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-1-2-0-upper-landing.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-2-3-0-lower-landing.jpg b/Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-2-3-0-lower-landing.jpg new file mode 100644 index 0000000..7765a7f Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-2-3-0-lower-landing.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-2-3-0-upper-landing.jpg b/Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-2-3-0-upper-landing.jpg new file mode 100644 index 0000000..86ecb2b Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-2-3-0-upper-landing.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-0-1-0-lower-landing.jpg b/Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-0-1-0-lower-landing.jpg new file mode 100644 index 0000000..d7b41a1 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-0-1-0-lower-landing.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-0-1-0-upper-landing.jpg b/Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-0-1-0-upper-landing.jpg new file mode 100644 index 0000000..987fc02 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-0-1-0-upper-landing.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-1-2-0-lower-landing.jpg b/Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-1-2-0-lower-landing.jpg new file mode 100644 index 0000000..04133fe Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-1-2-0-lower-landing.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-1-2-0-upper-landing.jpg b/Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-1-2-0-upper-landing.jpg new file mode 100644 index 0000000..99596a4 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-1-2-0-upper-landing.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-2-3-0-lower-landing.jpg b/Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-2-3-0-lower-landing.jpg new file mode 100644 index 0000000..6125e0b Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-2-3-0-lower-landing.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-2-3-0-upper-landing.jpg b/Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-2-3-0-upper-landing.jpg new file mode 100644 index 0000000..95ef1b7 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-2-3-0-upper-landing.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux151/captures/manifest.json b/Standalone/Research/WorldAssemblerM3/Linux151/captures/manifest.json new file mode 100644 index 0000000..ee29d68 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux151/captures/manifest.json @@ -0,0 +1,278 @@ +[ + { + "version": "before-l150", + "view": "0-1-0-upper-landing", + "passage": "passage/0-1/0", + "side": "upper", + "file": "Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-0-1-0-upper-landing.jpg", + "camera_m": [ + 37.0, + -5.25, + -372.3810546875 + ], + "target_m": [ + 28.40474992442715, + -7.918646874047773, + -372.325197471535 + ], + "yaw_deg": -107.24848481126237, + "pitch_deg": 0.3555935154874107, + "source_stamp": "c6b8a100355e271ddc40eb5973569d2ff4e2ba8d9c628c72f8e0af2493f0609f", + "pose_layout_identity": "resolved-layout-v3-fnv64:ed02312296ac2003", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-0-1-0-upper-landing.jpg pixels=921600 mean_luma=113.553 stddev_luma=8.807 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "version": "before-l150", + "view": "0-1-0-lower-landing", + "passage": "passage/0-1/0", + "side": "lower", + "file": "Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-0-1-0-lower-landing.jpg", + "camera_m": [ + 25.8369140625, + 3.19921875, + -736.6798828125 + ], + "target_m": [ + 17.97380590951016, + -1.1793118453464206, + -734.970169262755 + ], + "yaw_deg": -119.11101808688004, + "pitch_deg": 10.756207017349817, + "source_stamp": "c6b8a100355e271ddc40eb5973569d2ff4e2ba8d9c628c72f8e0af2493f0609f", + "pose_layout_identity": "resolved-layout-v3-fnv64:ed02312296ac2003", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-0-1-0-lower-landing.jpg pixels=921600 mean_luma=106.418 stddev_luma=2.881 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "version": "before-l150", + "view": "1-2-0-upper-landing", + "passage": "passage/1-2/0", + "side": "upper", + "file": "Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-1-2-0-upper-landing.jpg", + "camera_m": [ + 19.166015625, + -14.2001953125, + -731.8810546875 + ], + "target_m": [ + 10.35825176774404, + -16.050410390488395, + -733.216132282697 + ], + "yaw_deg": -101.86342177874246, + "pitch_deg": -8.437834550031965, + "source_stamp": "c6b8a100355e271ddc40eb5973569d2ff4e2ba8d9c628c72f8e0af2493f0609f", + "pose_layout_identity": "resolved-layout-v3-fnv64:ed02312296ac2003", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-1-2-0-upper-landing.jpg pixels=921600 mean_luma=108.420 stddev_luma=2.873 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "version": "before-l150", + "view": "1-2-0-lower-landing", + "passage": "passage/1-2/0", + "side": "lower", + "file": "Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-1-2-0-lower-landing.jpg", + "camera_m": [ + 19.166015625, + -14.2001953125, + -753.05390625 + ], + "target_m": [ + 10.48848361510052, + -16.587754362485967, + -751.7193175939389 + ], + "yaw_deg": -105.38384894032598, + "pitch_deg": 8.434788863627224, + "source_stamp": "c6b8a100355e271ddc40eb5973569d2ff4e2ba8d9c628c72f8e0af2493f0609f", + "pose_layout_identity": "resolved-layout-v3-fnv64:ed02312296ac2003", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-1-2-0-lower-landing.jpg pixels=921600 mean_luma=113.918 stddev_luma=10.489 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "version": "before-l150", + "view": "2-3-0-upper-landing", + "passage": "passage/2-3/0", + "side": "upper", + "file": "Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-2-3-0-upper-landing.jpg", + "camera_m": [ + -20.0625, + -5.068359375, + -812.075390625 + ], + "target_m": [ + -13.604954164712868, + -11.337338700629157, + -813.6662645746064 + ], + "yaw_deg": 134.15112141809087, + "pitch_deg": -10.024269857937275, + "source_stamp": "c6b8a100355e271ddc40eb5973569d2ff4e2ba8d9c628c72f8e0af2493f0609f", + "pose_layout_identity": "resolved-layout-v3-fnv64:ed02312296ac2003", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-2-3-0-upper-landing.jpg pixels=921600 mean_luma=108.739 stddev_luma=2.947 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "version": "before-l150", + "view": "2-3-0-lower-landing", + "passage": "passage/2-3/0", + "side": "lower", + "file": "Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-2-3-0-lower-landing.jpg", + "camera_m": [ + -20.0625, + -5.068359375, + -900.0333984375 + ], + "target_m": [ + -14.865342978484083, + -12.416118141706198, + -898.4427583280197 + ], + "yaw_deg": 144.72777711381306, + "pitch_deg": 10.022826283920647, + "source_stamp": "c6b8a100355e271ddc40eb5973569d2ff4e2ba8d9c628c72f8e0af2493f0609f", + "pose_layout_identity": "resolved-layout-v3-fnv64:ed02312296ac2003", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-2-3-0-lower-landing.jpg pixels=921600 mean_luma=112.176 stddev_luma=9.871 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "version": "after-l151", + "view": "0-1-0-upper-landing", + "passage": "passage/0-1/0", + "side": "upper", + "file": "Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-0-1-0-upper-landing.jpg", + "camera_m": [ + 37.0, + -5.25, + -372.3810546875 + ], + "target_m": [ + 28.40474992442715, + -7.918646874047773, + -372.325197471535 + ], + "yaw_deg": -107.24848481126237, + "pitch_deg": 0.3555935154874107, + "source_stamp": "1ca3b84f6d35f7f355ec0f72dabd3b87a455ea02e5789fea611916962e5db8e5", + "pose_layout_identity": "resolved-layout-v3-fnv64:ed02312296ac2003", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-0-1-0-upper-landing.jpg pixels=921600 mean_luma=118.392 stddev_luma=15.921 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "version": "after-l151", + "view": "0-1-0-lower-landing", + "passage": "passage/0-1/0", + "side": "lower", + "file": "Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-0-1-0-lower-landing.jpg", + "camera_m": [ + 25.8369140625, + 3.19921875, + -736.6798828125 + ], + "target_m": [ + 17.97380590951016, + -1.1793118453464206, + -734.970169262755 + ], + "yaw_deg": -119.11101808688004, + "pitch_deg": 10.756207017349817, + "source_stamp": "1ca3b84f6d35f7f355ec0f72dabd3b87a455ea02e5789fea611916962e5db8e5", + "pose_layout_identity": "resolved-layout-v3-fnv64:ed02312296ac2003", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-0-1-0-lower-landing.jpg pixels=921600 mean_luma=111.426 stddev_luma=12.411 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "version": "after-l151", + "view": "1-2-0-upper-landing", + "passage": "passage/1-2/0", + "side": "upper", + "file": "Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-1-2-0-upper-landing.jpg", + "camera_m": [ + 19.166015625, + -14.2001953125, + -731.8810546875 + ], + "target_m": [ + 10.35825176774404, + -16.050410390488395, + -733.216132282697 + ], + "yaw_deg": -101.86342177874246, + "pitch_deg": -8.437834550031965, + "source_stamp": "1ca3b84f6d35f7f355ec0f72dabd3b87a455ea02e5789fea611916962e5db8e5", + "pose_layout_identity": "resolved-layout-v3-fnv64:ed02312296ac2003", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-1-2-0-upper-landing.jpg pixels=921600 mean_luma=117.134 stddev_luma=15.664 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "version": "after-l151", + "view": "1-2-0-lower-landing", + "passage": "passage/1-2/0", + "side": "lower", + "file": "Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-1-2-0-lower-landing.jpg", + "camera_m": [ + 19.166015625, + -14.2001953125, + -753.05390625 + ], + "target_m": [ + 10.48848361510052, + -16.587754362485967, + -751.7193175939389 + ], + "yaw_deg": -105.38384894032598, + "pitch_deg": 8.434788863627224, + "source_stamp": "1ca3b84f6d35f7f355ec0f72dabd3b87a455ea02e5789fea611916962e5db8e5", + "pose_layout_identity": "resolved-layout-v3-fnv64:ed02312296ac2003", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-1-2-0-lower-landing.jpg pixels=921600 mean_luma=119.710 stddev_luma=14.975 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "version": "after-l151", + "view": "2-3-0-upper-landing", + "passage": "passage/2-3/0", + "side": "upper", + "file": "Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-2-3-0-upper-landing.jpg", + "camera_m": [ + -20.0625, + -5.068359375, + -812.075390625 + ], + "target_m": [ + -13.604954164712868, + -11.337338700629157, + -813.6662645746064 + ], + "yaw_deg": 134.15112141809087, + "pitch_deg": -10.024269857937275, + "source_stamp": "1ca3b84f6d35f7f355ec0f72dabd3b87a455ea02e5789fea611916962e5db8e5", + "pose_layout_identity": "resolved-layout-v3-fnv64:ed02312296ac2003", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-2-3-0-upper-landing.jpg pixels=921600 mean_luma=118.198 stddev_luma=15.882 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "version": "after-l151", + "view": "2-3-0-lower-landing", + "passage": "passage/2-3/0", + "side": "lower", + "file": "Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-2-3-0-lower-landing.jpg", + "camera_m": [ + -20.0625, + -5.068359375, + -900.0333984375 + ], + "target_m": [ + -14.865342978484083, + -12.416118141706198, + -898.4427583280197 + ], + "yaw_deg": 144.72777711381306, + "pitch_deg": 10.022826283920647, + "source_stamp": "1ca3b84f6d35f7f355ec0f72dabd3b87a455ea02e5789fea611916962e5db8e5", + "pose_layout_identity": "resolved-layout-v3-fnv64:ed02312296ac2003", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-2-3-0-lower-landing.jpg pixels=921600 mean_luma=121.734 stddev_luma=16.193 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + } +] diff --git a/Standalone/Source/StreamBenchmarkPipeline.h b/Standalone/Source/StreamBenchmarkPipeline.h index 48b5a30..ae028b7 100644 --- a/Standalone/Source/StreamBenchmarkPipeline.h +++ b/Standalone/Source/StreamBenchmarkPipeline.h @@ -3090,7 +3090,16 @@ namespace StandaloneStreamBenchmark const double horizontal=std::hypot(dx,dy)*coreMetres; const double vertical=std::abs(dz)*coreMetres; if(horizontal<=1.0e-6||vertical>horizontal*0.2679491924311227) + { + if(audit.gradeFailures<8) + std::printf("[ResolvedWalkGradeFailure] segment=%zu a_xyz_m=(%.3f,%.3f,%.3f) b_xyz_m=(%.3f,%.3f,%.3f) horizontal_m=%.6f vertical_m=%.6f grade_deg=%.3f\n", + segment-1,a[0]*coreMetres,a[1]*coreMetres, + a[2]*coreMetres,b[0]*coreMetres,b[1]*coreMetres, + b[2]*coreMetres,horizontal,vertical, + std::atan2(vertical,horizontal) + *180.0/3.14159265358979323846); ++audit.gradeFailures; + } const double length=std::sqrt(dx*dx+dy*dy+dz*dz)*coreMetres; const std::uint64_t intervals=std::max(1, static_cast(std::ceil(length/0.5))); @@ -3193,7 +3202,8 @@ namespace StandaloneStreamBenchmark -qToMetres(passage.upperMouthSubvoxels[2])+mouthCenterHeight); if(upperStratum==ResolvedLayout->strates.end() ||(upperStratum->generatorKind!="FloatingIslands" - &&upperStratum->generatorKind!="TunnelNetwork")) + &&upperStratum->generatorKind!="TunnelNetwork" + &&upperStratum->generatorKind!="SurfaceWorld")) return authoredMouthDepth; std::vector> route; for(const auto& anchor:upperStratum->anchors) @@ -3246,7 +3256,8 @@ namespace StandaloneStreamBenchmark {return candidate.strateIndex==strateIndex;}); if(stratum!=ResolvedLayout->strates.end() &&(stratum->generatorKind=="TunnelNetwork" - ||stratum->generatorKind=="FloatingIslands")) + ||stratum->generatorKind=="FloatingIslands" + ||stratum->generatorKind=="SurfaceWorld")) for(const auto& anchor:stratum->anchors) if(anchor.kind=="route_point"&&anchor.values.size()>=3) route.push_back({qToMetres(anchor.values[0]), @@ -3256,17 +3267,20 @@ namespace StandaloneStreamBenchmark const auto upper=std::find_if(ResolvedLayout->strates.begin(), ResolvedLayout->strates.end(),[&](const auto& stratum) {return stratum.strateIndex==passage.upperStrateIndex;}); - if(upper!=ResolvedLayout->strates.end() - &&upper->generatorKind=="TunnelNetwork"&&!controls.empty()) + if(upper!=ResolvedLayout->strates.end()&&!controls.empty() + &&(upper->generatorKind=="TunnelNetwork" + ||upper->generatorKind=="SurfaceWorld")) { auto route=routeForStrate(passage.upperStrateIndex); - if(route.size()>2)route.erase(route.begin(),route.end()-2); + if(upper->generatorKind=="TunnelNetwork"&&route.size()>2) + route.erase(route.begin(),route.end()-2); double routeHeight=0.0,distanceSquared=0.0; if(VoxelForgeCore::PassageSpline::ClosestRouteHeightAtXY(route, controls.front()[0],controls.front()[1],routeHeight,distanceSquared) &&distanceSquared<=std::pow(controls.front()[3]+2.0,2.0)) VoxelForgeCore::PassageSpline::BlendEndpointFloorToRoute( - controls,profile,0,route,1.0); + controls,profile,0,route,1.0, + upper->generatorKind=="SurfaceWorld"?0.98:1.13); } const auto lower=std::find_if(ResolvedLayout->strates.begin(), ResolvedLayout->strates.end(),[&](const auto& stratum) @@ -3325,6 +3339,44 @@ namespace StandaloneStreamBenchmark double x=0.0,y=0.0,z=0.0,floorZ=0.0,radius=0.0,distance=0.0; std::size_t segmentIndex=0; }; + const auto blendLandingChamberFloor=[&]( + const VoxelForgeCore::ResolvedLayoutPassageV1& passage, + double alongMetres,double totalLengthMetres,double x,double y, + double plannedFloor) + { + constexpr double LandingFloorBlendMetres=12.0; + const auto weight=[&](double distanceMetres, + const VoxelForgeCore::ResolvedLayoutPassageLandingChamberV1& chamber) + { + const double radius=qToMetres(chamber.radiusSubvoxels); + if(!(radius>0.0))return 0.0; + if(distanceMetres<=radius)return 1.0; + if(distanceMetres>=radius+LandingFloorBlendMetres)return 0.0; + const double t=(distanceMetres-radius)/LandingFloorBlendMetres; + return 1.0-t*t*(3.0-2.0*t); + }; + const double upperWeight=weight(std::max(0.0,alongMetres), + passage.upperLandingChamber); + const double lowerWeight=weight(std::max(0.0, + totalLengthMetres-alongMetres),passage.lowerLandingChamber); + if(upperWeight<=0.0&&lowerWeight<=0.0)return plannedFloor; + const auto floorAt=[&]( + const VoxelForgeCore::ResolvedLayoutPassageLandingChamberV1& chamber) + { + const double cx=qToMetres(chamber.floorCenterSubvoxels[0]); + const double cy=qToMetres(chamber.floorCenterSubvoxels[1]); + const double cz=qToMetres(chamber.floorCenterSubvoxels[2]); + return cz+chamber.floorGradientQ16[0]/65536.0*(x-cx) + +chamber.floorGradientQ16[1]/65536.0*(y-cy); + }; + const double upperFloor=floorAt(passage.upperLandingChamber); + const double lowerFloor=floorAt(passage.lowerLandingChamber); + const double totalWeight=upperWeight+lowerWeight; + if(totalWeight<=1.0) + return plannedFloor*(1.0-totalWeight) + +upperFloor*upperWeight+lowerFloor*lowerWeight; + return (upperFloor*upperWeight+lowerFloor*lowerWeight)/totalWeight; + }; const auto passageSmoothRoute=[&]( const VoxelForgeCore::ResolvedLayoutPassageV1& passage,double bodyDepth) { @@ -3354,6 +3406,16 @@ namespace StandaloneStreamBenchmark segment.alongMetres+segment.lengthMetres, segment.controlSpan+1}); } + if(route.size()>=2) + { + const double totalLength=route.back().distance; + for(auto& point:route) + { + point.floorZ=blendLandingChamberFloor(passage,point.distance, + totalLength,point.x,point.y,point.floorZ); + point.z=point.floorZ+passageTubeDepthAt(point.radius,bodyDepth); + } + } return route; }; std::vector allPassageBodyFloorDepths; @@ -3389,7 +3451,8 @@ namespace StandaloneStreamBenchmark {return stratum.strateIndex==passage.upperStrateIndex;}); if(upperStratum!=ResolvedLayout->strates.end() &&(upperStratum->generatorKind=="FloatingIslands" - ||upperStratum->generatorKind=="TunnelNetwork")) + ||upperStratum->generatorKind=="TunnelNetwork" + ||upperStratum->generatorKind=="SurfaceWorld")) { for(const auto& anchor:upperStratum->anchors) if(anchor.kind=="route_point"&&anchor.values.size()>=3) @@ -3466,16 +3529,49 @@ namespace StandaloneStreamBenchmark if(passageIndex>=allPassageFloorBlends.size())return plannedFloor; const auto& blends=allPassageFloorBlends[passageIndex]; double floor=plannedFloor; + const auto upperStratum=std::find_if(ResolvedLayout->strates.begin(), + ResolvedLayout->strates.end(),[&](const auto& stratum) + {return stratum.strateIndex + ==ResolvedLayout->passages[passageIndex].upperStrateIndex;}); + const double upperPlayerCenterHeight=upperStratum + !=ResolvedLayout->strates.end() + &&upperStratum->generatorKind=="SurfaceWorld"?0.98:1.13; floor=VoxelForgeCore::PassageSpline::BlendUpperMouthFloorAtXY( blends.upper.receivingRoute,x,y,floor,blends.upper.mouthX, blends.upper.mouthY,blends.upper.mouthFloor, blends.upper.tangentX,blends.upper.tangentY, - VoxelForgeCore::PassageSpline::UpperMouthFloorBlendRadiusMetres,1.13); - return VoxelForgeCore::PassageSpline::BlendUpperMouthFloorAtXY( + VoxelForgeCore::PassageSpline::UpperMouthFloorBlendRadiusMetres, + upperPlayerCenterHeight); + floor=VoxelForgeCore::PassageSpline::BlendUpperMouthFloorAtXY( blends.lower.receivingRoute,x,y,floor,blends.lower.mouthX, blends.lower.mouthY,blends.lower.mouthFloor, -blends.lower.tangentX,-blends.lower.tangentY, VoxelForgeCore::PassageSpline::UpperMouthFloorBlendRadiusMetres,1.13); + if(passageIndex>=allPassageSmoothRoutes.size())return floor; + const auto& route=allPassageSmoothRoutes[passageIndex]; + if(route.size()<2)return floor; + double closestDistanceSquared=std::numeric_limits::infinity(); + double closestAlong=0.0; + for(std::size_t index=1;index1.0e-12 + ?std::clamp(((x-a.x)*dx+(y-a.y)*dy)/lengthSquared,0.0,1.0):0.0; + const double nearestX=a.x+t*dx,nearestY=a.y+t*dy; + const double ex=x-nearestX,ey=y-nearestY; + const double distanceSquared=ex*ex+ey*ey; + if(distanceSquaredpassages[passageIndex],closestAlong, + route.back().distance,x,y,floor); }; const auto interpolatePassageRoute=[]( const std::vector& route,double distance) @@ -3627,6 +3723,26 @@ namespace StandaloneStreamBenchmark &&passageIndex!=selectedPassage)continue; const auto& passage=ResolvedLayout->passages[passageIndex]; if(passage.controlPoints.size()<2)continue; + const auto inLandingChamber=[&]( + const VoxelForgeCore::ResolvedLayoutPassageLandingChamberV1& chamber) + { + if(chamber.radiusSubvoxels<=0||chamber.clearHeightSubvoxels<=0) + return false; + const double centerX=qToMetres(chamber.floorCenterSubvoxels[0]); + const double centerY=qToMetres(chamber.floorCenterSubvoxels[1]); + const double floorCenterZ=qToMetres( + chamber.floorCenterSubvoxels[2]) + +chamber.floorGradientQ16[0]/65536.0*(xMetres-centerX) + +chamber.floorGradientQ16[1]/65536.0*(yMetres-centerY); + const double radialDistance=std::hypot(xMetres-centerX, + yMetres-centerY)-qToMetres(chamber.radiusSubvoxels); + const double floorDistance=floorCenterZ-zMetres; + const double ceilingDistance=zMetres-(floorCenterZ + +qToMetres(chamber.clearHeightSubvoxels)); + return std::max({radialDistance,floorDistance,ceilingDistance})<=0.0; + }; + if(inLandingChamber(passage.upperLandingChamber) + ||inLandingChamber(passage.lowerLandingChamber))return true; const auto& route=allPassageSmoothRoutes[passageIndex]; if(route.size()<2)continue; double bestRoundDistance=std::numeric_limits::infinity(); @@ -4580,6 +4696,78 @@ namespace StandaloneStreamBenchmark [index](const auto& s){return s.strateIndex==index;}); return it==ResolvedLayout->strates.end()?nullptr:&*it; }; + const auto landingChamberConnectionContains=[&]( + const VoxelForgeCore::ResolvedLayoutPassageV1& passage,bool upper, + double xMetres,double yMetres,double zMetres) + { + const auto& chamber=upper?passage.upperLandingChamber + :passage.lowerLandingChamber; + const auto* stratum=findStratum(upper?passage.upperStrateIndex + :passage.lowerStrateIndex); + if(!stratum||chamber.radiusSubvoxels<=0)return false; + std::vector route; + for(const auto& anchor:stratum->anchors) + if(anchor.kind=="route_point"&&anchor.values.size()>=3) + route.push_back(&anchor); + if(route.size()<2)return false; + const double cx=qToMetres(chamber.floorCenterSubvoxels[0]); + const double cy=qToMetres(chamber.floorCenterSubvoxels[1]); + const double mouthZ=qToMetres(upper?passage.upperMouthSubvoxels[2] + :passage.lowerMouthSubvoxels[2]); + std::size_t endpoint=route.size(); + double endpointDistanceSquared=1.0; + for(std::size_t index=0;indexvalues[0])-cx; + const double dy=qToMetres(route[index]->values[1])-cy; + const double z=qToMetres(route[index]->values[2]); + const double distanceSquared=dx*dx+dy*dy; + if(distanceSquared routeDistance(route.size(),0.0); + for(std::size_t index=1;indexvalues[0] + -route[index-1]->values[0]); + const double dy=qToMetres(route[index]->values[1] + -route[index-1]->values[1]); + routeDistance[index]=routeDistance[index-1]+std::hypot(dx,dy); + } + for(std::size_t index=1;index9.0)continue; + const double routeX=ax+t*dx,routeY=ay+t*dy; + const double routeZ=az+t*qToMetres(b.values[2]-a.values[2]); + const double lateralDistance=std::hypot(xMetres-routeX, + yMetres-routeY); + if(stratum->generatorKind=="TunnelNetwork" + ||stratum->generatorKind=="VerticalShafts") + { + const double routeFloor=routeZ-1.13; + if(lateralDistance<=0.80&&zMetres>=routeFloor-0.30 + &&zMetres<=routeFloor+2.25)return true; + } + else if(lateralDistance<=1.25 + &&std::abs(zMetres-routeZ)<=1.5)return true; + } + return false; + }; const auto controlSnapshot=CoreSnapshot.withoutResolvedLayoutPassages(); VoxelForgeCore::World blockedWorld(controlSnapshot); std::string blockedCompileError; @@ -4596,6 +4784,9 @@ namespace StandaloneStreamBenchmark { std::uint64_t floorSamples=0,floorViolations=0,openAirSamples=0; std::uint64_t wallSamples=0,wallViolations=0; + std::uint64_t chamberFloorSamples=0,chamberFloorViolations=0; + std::uint64_t chamberWallSamples=0,chamberWallViolations=0; + std::uint64_t chamberConnectionSkips=0; std::uint64_t floorInteriorWins=0,wallInteriorWins=0; std::uint64_t floorMouthBlendSkips=0,wallMouthBlendSkips=0; std::uint64_t interiorSamples=0,interiorViolations=0; @@ -4782,6 +4973,27 @@ namespace StandaloneStreamBenchmark shellDistance); } } + constexpr double LandingChamberShellThicknessMetres=0.75; + for(const auto& passage:ResolvedLayout->passages) + for(const auto* chamber:{&passage.upperLandingChamber, + &passage.lowerLandingChamber}) + { + if(chamber->radiusSubvoxels<=0)continue; + const double centerX=qToMetres(chamber->floorCenterSubvoxels[0]); + const double centerY=qToMetres(chamber->floorCenterSubvoxels[1]); + const double floorCenterZ=qToMetres( + chamber->floorCenterSubvoxels[2]) + +chamber->floorGradientQ16[0]/65536.0*(xMetres-centerX) + +chamber->floorGradientQ16[1]/65536.0*(yMetres-centerY); + const double radialDistance=std::hypot(xMetres-centerX, + yMetres-centerY)-qToMetres(chamber->radiusSubvoxels); + const double floorDistance=floorCenterZ-zMetres; + const double shellDistance=std::max({-floorDistance, + floorDistance-LandingChamberShellThicknessMetres, + radialDistance}); + minimumShellDistance=std::min(minimumShellDistance, + shellDistance); + } return minimumShellDistance; }; for(std::size_t segment=1;segment chamberFloorRadii{{0.0,2.5,5.0}}; + constexpr std::array chamberWallHeights{{1.13,3.20}}; + constexpr std::size_t ChamberAngularSamples=16; + for(const bool upper:{true,false}) + { + const auto& chamber=upper?passage.upperLandingChamber + :passage.lowerLandingChamber; + if(chamber.radiusSubvoxels<=0||chamber.clearHeightSubvoxels<=0)continue; + const double centerX=qToMetres(chamber.floorCenterSubvoxels[0]); + const double centerY=qToMetres(chamber.floorCenterSubvoxels[1]); + const double baseFloorZ=qToMetres(chamber.floorCenterSubvoxels[2]); + const double radius=qToMetres(chamber.radiusSubvoxels); + const double gx=chamber.floorGradientQ16[0]/65536.0; + const double gy=chamber.floorGradientQ16[1]/65536.0; + for(const double radial:chamberFloorRadii) + { + if(radial>=radius-1.0)continue; + const std::size_t angles=radial==0.0?1:ChamberAngularSamples; + for(std::size_t angleIndex=0;angleIndex(angleIndex)/ChamberAngularSamples; + const double sampleX=centerX+radial*std::cos(angle); + const double sampleY=centerY+radial*std::sin(angle); + const double floorZ=baseFloorZ+gx*(sampleX-centerX) + +gy*(sampleY-centerY); + for(const double depth:PassageShellAuditDepthsMetres) + { + const double sampleZ=floorZ-depth; + const VoxelForgeCore::Vec3d position{sampleX/coreMetres, + sampleY/coreMetres,sampleZ/coreMetres}; + if(Native.densityAt(position)<0.0f)continue; + if(landingChamberConnectionContains(passage,upper, + sampleX,sampleY,sampleZ)) + {++audit.chamberConnectionSkips;continue;} + ++audit.chamberFloorSamples; + const float density=Field.densityAt(position); + if(density>=PassageShellDensityTolerance) + { + ++audit.chamberFloorViolations; + std::printf("[PassageLandingChamberShellSample] id=%s side=%s kind=floor_missing xyz_m=(%.3f,%.3f,%.3f) native=%.6f density=%.6f\n", + passage.stableId.c_str(),upper?"upper":"lower", + sampleX,sampleY,sampleZ,Native.densityAt(position),density); + } + } + } + } + for(std::size_t angleIndex=0;angleIndex(angleIndex)/ChamberAngularSamples; + const double nx=std::cos(angle),ny=std::sin(angle); + for(const double depth:PassageShellAuditDepthsMetres) + for(const double height:chamberWallHeights) + { + const double sampleX=centerX+(radius+depth)*nx; + const double sampleY=centerY+(radius+depth)*ny; + const double floorZ=baseFloorZ+gx*(sampleX-centerX) + +gy*(sampleY-centerY); + const double sampleZ=floorZ+height; + const VoxelForgeCore::Vec3d position{sampleX/coreMetres, + sampleY/coreMetres,sampleZ/coreMetres}; + if(Native.densityAt(position)<0.0f)continue; + if(passageInteriorWinsAt(sampleX,sampleY,sampleZ, + std::numeric_limits::max(), + std::numeric_limits::max(),passageIndex) + ||landingChamberConnectionContains(passage,upper, + sampleX,sampleY,sampleZ)) + {++audit.chamberConnectionSkips;continue;} + ++audit.chamberWallSamples; + const float density=Field.densityAt(position); + if(density>=PassageShellDensityTolerance) + { + ++audit.chamberWallViolations; + std::printf("[PassageLandingChamberShellSample] id=%s side=%s kind=wall_missing xyz_m=(%.3f,%.3f,%.3f) native=%.6f density=%.6f radial_m=%.3f height_m=%.3f\n", + passage.stableId.c_str(),upper?"upper":"lower", + sampleX,sampleY,sampleZ,Native.densityAt(position),density, + radius+depth,height); + } + } + } + } return audit; }; const auto mouthInContent=[&](std::int32_t index, @@ -4947,14 +5242,74 @@ namespace StandaloneStreamBenchmark return zUm>=stratum->content.bottomMicrometres &&zUmcontent.topMicrometres; }; + const auto auditLandingChamber=[&](const VoxelForgeCore::World& field, + const VoxelForgeCore::ResolvedLayoutPassageV1& passage, + std::size_t passageIndex,bool upper) + { + FWalkAudit total;total.passed=true; + const auto& chamber=upper?passage.upperLandingChamber + :passage.lowerLandingChamber; + if(chamber.radiusSubvoxels<=0||chamber.clearHeightSubvoxels<=0) + {total.passed=false;return total;} + constexpr std::array,8> directions{{ + {{1.0,0.0}},{{0.7071067811865475,0.7071067811865475}}, + {{0.0,1.0}},{{-0.7071067811865475,0.7071067811865475}}, + {{-1.0,0.0}},{{-0.7071067811865475,-0.7071067811865475}}, + {{0.0,-1.0}},{{0.7071067811865475,-0.7071067811865475}}}}; + const double centerX=qToMetres(chamber.floorCenterSubvoxels[0]); + const double centerY=qToMetres(chamber.floorCenterSubvoxels[1]); + const double centerZ=qToMetres(chamber.floorCenterSubvoxels[2]); + const double radius=std::min(5.75, + qToMetres(chamber.radiusSubvoxels)-1.5); + const double gx=chamber.floorGradientQ16[0]/65536.0; + const double gy=chamber.floorGradientQ16[1]/65536.0; + const std::function interiorPredicate= + [&](double x,double y,double z) + {return passageInteriorWinsAt(x,y,z, + std::numeric_limits::max(), + std::numeric_limits::max(),passageIndex);}; + for(const auto& direction:directions) + { + std::vector> chamberPath; + const std::size_t intervals=std::max(1, + static_cast(std::ceil(radius/0.5))); + chamberPath.reserve(intervals+1); + for(std::size_t step=0;step<=intervals;++step) + { + const double distance=radius*step/intervals; + const double x=centerX+direction[0]*distance; + const double y=centerY+direction[1]*distance; + const double floorZ=centerZ+gx*(x-centerX)+gy*(y-centerY); + chamberPath.push_back({x/coreMetres,y/coreMetres, + (floorZ+1.13)/coreMetres}); + } + const FWalkAudit part=auditPlayerPath(field,chamberPath, + 0.125,0.125,1.125,3,0.45,{},interiorPredicate); + total.segments+=part.segments;total.centers+=part.centers; + total.capsuleSamples+=part.capsuleSamples; + total.blocked+=part.blocked; + total.passageInteriorCapsuleSamples+=part.passageInteriorCapsuleSamples; + total.passageInteriorSolidSamples+=part.passageInteriorSolidSamples; + total.supportSamples+=part.supportSamples; + total.unsupported+=part.unsupported; + total.gradeFailures+=part.gradeFailures; + total.supportOverlapSkips+=part.supportOverlapSkips; + total.passed=total.passed&&part.passed; + } + return total; + }; std::uint64_t passageCount=0,passagePasses=0,blockedControls=0,naturalPasses=0; std::uint64_t passageInteriorCapsuleSamples=0,passageInteriorSolidSamples=0; std::uint64_t passageInteriorControls=0; + std::uint64_t landingChamberCount=0,landingChamberNegativeControls=0; std::uint64_t mouthProfilePasses=0,ripplePasses=0,rippleControlsDetected=0; std::uint64_t floorRippleControlsDetected=0,floorStepRippleControlsDetected=0; std::uint64_t shellAudits=0,shellFloorSamples=0,shellFloorViolations=0; std::uint64_t shellFloorMouthBlendSkips=0; std::uint64_t shellOpenAirSamples=0,shellWallSamples=0,shellWallViolations=0; + std::uint64_t shellChamberFloorSamples=0,shellChamberFloorViolations=0; + std::uint64_t shellChamberWallSamples=0,shellChamberWallViolations=0; + std::uint64_t shellChamberConnectionSkips=0; std::uint64_t shellInteriorSamples=0,shellInteriorViolations=0; for(std::size_t passageIndex=0;passageIndexpassages.size();++passageIndex) { @@ -4980,11 +5335,26 @@ namespace StandaloneStreamBenchmark std::numeric_limits::max(),passageIndex);}; const FWalkAudit passageAudit=auditPlayerPath(Shape,path,0.375,0.125, 1.125,3,0.45,passageSupportExemption,passageInteriorPredicate); + const FWalkAudit upperChamberAudit=auditLandingChamber(Shape, + passage,passageIndex,true); + const FWalkAudit lowerChamberAudit=auditLandingChamber(Shape, + passage,passageIndex,false); + const bool landingChambersWalkable=upperChamberAudit.passed + &&lowerChamberAudit.passed; const bool passageInteriorAirPassed= passageAudit.passageInteriorCapsuleSamples>0 - &&passageAudit.passageInteriorSolidSamples==0; - passageInteriorCapsuleSamples+=passageAudit.passageInteriorCapsuleSamples; - passageInteriorSolidSamples+=passageAudit.passageInteriorSolidSamples; + &&passageAudit.passageInteriorSolidSamples==0 + &&upperChamberAudit.passageInteriorCapsuleSamples>0 + &&lowerChamberAudit.passageInteriorCapsuleSamples>0 + &&upperChamberAudit.passageInteriorSolidSamples==0 + &&lowerChamberAudit.passageInteriorSolidSamples==0; + passageInteriorCapsuleSamples+=passageAudit.passageInteriorCapsuleSamples + +upperChamberAudit.passageInteriorCapsuleSamples + +lowerChamberAudit.passageInteriorCapsuleSamples; + passageInteriorSolidSamples+=passageAudit.passageInteriorSolidSamples + +upperChamberAudit.passageInteriorSolidSamples + +lowerChamberAudit.passageInteriorSolidSamples; + landingChamberCount+=2; const FPassageNaturalnessAudit naturalAudit= auditPassageNaturalness(Shape,passageIndex); const auto lowerProfileStratum=std::find_if(ResolvedLayout->strates.begin(), @@ -5027,6 +5397,11 @@ namespace StandaloneStreamBenchmark shellOpenAirSamples+=shellAudit.openAirSamples; shellWallSamples+=shellAudit.wallSamples; shellWallViolations+=shellAudit.wallViolations; + shellChamberFloorSamples+=shellAudit.chamberFloorSamples; + shellChamberFloorViolations+=shellAudit.chamberFloorViolations; + shellChamberWallSamples+=shellAudit.chamberWallSamples; + shellChamberWallViolations+=shellAudit.chamberWallViolations; + shellChamberConnectionSkips+=shellAudit.chamberConnectionSkips; shellInteriorSamples+=shellAudit.interiorSamples; shellInteriorViolations+=shellAudit.interiorViolations; const auto radialSquared=[](const std::array& p) @@ -5043,7 +5418,8 @@ namespace StandaloneStreamBenchmark rippleControlsDetected+=rippleAudit.controlDetected; floorRippleControlsDetected+=rippleAudit.floorControlDetected; floorStepRippleControlsDetected+=rippleAudit.floorStepControlDetected; - passagePasses+=passageAudit.passed&&passageInteriorAirPassed + passagePasses+=passageAudit.passed&&landingChambersWalkable + &&passageInteriorAirPassed &&mouthsInRadius&&mouthsInContent &&naturalAudit.passed&&mouthProfileAudit.passed&&rippleAudit.passed; std::printf("[ResolvedPassageAudit] id=%s interface=%d-%d control_points=%zu center_samples=%llu capsule_samples=%llu blocked=%llu passage_interior_capsule_samples=%llu passage_interior_solid_samples=%llu interior_air=%s support_samples=%llu support_overlap_skips=%llu unsupported=%llu grade_failures=%llu mouth_content=%d/2 mouth_radius=%d/2 walkability=%s\n", @@ -5058,7 +5434,26 @@ namespace StandaloneStreamBenchmark static_cast(passageAudit.supportOverlapSkips), static_cast(passageAudit.unsupported), static_cast(passageAudit.gradeFailures),mouthsInContent?2:0, - mouthsInRadius?2:0,passageAudit.passed?"PASS":"FAIL"); + mouthsInRadius?2:0, + passageAudit.passed&&landingChambersWalkable?"PASS":"FAIL"); + std::printf("[PassageLandingChamberAudit] id=%s side=upper capsule_samples=%llu solid_samples=%llu support_samples=%llu unsupported=%llu grade_failures=%llu status=%s\n", + passage.stableId.c_str(), + static_cast(upperChamberAudit.passageInteriorCapsuleSamples), + static_cast(upperChamberAudit.passageInteriorSolidSamples), + static_cast(upperChamberAudit.supportSamples), + static_cast(upperChamberAudit.unsupported), + static_cast(upperChamberAudit.gradeFailures), + upperChamberAudit.passed&&upperChamberAudit.passageInteriorSolidSamples==0 + &&upperChamberAudit.passageInteriorCapsuleSamples>0?"PASS":"FAIL"); + std::printf("[PassageLandingChamberAudit] id=%s side=lower capsule_samples=%llu solid_samples=%llu support_samples=%llu unsupported=%llu grade_failures=%llu status=%s\n", + passage.stableId.c_str(), + static_cast(lowerChamberAudit.passageInteriorCapsuleSamples), + static_cast(lowerChamberAudit.passageInteriorSolidSamples), + static_cast(lowerChamberAudit.supportSamples), + static_cast(lowerChamberAudit.unsupported), + static_cast(lowerChamberAudit.gradeFailures), + lowerChamberAudit.passed&&lowerChamberAudit.passageInteriorSolidSamples==0 + &&lowerChamberAudit.passageInteriorCapsuleSamples>0?"PASS":"FAIL"); std::printf("[PassageNaturalnessAudit] id=%s flat_run_max_m=%.3f limit_m=%.3f wall_relief_p2p_m=%.3f minimum_m=%.3f samples=%llu missing=%llu status=%s\n", passage.stableId.c_str(),naturalAudit.maximumFlatRunMetres,MaximumFlatRunMetres, naturalAudit.wallReliefPeakToPeakMetres,MinimumWallReliefMetres, @@ -5114,12 +5509,17 @@ namespace StandaloneStreamBenchmark passage.stableId.c_str(),value[0],value[1],value[2],value[2]-value[1], value[3],value[4],value[5]); } - std::printf("[PassageShellAudit] id=%s floor_samples=%llu floor_violations=%llu floor_overlap_skips=%llu floor_mouth_blend_skips=%llu open_air_shell_samples=%llu wall_samples=%llu wall_violations=%llu wall_overlap_skips=%llu wall_mouth_blend_skips=%llu interior_samples=%llu interior_violations=%llu spacing_m=%.3f status=%s\n", + std::printf("[PassageShellAudit] id=%s floor_samples=%llu floor_violations=%llu floor_overlap_skips=%llu floor_mouth_blend_skips=%llu chamber_floor_samples=%llu chamber_floor_violations=%llu chamber_wall_samples=%llu chamber_wall_violations=%llu chamber_connection_skips=%llu open_air_shell_samples=%llu wall_samples=%llu wall_violations=%llu wall_overlap_skips=%llu wall_mouth_blend_skips=%llu interior_samples=%llu interior_violations=%llu spacing_m=%.3f status=%s\n", passage.stableId.c_str(), static_cast(shellAudit.floorSamples), static_cast(shellAudit.floorViolations), static_cast(shellAudit.floorInteriorWins), static_cast(shellAudit.floorMouthBlendSkips), + static_cast(shellAudit.chamberFloorSamples), + static_cast(shellAudit.chamberFloorViolations), + static_cast(shellAudit.chamberWallSamples), + static_cast(shellAudit.chamberWallViolations), + static_cast(shellAudit.chamberConnectionSkips), static_cast(shellAudit.openAirSamples), static_cast(shellAudit.wallSamples), static_cast(shellAudit.wallViolations), @@ -5129,6 +5529,8 @@ namespace StandaloneStreamBenchmark static_cast(shellAudit.interiorViolations), PassageShellAuditSpacingMetres, shellAudit.floorViolations==0&&shellAudit.wallViolations==0 + &&shellAudit.chamberFloorViolations==0 + &&shellAudit.chamberWallViolations==0 &&shellAudit.interiorViolations==0 ?"PASS":"FAIL"); for(std::size_t i=0;i::max(),passageIndex);}; const FWalkAudit blockedAudit=auditPlayerPath(blockedWorld,path, 0.125,0.125,0.25,3,0.45,{},passageInteriorPredicate); + const FWalkAudit upperChamberBlocked=auditLandingChamber(blockedWorld, + passage,passageIndex,true); + const FWalkAudit lowerChamberBlocked=auditLandingChamber(blockedWorld, + passage,passageIndex,false); blockedControls+=blockedAudit.blocked>0&&!blockedAudit.passed; + const bool upperChamberControlDetected= + upperChamberBlocked.passageInteriorCapsuleSamples>0 + &&upperChamberBlocked.passageInteriorSolidSamples>0; + const bool lowerChamberControlDetected= + lowerChamberBlocked.passageInteriorCapsuleSamples>0 + &&lowerChamberBlocked.passageInteriorSolidSamples>0; + landingChamberNegativeControls+=upperChamberControlDetected + +lowerChamberControlDetected; const bool passageInteriorControlDetected= blockedAudit.passageInteriorCapsuleSamples>0 &&blockedAudit.passageInteriorSolidSamples>0; @@ -5433,6 +5847,16 @@ namespace StandaloneStreamBenchmark std::printf("[ResolvedPassageBlockedControl] id=%s blocked_capsule_samples=%llu detected=%s\n", passage.stableId.c_str(),static_cast(blockedAudit.blocked), blockedAudit.blocked>0&&!blockedAudit.passed?"true":"false"); + std::printf("[PassageLandingChamberNegativeControl] id=%s side=upper capsule_samples=%llu interior_solid_samples=%llu detected=%s\n", + passage.stableId.c_str(), + static_cast(upperChamberBlocked.passageInteriorCapsuleSamples), + static_cast(upperChamberBlocked.passageInteriorSolidSamples), + upperChamberControlDetected?"true":"false"); + std::printf("[PassageLandingChamberNegativeControl] id=%s side=lower capsule_samples=%llu interior_solid_samples=%llu detected=%s\n", + passage.stableId.c_str(), + static_cast(lowerChamberBlocked.passageInteriorCapsuleSamples), + static_cast(lowerChamberBlocked.passageInteriorSolidSamples), + lowerChamberControlDetected?"true":"false"); std::printf("[ResolvedPassageInteriorNegativeControl] id=%s passage_interior_capsule_samples=%llu interior_solid_samples=%llu detected=%s status=%s\n", passage.stableId.c_str(), static_cast(blockedAudit.passageInteriorCapsuleSamples), @@ -5443,21 +5867,35 @@ namespace StandaloneStreamBenchmark const bool passageInteriorAuditPassed=passageCount>0 &&passageInteriorCapsuleSamples>0&&passageInteriorSolidSamples==0 &&passageInteriorControls==passageCount; - std::printf("[PassageInteriorCapsuleAuditSummary] interfaces=%llu capsule_samples=%llu solid_samples=%llu negative_controls=%llu/%llu status=%s\n", + std::printf("[PassageInteriorCapsuleAuditSummary] interfaces=%llu capsule_samples=%llu solid_samples=%llu negative_controls=%llu/%llu landing_chambers=%llu chamber_negative_controls=%llu/%llu status=%s\n", static_cast(passageCount), static_cast(passageInteriorCapsuleSamples), static_cast(passageInteriorSolidSamples), static_cast(passageInteriorControls), static_cast(passageCount), + static_cast(landingChamberCount), + static_cast(landingChamberNegativeControls), + static_cast(landingChamberCount), passageInteriorAuditPassed?"PASS":"FAIL"); + std::printf("[PassageLandingChamberNegativeControlSummary] detected=%llu/%llu naturally_open_baselines=%llu status=OBSERVED\n", + static_cast(landingChamberNegativeControls), + static_cast(landingChamberCount), + static_cast(landingChamberCount + -landingChamberNegativeControls)); const bool shellAuditPassed=shellAudits==passageCount &&shellFloorViolations==0&&shellWallViolations==0 + &&shellChamberFloorViolations==0&&shellChamberWallViolations==0 &&shellInteriorViolations==0&&shellOpenAirSamples>0; - std::printf("[PassageShellAuditSummary] interfaces=%llu floor_samples=%llu floor_violations=%llu floor_mouth_blend_skips=%llu open_air_shell_samples=%llu wall_samples=%llu wall_violations=%llu interior_samples=%llu interior_violations=%llu status=%s\n", + std::printf("[PassageShellAuditSummary] interfaces=%llu floor_samples=%llu floor_violations=%llu floor_mouth_blend_skips=%llu chamber_floor_samples=%llu chamber_floor_violations=%llu chamber_wall_samples=%llu chamber_wall_violations=%llu chamber_connection_skips=%llu open_air_shell_samples=%llu wall_samples=%llu wall_violations=%llu interior_samples=%llu interior_violations=%llu status=%s\n", static_cast(shellAudits), static_cast(shellFloorSamples), static_cast(shellFloorViolations), static_cast(shellFloorMouthBlendSkips), + static_cast(shellChamberFloorSamples), + static_cast(shellChamberFloorViolations), + static_cast(shellChamberWallSamples), + static_cast(shellChamberWallViolations), + static_cast(shellChamberConnectionSkips), static_cast(shellOpenAirSamples), static_cast(shellWallSamples), static_cast(shellWallViolations),