passages: rounded half-pipe crest (L113, v220); floor bands diagnosed, speed not recovered
Open-air walls: 0.75 m rounded shell, relief fades over the top 0.25 m - the needle spikes are gone. Every owner plan passes the 15 deg forward-grade check (max ~10.7 deg) and all walk audits. Floor bands: median 1.25 m interval, ~0.21 m drop on a ~10.4 deg floor = one 0.25 m voxel row projected along the grade (not control points, not slab thickness) - not yet fixed. Exact segment-to-tile filter did not reduce candidates; removed. Timings this round are unreliable (a gitea process used ~43% CPU throughout). Fast tier PASS; Clang/GCC deterministic; UE builds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
This commit is contained in:
1 parent
16554cd426
commit
36d954cf2f
114 files changed
+7814
-63
No files matched your search
@@ -121,7 +121,8 @@ namespace VoxelForgeCore::PassageSpline
|
||||
|
||||
inline std::vector<FSegment> BuildSegments(std::span<const FPoint> controls,
|
||||
std::size_t spanCount,double spacingMetres,double unitsPerMetre=1.0,
|
||||
std::span<const double> plannedFloorProfile={})
|
||||
std::span<const double> plannedFloorProfile={},
|
||||
double maximumChordErrorMetres=0.0)
|
||||
{
|
||||
std::vector<FSegment> result;
|
||||
if(controls.size()<2||!(spacingMetres>0.0)||!(unitsPerMetre>0.0))return result;
|
||||
@@ -141,18 +142,14 @@ namespace VoxelForgeCore::PassageSpline
|
||||
previous=current;
|
||||
}
|
||||
const double spanLength=horizontalArc.back();
|
||||
const std::size_t segmentCount=std::max<std::size_t>(1,
|
||||
const std::size_t baseSegmentCount=std::max<std::size_t>(1,
|
||||
static_cast<std::size_t>(std::ceil(spanLength/spacingMetres)));
|
||||
std::vector<FPoint> points;
|
||||
std::vector<double> parameters;
|
||||
points.reserve(segmentCount+1);
|
||||
parameters.reserve(segmentCount+1);
|
||||
points.push_back(EvaluateRaw(controls,span,0.0,unitsPerMetre));
|
||||
parameters.push_back(0.0);
|
||||
for(std::size_t segment=1;segment<=segmentCount;++segment)
|
||||
points.reserve(baseSegmentCount+1);
|
||||
parameters.reserve(baseSegmentCount+1);
|
||||
const auto parameterAtArc=[&](double targetArc)
|
||||
{
|
||||
const double targetArc=spanLength*static_cast<double>(segment)
|
||||
/segmentCount;
|
||||
const auto upper=std::lower_bound(horizontalArc.begin()+1,
|
||||
horizontalArc.end(),targetArc);
|
||||
const std::size_t upperIndex=static_cast<std::size_t>(
|
||||
@@ -161,10 +158,63 @@ namespace VoxelForgeCore::PassageSpline
|
||||
const double arcSpan=horizontalArc[upperIndex]-horizontalArc[lowerIndex];
|
||||
const double arcT=arcSpan>1.0e-12
|
||||
?std::clamp((targetArc-horizontalArc[lowerIndex])/arcSpan,0.0,1.0):0.0;
|
||||
const double t=(static_cast<double>(lowerIndex)+arcT)/ArcTableSteps;
|
||||
points.push_back(EvaluateRaw(controls,span,t,unitsPerMetre));
|
||||
parameters.push_back(t);
|
||||
return (static_cast<double>(lowerIndex)+arcT)/ArcTableSteps;
|
||||
};
|
||||
parameters.push_back(0.0);
|
||||
const double chordError=std::max(0.0,maximumChordErrorMetres);
|
||||
const auto appendAdaptive=[&](auto&& self,double tA,double tB,
|
||||
const FPoint& a,const FPoint& b,unsigned depth)->void
|
||||
{
|
||||
constexpr std::array<double,3> CheckFractions={0.25,0.5,0.75};
|
||||
double maximumError=0.0,polylineLength=0.0;
|
||||
FPoint previous=a;
|
||||
for(const double fraction:CheckFractions)
|
||||
{
|
||||
const FPoint current=EvaluateRaw(controls,span,
|
||||
std::lerp(tA,tB,fraction),unitsPerMetre);
|
||||
const double chordX=std::lerp(a[0],b[0],fraction);
|
||||
const double chordY=std::lerp(a[1],b[1],fraction);
|
||||
const double dx=(current[0]-chordX)/unitsPerMetre;
|
||||
const double dy=(current[1]-chordY)/unitsPerMetre;
|
||||
const double dr=(current[3]-std::lerp(a[3],b[3],fraction))
|
||||
/unitsPerMetre;
|
||||
maximumError=std::max(maximumError,
|
||||
std::sqrt(dx*dx+dy*dy+dr*dr));
|
||||
const double segmentX=(current[0]-previous[0])/unitsPerMetre;
|
||||
const double segmentY=(current[1]-previous[1])/unitsPerMetre;
|
||||
polylineLength+=std::sqrt(segmentX*segmentX+segmentY*segmentY);
|
||||
previous=current;
|
||||
}
|
||||
const double finalX=(b[0]-previous[0])/unitsPerMetre;
|
||||
const double finalY=(b[1]-previous[1])/unitsPerMetre;
|
||||
polylineLength+=std::sqrt(finalX*finalX+finalY*finalY);
|
||||
const bool split=(polylineLength>spacingMetres*(1.0+1.0e-9)
|
||||
||(chordError>0.0&&maximumError>chordError))&&depth<12;
|
||||
if(split)
|
||||
{
|
||||
const double tMid=(tA+tB)*0.5;
|
||||
const FPoint middle=EvaluateRaw(controls,span,tMid,unitsPerMetre);
|
||||
self(self,tA,tMid,a,middle,depth+1);
|
||||
self(self,tMid,tB,middle,b,depth+1);
|
||||
return;
|
||||
}
|
||||
parameters.push_back(tB);
|
||||
};
|
||||
for(std::size_t segment=1;segment<=baseSegmentCount;++segment)
|
||||
{
|
||||
const double targetA=spanLength*static_cast<double>(segment-1)
|
||||
/baseSegmentCount;
|
||||
const double targetB=spanLength*static_cast<double>(segment)
|
||||
/baseSegmentCount;
|
||||
const double tA=parameterAtArc(targetA);
|
||||
const double tB=parameterAtArc(targetB);
|
||||
const FPoint a=EvaluateRaw(controls,span,tA,unitsPerMetre);
|
||||
const FPoint b=EvaluateRaw(controls,span,tB,unitsPerMetre);
|
||||
appendAdaptive(appendAdaptive,tA,tB,a,b,0);
|
||||
}
|
||||
points.reserve(parameters.size());
|
||||
for(const double t:parameters)
|
||||
points.push_back(EvaluateRaw(controls,span,t,unitsPerMetre));
|
||||
std::vector<double> sampledHorizontalArc(points.size(),0.0);
|
||||
for(std::size_t i=1;i<points.size();++i)
|
||||
{
|
||||
|
||||
@@ -471,7 +471,8 @@ namespace VoxelForgeCore
|
||||
// Version 212 balances generated entry routes against existing walk lanes and preserves their shared treads.
|
||||
// Version 218 sweeps passages around a dense smooth spline and limits exposed shells to a trough.
|
||||
// Version 219 smooths passage XY while mapping each planned height chord over its sampled horizontal arc.
|
||||
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 219;
|
||||
// Version 220 adaptively samples passage curves and rounds open-air wall crests.
|
||||
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 220;
|
||||
|
||||
enum class FieldChannel : std::uint32_t
|
||||
{
|
||||
|
||||
@@ -1365,8 +1365,6 @@ public:
|
||||
const double wallElevationMetres=(qz-(floorCenterZ-floorDepth))
|
||||
/qPerMetre;
|
||||
const double mouthWallThickness=segment.routeSegment
|
||||
&&wallElevationMetres>=0.0
|
||||
&&wallElevationMetres<=OpenAirSideWallHeightMetres
|
||||
?smoothShellFade*shellThickness:0.0;
|
||||
double floorExtensionFade=1.0;
|
||||
if(!segment.routeSegment)
|
||||
@@ -1380,10 +1378,13 @@ public:
|
||||
}
|
||||
const double floorShellThickness=smoothFloorBlend*shellThickness
|
||||
*floorExtensionFade*smoothShellFade;
|
||||
const double localThickness=std::max(mouthWallThickness,
|
||||
floorShellThickness);
|
||||
const double localShellDistance=std::max(-static_cast<double>(flatDistance),
|
||||
static_cast<double>(flatDistance)-localThickness);
|
||||
const double wallShellDistance=PassageRoundedWallShellDistance(
|
||||
flatDistance,wallElevationMetres*qPerMetre,
|
||||
OpenAirSideWallHeightMetres*qPerMetre,mouthWallThickness);
|
||||
const double floorShellDistance=std::max(-static_cast<double>(flatDistance),
|
||||
static_cast<double>(flatDistance)-floorShellThickness);
|
||||
const double localShellDistance=std::min(wallShellDistance,
|
||||
floorShellDistance);
|
||||
nearest.shellSignedDistance=std::min(nearest.shellSignedDistance,
|
||||
localShellDistance);
|
||||
};
|
||||
@@ -1391,11 +1392,17 @@ public:
|
||||
qx,qy,qz,0,shellSearchBound,visitShellSegment);
|
||||
}
|
||||
if(!nearest.valid)continue;
|
||||
const double wallElevationMetres=(qz-(nearest.floorCenterZ
|
||||
-nearest.floorDepth))/qPerMetre;
|
||||
double distanceToCenter=std::max(1.0e-9,
|
||||
nearest.roundSignedDistance+nearest.radius);
|
||||
const double verticalWallAlignment=distanceToCenter>1.0e-9
|
||||
?std::clamp(-(qz-nearest.centerZ)/distanceToCenter,0.0,1.0):0.0;
|
||||
const double wallReliefWeight=0.05+0.95*(1.0-verticalWallAlignment);
|
||||
const double crestFade=std::clamp((OpenAirSideWallHeightMetres
|
||||
-wallElevationMetres)/0.25,0.0,1.0);
|
||||
const double smoothCrestFade=crestFade*crestFade*(3.0-2.0*crestFade);
|
||||
const double wallReliefWeight=(0.05+0.95*(1.0-verticalWallAlignment))
|
||||
*smoothCrestFade;
|
||||
float relief=0.0f;
|
||||
if(nearest.swept)
|
||||
{
|
||||
@@ -1427,11 +1434,7 @@ public:
|
||||
const double floorBlend=std::clamp(floorDistanceMetres/0.25,0.0,1.0);
|
||||
const double smoothFloorBlend=floorBlend*floorBlend
|
||||
*(3.0-2.0*floorBlend);
|
||||
const double wallElevationMetres=(qz-(nearest.floorCenterZ-nearest.floorDepth))
|
||||
/qPerMetre;
|
||||
const double mouthWallThickness=segment.routeSegment
|
||||
&&wallElevationMetres>=0.0
|
||||
&&wallElevationMetres<=OpenAirSideWallHeightMetres
|
||||
?smoothShellFade*shellThickness:0.0;
|
||||
double floorExtensionFade=1.0;
|
||||
if(!segment.routeSegment)
|
||||
@@ -1445,11 +1448,14 @@ public:
|
||||
}
|
||||
const double floorShellThickness=smoothFloorBlend*shellThickness
|
||||
*floorExtensionFade*smoothShellFade;
|
||||
const double localShellThickness=std::max(mouthWallThickness,
|
||||
floorShellThickness);
|
||||
const double wallShellDistance=PassageRoundedWallShellDistance(
|
||||
shapedDistance,wallElevationMetres*qPerMetre,
|
||||
OpenAirSideWallHeightMetres*qPerMetre,mouthWallThickness);
|
||||
const double floorShellDistance=std::max(-static_cast<double>(shapedDistance),
|
||||
static_cast<double>(shapedDistance)-floorShellThickness);
|
||||
unionDistance=std::min(unionDistance,static_cast<double>(shapedDistance));
|
||||
const double localShellDistance=std::max(-static_cast<double>(shapedDistance),
|
||||
static_cast<double>(shapedDistance)-localShellThickness);
|
||||
const double localShellDistance=std::min(wallShellDistance,
|
||||
floorShellDistance);
|
||||
unionShellDistance=std::min(unionShellDistance,
|
||||
std::min(localShellDistance,nearest.shellSignedDistance));
|
||||
}
|
||||
@@ -3436,6 +3442,25 @@ private:
|
||||
return SmoothMaxDistance(roundDistanceSubvoxels,floorDistance,
|
||||
static_cast<float>(CornerBlendMetres*qPerMetre));
|
||||
}
|
||||
static double PassageRoundedWallShellDistance(double flatDistanceSubvoxels,
|
||||
double wallElevationSubvoxels,double wallHeightSubvoxels,
|
||||
double wallThicknessSubvoxels)
|
||||
{
|
||||
if(!(wallThicknessSubvoxels>1.0e-9)||!(wallHeightSubvoxels>0.0))
|
||||
return std::numeric_limits<double>::infinity();
|
||||
const double radius=wallThicknessSubvoxels*0.5;
|
||||
const double capCenter=wallHeightSubvoxels-radius;
|
||||
if(!(capCenter>0.0))return std::numeric_limits<double>::infinity();
|
||||
const double x=flatDistanceSubvoxels-radius;
|
||||
const double boxHalfHeight=capCenter*0.5;
|
||||
const double boxX=std::abs(x)-radius;
|
||||
const double boxY=std::abs(wallElevationSubvoxels-boxHalfHeight)
|
||||
-boxHalfHeight;
|
||||
const double boxDistance=std::min(std::max(boxX,boxY),0.0)
|
||||
+std::hypot(std::max(boxX,0.0),std::max(boxY,0.0));
|
||||
const double capDistance=std::hypot(x,wallElevationSubvoxels-capCenter)-radius;
|
||||
return std::min(boxDistance,capDistance);
|
||||
}
|
||||
struct FPassageNearestNode
|
||||
{
|
||||
std::array<double,3> minimum{},maximum{};
|
||||
@@ -4499,7 +4524,7 @@ private:
|
||||
if(!ResolvedLayout)
|
||||
{
|
||||
if(tracePreparation)
|
||||
std::printf("[L112PassagePrepare] passages=0 control_spans=0 segments=0 spacing_m=1.00 bytes=0 prepare_ms=%.3f\n",
|
||||
std::printf("[L113PassagePrepare] passages=0 control_spans=0 segments=0 max_spacing_m=8.00 max_chord_error_m=0.025 bytes=0 prepare_ms=%.3f\n",
|
||||
std::chrono::duration<double,std::milli>(
|
||||
std::chrono::steady_clock::now()-preparationStart).count());
|
||||
return;
|
||||
@@ -4510,7 +4535,8 @@ private:
|
||||
constexpr double Q=static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
|
||||
const double qPerMetre=Q*1000000.0/
|
||||
std::max<std::uint32_t>(1,ResolvedLayout->coreVoxelSizeMicrometres);
|
||||
constexpr double PassageSegmentSpacingMetres=1.0;
|
||||
constexpr double PassageSegmentSpacingMetres=8.0;
|
||||
constexpr double PassageSegmentChordErrorMetres=0.025;
|
||||
std::size_t preparedSegments=0,controlSpans=0;
|
||||
for(std::size_t passageIndex=0;passageIndex<ResolvedLayout->passages.size();
|
||||
++passageIndex)
|
||||
@@ -4569,7 +4595,29 @@ private:
|
||||
upperMouthFloorDepth*qPerMetre);
|
||||
const auto sampledSegments=VoxelForgeCore::PassageSpline::BuildSegments(
|
||||
controls,controls.size()-1,PassageSegmentSpacingMetres,qPerMetre,
|
||||
plannedFloorProfile);
|
||||
plannedFloorProfile,PassageSegmentChordErrorMetres);
|
||||
constexpr double MaximumFloorGradeDegrees=15.0;
|
||||
constexpr double DegreesPerRadian=180.0/3.14159265358979323846;
|
||||
double maximumFloorGradeDegrees=0.0;
|
||||
std::size_t floorGradeSamples=0,floorGradeViolations=0;
|
||||
for(const auto& segment:sampledSegments)
|
||||
{
|
||||
if(segment.controlSpan>=routeEnd)continue;
|
||||
const double dx=(segment.b[0]-segment.a[0])/qPerMetre;
|
||||
const double dy=(segment.b[1]-segment.a[1])/qPerMetre;
|
||||
const double dz=(segment.plannedFloorB-segment.plannedFloorA)/qPerMetre;
|
||||
const double horizontal=std::hypot(dx,dy);
|
||||
const double grade=horizontal>1.0e-9
|
||||
?std::atan2(std::abs(dz),horizontal)*DegreesPerRadian
|
||||
:(std::abs(dz)>1.0e-9?90.0:0.0);
|
||||
maximumFloorGradeDegrees=std::max(maximumFloorGradeDegrees,grade);
|
||||
++floorGradeSamples;
|
||||
if(grade>MaximumFloorGradeDegrees+1.0e-8)++floorGradeViolations;
|
||||
}
|
||||
std::printf("[PassagePlanGradeAudit] id=%s samples=%zu max_grade_deg=%.6f limit_deg=%.3f violations=%zu status=%s\n",
|
||||
passage.stableId.c_str(),floorGradeSamples,maximumFloorGradeDegrees,
|
||||
MaximumFloorGradeDegrees,floorGradeViolations,
|
||||
floorGradeViolations==0?"PASS":"FAIL");
|
||||
constants.upperMouthX=controls.front()[0];
|
||||
constants.upperMouthY=controls.front()[1];
|
||||
constants.upperMouthPlannedFloor=plannedFloorProfile.front();
|
||||
@@ -4663,9 +4711,10 @@ private:
|
||||
PassageSegmentOffsets[passageIndex+1]=preparedSegments;
|
||||
}
|
||||
if(tracePreparation)
|
||||
std::printf("[L112PassagePrepare] passages=%zu control_spans=%zu segments=%zu spacing_m=%.2f bytes=%zu prepare_ms=%.3f\n",
|
||||
std::printf("[L113PassagePrepare] passages=%zu control_spans=%zu segments=%zu max_spacing_m=%.2f max_chord_error_m=%.3f bytes=%zu prepare_ms=%.3f\n",
|
||||
PassageSegmentData.size(),controlSpans,preparedSegments,
|
||||
PassageSegmentSpacingMetres,preparedSegments*sizeof(FPassageSegmentData),
|
||||
PassageSegmentSpacingMetres,PassageSegmentChordErrorMetres,
|
||||
preparedSegments*sizeof(FPassageSegmentData),
|
||||
std::chrono::duration<double,std::milli>(
|
||||
std::chrono::steady_clock::now()-preparationStart).count());
|
||||
}
|
||||
@@ -4688,6 +4737,7 @@ private:
|
||||
constexpr double PassageShellMetres=0.75;
|
||||
constexpr double ReliefMetres=0.21;
|
||||
constexpr double SweepReachMetres=4.0;
|
||||
constexpr double CornerBlendMetres=0.50;
|
||||
for(std::size_t passageIndex=0;passageIndex<ResolvedLayout->passages.size();
|
||||
++passageIndex)
|
||||
{
|
||||
@@ -4707,9 +4757,10 @@ private:
|
||||
routeMinimum[axis]=std::min(prepared.a[axis],prepared.b[axis])/Q;
|
||||
routeMaximum[axis]=std::max(prepared.a[axis],prepared.b[axis])/Q;
|
||||
}
|
||||
const double routeExtent=maximumRadius+PassageShellMetres
|
||||
+ReliefMetres+0.01;
|
||||
const double tileExtent=routeExtent+SweepReachMetres;
|
||||
const double requiredReach=std::max(SweepReachMetres+ReliefMetres
|
||||
+CornerBlendMetres*0.25,PassageShellMetres+ReliefMetres);
|
||||
const double routeExtent=maximumRadius+requiredReach+0.01;
|
||||
const double tileExtent=routeExtent;
|
||||
FPassageProofBound proof;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
@@ -11414,8 +11465,6 @@ inline void FStandaloneShapeWorld::FScopedPassageTileQueries::prepareSweptTubeBa
|
||||
const double wallElevationMetres=(qz-(floorCenterZ-floorDepth))
|
||||
/qPerMetre;
|
||||
const double mouthWallThickness=segment.routeSegment
|
||||
&&wallElevationMetres>=0.0
|
||||
&&wallElevationMetres<=OpenAirSideWallHeightMetres
|
||||
?smoothShellFade*shellThickness:0.0;
|
||||
double floorExtensionFade=1.0;
|
||||
if(!segment.routeSegment)
|
||||
@@ -11429,10 +11478,14 @@ inline void FStandaloneShapeWorld::FScopedPassageTileQueries::prepareSweptTubeBa
|
||||
}
|
||||
const double floorShellThickness=smoothFloorBlend*shellThickness
|
||||
*floorExtensionFade*smoothShellFade;
|
||||
const double localThickness=std::max(mouthWallThickness,
|
||||
floorShellThickness);
|
||||
const double localShellDistance=std::max(-static_cast<double>(flatDistance),
|
||||
static_cast<double>(flatDistance)-localThickness);
|
||||
const double wallShellDistance=
|
||||
FStandaloneShapeWorld::PassageRoundedWallShellDistance(
|
||||
flatDistance,wallElevationMetres*qPerMetre,
|
||||
OpenAirSideWallHeightMetres*qPerMetre,mouthWallThickness);
|
||||
const double floorShellDistance=std::max(-static_cast<double>(flatDistance),
|
||||
static_cast<double>(flatDistance)-floorShellThickness);
|
||||
const double localShellDistance=std::min(wallShellDistance,
|
||||
floorShellDistance);
|
||||
sample.shellSignedDistance=std::min(sample.shellSignedDistance,
|
||||
localShellDistance);
|
||||
if(!sample.valid||flatDistance<sample.signedDistance
|
||||
|
||||
Reference in new issue
Block a user