proto: liminal pass 2 - hills to the horizon, swept road over a cloud floor, pillar grid (L142)
Captures stream LOD0-8 (hills repeat at 80 m to the horizon). Road: cubic swept solid deck, 376.8 m, 8 m wide, max 12.06 deg, 17 supports, cloud floor ~45 m below; walk audit 0 blocked/unsupported, 300 m negative control fails as expected. 15x15 identical pillar grid at 76 m. Owner canonical hashes unchanged; fast tier, Clang/GCC green. 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
9c6d261d01
commit
6cd9e4da76
17 files changed
+1129
-98
No files matched your search
@@ -1882,8 +1882,11 @@ int main(int argc,char** argv)
|
||||
ownerJsonRoot.Get("motif_family_override").Data);
|
||||
const bool hasLiminalRoad=!std::holds_alternative<std::nullptr_t>(
|
||||
ownerJsonRoot.Get("liminal_road_override").Data);
|
||||
if(hasMotifFamily&&hasLiminalRoad)
|
||||
{std::fprintf(stderr,"motif_family_override and liminal_road_override cannot be combined\n");return 2;}
|
||||
const bool hasLiminalPillarGrid=!std::holds_alternative<std::nullptr_t>(
|
||||
ownerJsonRoot.Get("liminal_pillar_grid_override").Data);
|
||||
if((hasMotifFamily&&hasLiminalRoad)||(hasMotifFamily&&hasLiminalPillarGrid)
|
||||
||(hasLiminalRoad&&hasLiminalPillarGrid))
|
||||
{std::fprintf(stderr,"liminal content overrides must be used in separate fixtures\n");return 2;}
|
||||
if(hasMotifFamily)
|
||||
{
|
||||
std::vector<VoxelForgeCore::ResolvedLayoutStampRequestV1> requests;
|
||||
@@ -1914,22 +1917,62 @@ int main(int argc,char** argv)
|
||||
}
|
||||
if(hasLiminalRoad)
|
||||
{
|
||||
std::vector<VoxelForgeCore::ResolvedLayoutStampRequestV1> requests(1);
|
||||
if(!StandaloneWorldAssemblyPrototype::BuildLiminalRoadRequest(ownerJsonRoot,
|
||||
*ownerWorld,worldAssembly,resolvedLayout,requests.front(),error)
|
||||
std::vector<VoxelForgeCore::ResolvedLayoutStampRequestV1> requests;
|
||||
if(!StandaloneWorldAssemblyPrototype::BuildLiminalRoadRequests(ownerJsonRoot,
|
||||
*ownerWorld,worldAssembly,resolvedLayout,requests,error)
|
||||
||!VoxelForgeCore::ResolveResolvedLayoutStampsV1(
|
||||
resolvedLayout,seed,requests,error))
|
||||
{std::fprintf(stderr,"Liminal road resolution failed: %s\n",error.c_str());return 2;}
|
||||
const auto& road=resolvedLayout.stamps.front();
|
||||
std::printf("[LiminalRoadResolved] vocabulary=%u id=%s strate=%d shape=rounded_dais "
|
||||
"center_m=(%.3f,%.3f,%.3f) top_m=%.3f span_m=%.3f width_m=%.3f "
|
||||
"grade_deg=0.000 placement=free route_keepout=PASS band_clearance=PASS status=PASS\n",
|
||||
resolvedLayout.stampVocabularyVersion,road.stableId.c_str(),road.strateIndex,
|
||||
road.positionMicrometres[0]/1.0e6,road.positionMicrometres[1]/1.0e6,
|
||||
road.positionMicrometres[2]/1.0e6,
|
||||
(road.positionMicrometres[2]+road.halfExtentsMicrometres[2])/1.0e6,
|
||||
2.0*road.halfExtentsMicrometres[0]/1.0e6,
|
||||
2.0*road.halfExtentsMicrometres[1]/1.0e6);
|
||||
std::vector<const VoxelForgeCore::ResolvedLayoutStampV1*> deck;
|
||||
std::size_t supportCount=0;
|
||||
const std::string deckPrefix="liminal/road-clouds-v2/deck/";
|
||||
const std::string supportPrefix="liminal/road-clouds-v2/support/";
|
||||
for(const auto& stamp:resolvedLayout.stamps)
|
||||
{
|
||||
if(stamp.stableId.starts_with(deckPrefix))deck.push_back(&stamp);
|
||||
else if(stamp.stableId.starts_with(supportPrefix))++supportCount;
|
||||
}
|
||||
std::sort(deck.begin(),deck.end(),[](const auto* a,const auto* b)
|
||||
{return a->stableId<b->stableId;});
|
||||
double pathLength=0.0,maxGradeDegrees=0.0;
|
||||
for(std::size_t i=0;i<deck.size();++i)
|
||||
{
|
||||
const auto* segment=deck[i];
|
||||
const double dx=(segment->sweepEndpointsMicrometres[1][0]
|
||||
-segment->sweepEndpointsMicrometres[0][0])/1.0e6;
|
||||
const double dy=(segment->sweepEndpointsMicrometres[1][1]
|
||||
-segment->sweepEndpointsMicrometres[0][1])/1.0e6;
|
||||
const double dz=std::abs(segment->sweepEndpointsMicrometres[1][2]
|
||||
-segment->sweepEndpointsMicrometres[0][2])/1.0e6;
|
||||
const double horizontal=std::hypot(dx,dy);
|
||||
pathLength+=horizontal;
|
||||
maxGradeDegrees=std::max(maxGradeDegrees,
|
||||
std::atan2(dz,horizontal)*57.29577951308232);
|
||||
}
|
||||
const double width=deck.empty()?0.0:
|
||||
2.0*deck.front()->halfExtentsMicrometres[1]/1.0e6;
|
||||
std::printf("[LiminalRoadResolved] vocabulary=%u id=liminal/road-clouds-v2 "
|
||||
"strate=%d shape=swept_deck_segments deck_segments=%zu supports=%zu "
|
||||
"path_length_m=%.3f width_m=%.3f grade_max_deg=%.3f "
|
||||
"placement=free route_keepout=PASS band_clearance=PASS status=%s\n",
|
||||
resolvedLayout.stampVocabularyVersion,
|
||||
deck.empty()?-1:deck.front()->strateIndex,deck.size(),supportCount,pathLength,
|
||||
width,maxGradeDegrees,deck.size()>1&&pathLength>=300.0&&width>=3.0
|
||||
&&maxGradeDegrees<=15.0?"PASS":"FAIL");
|
||||
if(deck.size()<2||pathLength<80.0||width<3.0||maxGradeDegrees>15.0)
|
||||
{std::fprintf(stderr,"Liminal road resolved outside the walkable road bounds\n");return 2;}
|
||||
}
|
||||
if(hasLiminalPillarGrid)
|
||||
{
|
||||
std::vector<VoxelForgeCore::ResolvedLayoutStampRequestV1> requests;
|
||||
if(!StandaloneWorldAssemblyPrototype::BuildLiminalPillarGridRequests(ownerJsonRoot,
|
||||
*ownerWorld,worldAssembly,resolvedLayout,requests,error)
|
||||
||!VoxelForgeCore::ResolveResolvedLayoutStampsV1(
|
||||
resolvedLayout,seed,requests,error))
|
||||
{std::fprintf(stderr,"Liminal pillar-grid resolution failed: %s\n",error.c_str());return 2;}
|
||||
std::printf("[LiminalPillarGridResolved] vocabulary=%u pillars=%zu shape=identical_vertical_cylinders "
|
||||
"placement=regular_grid route_keepout=PASS band_clearance=PASS status=PASS\n",
|
||||
resolvedLayout.stampVocabularyVersion,resolvedLayout.stamps.size());
|
||||
}
|
||||
if(options.LandmarkChamberDemo)
|
||||
{
|
||||
|
||||
@@ -2966,53 +2966,122 @@ namespace StandaloneStreamBenchmark
|
||||
{
|
||||
bool found=false;
|
||||
bool passed=true;
|
||||
std::vector<const VoxelForgeCore::ResolvedLayoutStampV1*> deck;
|
||||
constexpr std::string_view DeckPrefix="liminal/road-clouds-v2/deck/";
|
||||
for(const auto& road:ResolvedLayout->stamps)
|
||||
{
|
||||
if(road.stableId!="liminal/raised-road-v1")continue;
|
||||
if(road.stableId.starts_with(DeckPrefix))deck.push_back(&road);
|
||||
}
|
||||
if(!deck.empty())
|
||||
{
|
||||
found=true;
|
||||
const bool longAxisX=(road.yawQuarterTurns&1u)==0;
|
||||
const double centerX=road.positionMicrometres[0]/1.0e6;
|
||||
const double centerY=road.positionMicrometres[1]/1.0e6;
|
||||
const double centerZ=road.positionMicrometres[2]/1.0e6;
|
||||
const double halfLength=road.halfExtentsMicrometres[longAxisX?0:1]/1.0e6;
|
||||
const double halfWidth=road.halfExtentsMicrometres[longAxisX?1:0]/1.0e6;
|
||||
const double deckZ=centerZ+road.halfExtentsMicrometres[2]/1.0e6;
|
||||
constexpr double CapsuleRadiusMetres=0.34;
|
||||
std::sort(deck.begin(),deck.end(),[](const auto* a,const auto* b)
|
||||
{return a->stableId<b->stableId;});
|
||||
constexpr double CapsuleHeightMetres=1.76;
|
||||
constexpr double HalfCapsuleHeightMetres=CapsuleHeightMetres*0.5;
|
||||
constexpr double EndInsetMetres=1.0;
|
||||
constexpr double SlopeCapsuleClearanceMetres=0.12;
|
||||
std::vector<std::array<double,3>> roadPath;
|
||||
const double inset=std::min(EndInsetMetres,halfLength*0.25);
|
||||
for(const double along:{-halfLength+inset,halfLength-inset})
|
||||
roadPath.push_back({(centerX+(longAxisX?along:0.0))/coreMetres,
|
||||
(centerY+(longAxisX?0.0:along))/coreMetres,
|
||||
(deckZ+HalfCapsuleHeightMetres)/coreMetres});
|
||||
roadPath.reserve(deck.size()*4);
|
||||
std::vector<std::array<double,3>> centerline;
|
||||
centerline.reserve(deck.size()+1);
|
||||
bool surfacePathValid=true;
|
||||
for(std::size_t i=0;i<deck.size();++i)
|
||||
{
|
||||
const auto* segment=deck[i];
|
||||
if(segment->kind!=VoxelForgeCore::ResolvedSdfStampKindV1::fillSweptDeckSegment)
|
||||
surfacePathValid=false;
|
||||
const auto point=[&](std::size_t endpoint)
|
||||
{return std::array<double,3>{
|
||||
segment->sweepEndpointsMicrometres[endpoint][0]/1.0e6,
|
||||
segment->sweepEndpointsMicrometres[endpoint][1]/1.0e6,
|
||||
segment->sweepEndpointsMicrometres[endpoint][2]/1.0e6};};
|
||||
const auto a=point(0),b=point(1);
|
||||
if(i==0)centerline.push_back(a);
|
||||
else if(std::abs(centerline.back()[0]-a[0])>1.0e-6
|
||||
||std::abs(centerline.back()[1]-a[1])>1.0e-6
|
||||
||std::abs(centerline.back()[2]-a[2])>1.0e-6)
|
||||
surfacePathValid=false;
|
||||
centerline.push_back(b);
|
||||
}
|
||||
double pathLengthMetres=0.0,maxGradeDegrees=0.0;
|
||||
std::uint64_t surfaceProbes=0,surfaceMisses=0;
|
||||
const auto surfaceTopAt=[&](double xMetres,double yMetres,
|
||||
double nominalTop,double& surfaceTop)
|
||||
{
|
||||
++surfaceProbes;
|
||||
double low=nominalTop-0.25,high=nominalTop+1.0;
|
||||
if(Shape.densityAt({xMetres/coreMetres,yMetres/coreMetres,
|
||||
low/coreMetres})>=0.0f
|
||||
||Shape.densityAt({xMetres/coreMetres,yMetres/coreMetres,
|
||||
high/coreMetres})<0.0f)
|
||||
{++surfaceMisses;return false;}
|
||||
for(int iteration=0;iteration<20;++iteration)
|
||||
{
|
||||
const double middle=(low+high)*0.5;
|
||||
if(Shape.densityAt({xMetres/coreMetres,yMetres/coreMetres,
|
||||
middle/coreMetres})<0.0f)low=middle;
|
||||
else high=middle;
|
||||
}
|
||||
surfaceTop=high;
|
||||
return true;
|
||||
};
|
||||
const double halfThickness=deck.front()->halfExtentsMicrometres[2]/1.0e6;
|
||||
for(std::size_t segment=1;segment<centerline.size();++segment)
|
||||
{
|
||||
const auto& a=centerline[segment-1];const auto& b=centerline[segment];
|
||||
const double dx=b[0]-a[0],dy=b[1]-a[1];
|
||||
const double horizontal=std::hypot(dx,dy);
|
||||
pathLengthMetres+=horizontal;
|
||||
const std::uint64_t intervals=std::max<std::uint64_t>(1,
|
||||
static_cast<std::uint64_t>(std::ceil(horizontal/0.5)));
|
||||
for(std::uint64_t step=0;step<=intervals;++step)
|
||||
{
|
||||
if(segment>1&&step==0)continue;
|
||||
const double t=static_cast<double>(step)/intervals;
|
||||
const double x=a[0]+dx*t,y=a[1]+dy*t;
|
||||
const double nominalTop=a[2]+(b[2]-a[2])*t+halfThickness;
|
||||
double actualTop=nominalTop;
|
||||
if(!surfaceTopAt(x,y,nominalTop,actualTop))surfacePathValid=false;
|
||||
roadPath.push_back({x/coreMetres,y/coreMetres,
|
||||
(actualTop+HalfCapsuleHeightMetres
|
||||
+SlopeCapsuleClearanceMetres)/coreMetres});
|
||||
}
|
||||
}
|
||||
for(std::size_t i=1;i<roadPath.size();++i)
|
||||
{
|
||||
const double dx=(roadPath[i][0]-roadPath[i-1][0])*coreMetres;
|
||||
const double dy=(roadPath[i][1]-roadPath[i-1][1])*coreMetres;
|
||||
const double dz=std::abs(roadPath[i][2]-roadPath[i-1][2])*coreMetres;
|
||||
const double horizontal=std::hypot(dx,dy);
|
||||
maxGradeDegrees=std::max(maxGradeDegrees,
|
||||
std::atan2(dz,horizontal)*57.29577951308232);
|
||||
}
|
||||
const FWalkAudit roadAudit=auditPlayerPath(Shape,roadPath,
|
||||
0.125,0.125,1.125,3,0.45);
|
||||
const double widthMetres=2.0*halfWidth;
|
||||
const double horizontal=std::hypot(roadPath[1][0]-roadPath[0][0],
|
||||
roadPath[1][1]-roadPath[0][1])*coreMetres;
|
||||
const double vertical=std::abs(roadPath[1][2]-roadPath[0][2])*coreMetres;
|
||||
const double gradeDegrees=std::atan2(vertical,horizontal)*
|
||||
57.29577951308232;
|
||||
passed=road.kind==VoxelForgeCore::ResolvedSdfStampKindV1::fillDais
|
||||
&&widthMetres>=2.0*CapsuleRadiusMetres
|
||||
&&gradeDegrees<=15.0&&roadAudit.passed;
|
||||
std::printf("[LiminalRoadWalkAudit] id=%s stations=%llu capsule_samples=%llu "
|
||||
const double widthMetres=2.0*deck.front()->halfExtentsMicrometres[1]/1.0e6;
|
||||
passed=std::all_of(deck.begin(),deck.end(),[](const auto* road)
|
||||
{return road->kind==VoxelForgeCore::ResolvedSdfStampKindV1::fillSweptDeckSegment;})
|
||||
&&pathLengthMetres>=300.0&&widthMetres>=3.0
|
||||
&&maxGradeDegrees<=15.0&&surfacePathValid&&roadAudit.passed;
|
||||
std::printf("[LiminalRoadWalkAudit] id=liminal/road-clouds-v2 "
|
||||
"deck_stamps=%zu path_length_m=%.3f stations=%llu capsule_samples=%llu "
|
||||
"blocked=%llu support_samples=%llu unsupported=%llu grade_failures=%llu "
|
||||
"width_m=%.3f capsule_diameter_m=%.3f grade_deg=%.3f grade_limit_deg=15.000 "
|
||||
"status=%s\n",road.stableId.c_str(),
|
||||
"surface_probes=%llu surface_misses=%llu width_m=%.3f minimum_width_m=3.000 "
|
||||
"max_grade_deg=%.3f grade_limit_deg=15.000 status=%s\n",
|
||||
deck.size(),pathLengthMetres,
|
||||
static_cast<unsigned long long>(roadAudit.centers),
|
||||
static_cast<unsigned long long>(roadAudit.capsuleSamples),
|
||||
static_cast<unsigned long long>(roadAudit.blocked),
|
||||
static_cast<unsigned long long>(roadAudit.supportSamples),
|
||||
static_cast<unsigned long long>(roadAudit.unsupported),
|
||||
static_cast<unsigned long long>(roadAudit.gradeFailures),widthMetres,
|
||||
2.0*CapsuleRadiusMetres,gradeDegrees,passed?"PASS":"FAIL");
|
||||
static_cast<unsigned long long>(roadAudit.gradeFailures),
|
||||
static_cast<unsigned long long>(surfaceProbes),
|
||||
static_cast<unsigned long long>(surfaceMisses),widthMetres,
|
||||
maxGradeDegrees,passed?"PASS":"FAIL");
|
||||
}
|
||||
if(!found&&required)
|
||||
{
|
||||
std::printf("[LiminalRoadWalkAudit] status=FAIL reason=road_stamp_missing\n");
|
||||
std::printf("[LiminalRoadWalkAudit] status=FAIL reason=road_path_missing\n");
|
||||
return false;
|
||||
}
|
||||
return passed;
|
||||
|
||||
@@ -587,13 +587,15 @@ namespace StandaloneWorldAssemblyPrototype
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool BuildLiminalRoadRequest(const VFJson::Value& root,
|
||||
inline bool BuildLiminalRoadRequests(const VFJson::Value& root,
|
||||
const FStandaloneOwnerWorld& owner,
|
||||
const VoxelForgeCore::WorldAssemblyManifest& assembly,
|
||||
const VoxelForgeCore::ResolvedLayoutSnapshotV3& layout,
|
||||
VoxelForgeCore::ResolvedLayoutStampRequestV1& request,std::string& error)
|
||||
std::vector<VoxelForgeCore::ResolvedLayoutStampRequestV1>& requests,
|
||||
std::string& error)
|
||||
{
|
||||
using namespace VoxelForgeCore;
|
||||
requests.clear();
|
||||
const VFJson::Value& overrideValue=root.Get("liminal_road_override");
|
||||
if(!overrideValue.IsObject())
|
||||
{error="liminal_road_override must be an object";return false;}
|
||||
@@ -604,49 +606,229 @@ namespace StandaloneWorldAssemblyPrototype
|
||||
|
||||
const VFJson::Value& strateValue=overrideValue.Get("strate_index");
|
||||
const auto* id=std::get_if<std::string>(&overrideValue.Get("stable_id").Data);
|
||||
const auto& position=overrideValue.Get("position_xy_m").AsArray();
|
||||
const auto& extents=overrideValue.Get("half_extents_m").AsArray();
|
||||
const VFJson::Value& heightValue=overrideValue.Get("deck_height_above_field_m");
|
||||
const VFJson::Value& yawValue=overrideValue.Get("yaw_quarter_turns");
|
||||
if(!strateValue.IsNumber()||!id||id->empty()||position.size()!=2||extents.size()!=3
|
||||
||!heightValue.IsNumber()||!std::isfinite(heightValue.Number())
|
||||
||!yawValue.IsNumber()||yawValue.Integer()<0||yawValue.Integer()>3)
|
||||
{error="liminal road requires stable_id, strate_index, position, extents, deck height and yaw";return false;}
|
||||
const auto& position=overrideValue.Get("start_xy_m").AsArray();
|
||||
const auto readNumber=[&](const char* key,double fallback,double& result)
|
||||
{
|
||||
const VFJson::Value& value=overrideValue.Get(key);
|
||||
if(std::holds_alternative<std::nullptr_t>(value.Data))
|
||||
{result=fallback;return true;}
|
||||
if(!value.IsNumber()||!std::isfinite(value.Number()))return false;
|
||||
result=value.Number();return true;
|
||||
};
|
||||
double pathLength=0.0,curveControl=0.0,deckHeight=0.0,riseControl=0.0;
|
||||
double halfWidth=0.0,halfThickness=0.0,sampleSpacing=0.0;
|
||||
double supportSpacing=0.0,supportRadius=0.0;
|
||||
if(!strateValue.IsNumber()||!id||id->empty()||position.size()!=2
|
||||
||!readNumber("path_length_m",360.0,pathLength)
|
||||
||!readNumber("curve_control_offset_m",84.0,curveControl)
|
||||
||!readNumber("deck_height_above_field_m",44.0,deckHeight)
|
||||
||!readNumber("rise_control_m",22.0,riseControl)
|
||||
||!readNumber("half_width_m",3.2,halfWidth)
|
||||
||!readNumber("half_thickness_m",0.65,halfThickness)
|
||||
||!readNumber("sample_spacing_m",1.0,sampleSpacing)
|
||||
||!readNumber("support_spacing_m",24.0,supportSpacing)
|
||||
||!readNumber("support_radius_m",0.55,supportRadius))
|
||||
{error="liminal road requires a valid identity, start, and finite path parameters";return false;}
|
||||
for(const auto& value:position)
|
||||
if(!value.IsNumber()||!std::isfinite(value.Number()))
|
||||
{error="liminal road position_xy_m must contain finite metre values";return false;}
|
||||
for(const auto& value:extents)
|
||||
if(!value.IsNumber()||!std::isfinite(value.Number())
|
||||
||value.Number()<0.5||value.Number()>100.0)
|
||||
{error="liminal road half_extents_m must contain values from 0.5 to 100 metres";return false;}
|
||||
if(heightValue.Number()<1.0||heightValue.Number()>100.0)
|
||||
{error="liminal road deck height must be from 1 to 100 metres above the field";return false;}
|
||||
{error="liminal road start_xy_m must contain finite metre values";return false;}
|
||||
if(std::abs(position[0].Number())>5000.0||std::abs(position[1].Number())>5000.0
|
||||
||pathLength<80.0||pathLength>1000.0||curveControl<0.0||curveControl>200.0
|
||||
||deckHeight<30.0||deckHeight>60.0||riseControl<0.0||riseControl>30.0
|
||||
||halfWidth<1.5||halfWidth>10.0||halfThickness<0.5||halfThickness>3.0
|
||||
||sampleSpacing<1.0||sampleSpacing>6.0
|
||||
||supportSpacing<8.0||supportSpacing>60.0
|
||||
||supportRadius<0.5||supportRadius>1.5)
|
||||
{error="liminal road dimensions are outside the bounded walkable-road ranges";return false;}
|
||||
const auto strateIndex=static_cast<std::int32_t>(strateValue.Integer());
|
||||
const auto strate=std::find_if(layout.strates.begin(),layout.strates.end(),
|
||||
[strateIndex](const auto& value){return value.strateIndex==strateIndex
|
||||
&&value.generatorKind=="SurfaceWorld";});
|
||||
if(strate==layout.strates.end())
|
||||
{error="liminal_road_override must select a resolved SurfaceWorld strate";return false;}
|
||||
if(extents[0].Number()<10.0||extents[1].Number()<0.75
|
||||
||extents[0].Number()<=extents[1].Number()*4.0)
|
||||
{error="liminal road must be long and narrow, with at least 1.5 metres half-width";return false;}
|
||||
|
||||
const auto metresToMicrometres=[](double value)
|
||||
{return static_cast<std::int64_t>(std::llround(value*1000000.0));};
|
||||
const std::int64_t fieldFloorMicrometres=static_cast<std::int64_t>(std::llround(
|
||||
static_cast<double>(assembly.recipe.surfaceProfile.h0Subvoxels)
|
||||
*owner.CoreVoxelSizeMicrons/WorldAssemblySubvoxelsPerVoxel));
|
||||
request={};
|
||||
request.stableId=*id;
|
||||
request.strateIndex=strateIndex;
|
||||
request.kind=ResolvedSdfStampKindV1::fillDais;
|
||||
request.anchorPositionMicrometres={metresToMicrometres(position[0].Number()),
|
||||
metresToMicrometres(position[1].Number()),
|
||||
fieldFloorMicrometres+metresToMicrometres(heightValue.Number())
|
||||
-metresToMicrometres(extents[2].Number())};
|
||||
request.halfExtentsMicrometres={metresToMicrometres(extents[0].Number()),
|
||||
metresToMicrometres(extents[1].Number()),metresToMicrometres(extents[2].Number())};
|
||||
request.yawQuarterTurns=static_cast<std::uint8_t>(yawValue.Integer());
|
||||
const std::int64_t startX=metresToMicrometres(position[0].Number());
|
||||
const std::int64_t startY=metresToMicrometres(position[1].Number());
|
||||
const std::int64_t lengthUm=metresToMicrometres(pathLength);
|
||||
const std::int64_t lateralUm=metresToMicrometres(curveControl);
|
||||
const std::int64_t deckBaseUm=fieldFloorMicrometres+metresToMicrometres(deckHeight);
|
||||
const std::int64_t riseUm=metresToMicrometres(riseControl);
|
||||
const std::int64_t halfWidthUm=metresToMicrometres(halfWidth);
|
||||
const std::int64_t halfThicknessUm=metresToMicrometres(halfThickness);
|
||||
const std::uint32_t steps=static_cast<std::uint32_t>(std::ceil(pathLength/sampleSpacing));
|
||||
const auto lerpInteger=[steps](std::int64_t a,std::int64_t b,std::uint32_t i)
|
||||
{
|
||||
const std::int64_t numerator=(b-a)*static_cast<std::int64_t>(i);
|
||||
const std::int64_t rounded=numerator>=0
|
||||
?numerator+static_cast<std::int64_t>(steps/2)
|
||||
:numerator-static_cast<std::int64_t>(steps/2);
|
||||
return a+rounded/static_cast<std::int64_t>(steps);
|
||||
};
|
||||
const auto bezier=[&](const std::array<std::int64_t,4>& controls,std::uint32_t i)
|
||||
{
|
||||
const std::int64_t a=lerpInteger(controls[0],controls[1],i);
|
||||
const std::int64_t b=lerpInteger(controls[1],controls[2],i);
|
||||
const std::int64_t c=lerpInteger(controls[2],controls[3],i);
|
||||
const std::int64_t d=lerpInteger(a,b,i);
|
||||
const std::int64_t e=lerpInteger(b,c,i);
|
||||
return lerpInteger(d,e,i);
|
||||
};
|
||||
const std::array<std::int64_t,4> xControls={startX,
|
||||
startX+lengthUm/3,startX+(2*lengthUm)/3,startX+lengthUm};
|
||||
const std::array<std::int64_t,4> yControls={startY,startY+lateralUm,
|
||||
startY-lateralUm,startY};
|
||||
const std::array<std::int64_t,4> zControls={deckBaseUm,deckBaseUm+riseUm,
|
||||
deckBaseUm-riseUm,deckBaseUm};
|
||||
std::vector<std::array<std::int64_t,3>> path;
|
||||
path.reserve(static_cast<std::size_t>(steps)+1);
|
||||
const auto halfUp=[](std::int64_t numerator,std::int64_t denominator)
|
||||
{return (numerator>=0?numerator+denominator/2:numerator-denominator/2)/denominator;};
|
||||
for(std::uint32_t i=0;i<=steps;++i)
|
||||
{
|
||||
const auto t=static_cast<std::int64_t>(i);
|
||||
const auto n=static_cast<std::int64_t>(steps);
|
||||
path.push_back({xControls[0]+halfUp(lengthUm*t,n),
|
||||
bezier(yControls,i),bezier(zControls,i)});
|
||||
}
|
||||
const auto makeStamp=[&](std::string stableId,ResolvedSdfStampKindV1 kind,
|
||||
const std::array<std::int64_t,3>& positionUm,
|
||||
const std::array<std::int64_t,3>& halfExtentsUm,std::int64_t smoothWidthUm)
|
||||
{
|
||||
ResolvedLayoutStampRequestV1 request;
|
||||
request.stableId=std::move(stableId);request.strateIndex=strateIndex;
|
||||
request.kind=kind;request.anchorPositionMicrometres=positionUm;
|
||||
request.halfExtentsMicrometres=halfExtentsUm;
|
||||
request.smoothWidthMicrometres=smoothWidthUm;
|
||||
requests.push_back(std::move(request));
|
||||
};
|
||||
const std::int64_t deckSmoothUm=250000;
|
||||
const std::int64_t segmentOverlapUm=350000;
|
||||
for(std::uint32_t i=1;i<path.size();++i)
|
||||
{
|
||||
const auto& a=path[i-1];const auto& b=path[i];
|
||||
const double dx=static_cast<double>(b[0]-a[0]);
|
||||
const double dy=static_cast<double>(b[1]-a[1]);
|
||||
const double dz=static_cast<double>(b[2]-a[2]);
|
||||
const std::int64_t halfLengthUm=static_cast<std::int64_t>(std::ceil(
|
||||
std::sqrt(dx*dx+dy*dy+dz*dz)*0.5))+segmentOverlapUm;
|
||||
ResolvedLayoutStampRequestV1 request;
|
||||
request.stableId=*id+"/deck/"+std::to_string(10000+i-1).substr(1);
|
||||
request.strateIndex=strateIndex;
|
||||
request.kind=ResolvedSdfStampKindV1::fillSweptDeckSegment;
|
||||
request.anchorPositionMicrometres={
|
||||
(a[0]+b[0])/2,(a[1]+b[1])/2,(a[2]+b[2])/2};
|
||||
request.sweepEndpointsMicrometres={a,b};
|
||||
request.halfExtentsMicrometres={halfLengthUm,halfWidthUm,halfThicknessUm};
|
||||
request.smoothWidthMicrometres=deckSmoothUm;
|
||||
requests.push_back(std::move(request));
|
||||
}
|
||||
const std::int64_t supportBaseUm=fieldFloorMicrometres-2000000;
|
||||
const std::int64_t supportRadiusUm=metresToMicrometres(supportRadius);
|
||||
double distanceSinceSupport=supportSpacing;
|
||||
std::uint32_t supportIndex=0;
|
||||
for(std::uint32_t i=0;i<path.size();++i)
|
||||
{
|
||||
if(i>0)
|
||||
{
|
||||
const auto& a=path[i-1];const auto& b=path[i];
|
||||
const double dx=static_cast<double>(b[0]-a[0]);
|
||||
const double dy=static_cast<double>(b[1]-a[1]);
|
||||
distanceSinceSupport+=std::hypot(dx,dy)/1000000.0;
|
||||
}
|
||||
if(i!=0&&i+1!=path.size()&&distanceSinceSupport<supportSpacing)continue;
|
||||
const std::int64_t supportTopUm=path[i][2]-halfThicknessUm+50000;
|
||||
const std::int64_t supportHalfHeightUm=(supportTopUm-supportBaseUm)/2;
|
||||
const std::int64_t supportCenterUm=(supportTopUm+supportBaseUm)/2;
|
||||
if(supportHalfHeightUm<500000)
|
||||
{error="liminal road support is too short to resolve safely";requests.clear();return false;}
|
||||
makeStamp(*id+"/support/"+std::to_string(10000+supportIndex).substr(1),
|
||||
ResolvedSdfStampKindV1::fillPillar,
|
||||
{path[i][0],path[i][1],supportCenterUm},
|
||||
{supportRadiusUm,supportRadiusUm,supportHalfHeightUm},100000);
|
||||
++supportIndex;distanceSinceSupport=0.0;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool BuildLiminalPillarGridRequests(const VFJson::Value& root,
|
||||
const FStandaloneOwnerWorld& owner,
|
||||
const VoxelForgeCore::WorldAssemblyManifest& assembly,
|
||||
const VoxelForgeCore::ResolvedLayoutSnapshotV3& layout,
|
||||
std::vector<VoxelForgeCore::ResolvedLayoutStampRequestV1>& requests,
|
||||
std::string& error)
|
||||
{
|
||||
using namespace VoxelForgeCore;
|
||||
requests.clear();
|
||||
const VFJson::Value& overrideValue=root.Get("liminal_pillar_grid_override");
|
||||
if(!overrideValue.IsObject())
|
||||
{error="liminal_pillar_grid_override must be an object";return false;}
|
||||
if(assembly.recipe.surfaceProfile.version!=1
|
||||
||assembly.recipe.surfaceProfile.kind!=WorldAssemblySurfaceProfileKindV1::flatField
|
||||
||owner.CoreVoxelSizeMicrons==0)
|
||||
{error="the liminal pillar grid requires a valid flatField SurfaceWorld recipe";return false;}
|
||||
const auto* id=std::get_if<std::string>(&overrideValue.Get("stable_id").Data);
|
||||
const VFJson::Value& strateValue=overrideValue.Get("strate_index");
|
||||
const auto& center=overrideValue.Get("center_xy_m").AsArray();
|
||||
const auto readNumber=[&](const char* key,double fallback,double& result)
|
||||
{
|
||||
const VFJson::Value& value=overrideValue.Get(key);
|
||||
if(std::holds_alternative<std::nullptr_t>(value.Data))
|
||||
{result=fallback;return true;}
|
||||
if(!value.IsNumber()||!std::isfinite(value.Number()))return false;
|
||||
result=value.Number();return true;
|
||||
};
|
||||
double spacing=0.0,radius=0.0,height=0.0;
|
||||
const VFJson::Value& rowsValue=overrideValue.Get("rows");
|
||||
const VFJson::Value& columnsValue=overrideValue.Get("columns");
|
||||
if(!id||id->empty()||!strateValue.IsNumber()||center.size()!=2
|
||||
||!readNumber("spacing_m",76.0,spacing)
|
||||
||!readNumber("radius_m",4.0,radius)
|
||||
||!readNumber("height_m",28.0,height)
|
||||
||!rowsValue.IsNumber()||!columnsValue.IsNumber())
|
||||
{error="liminal pillar grid requires an identity, center, dimensions and counts";return false;}
|
||||
for(const auto& value:center)
|
||||
if(!value.IsNumber()||!std::isfinite(value.Number()))
|
||||
{error="liminal pillar grid center_xy_m must contain finite metre values";return false;}
|
||||
const int rows=static_cast<int>(rowsValue.Integer());
|
||||
const int columns=static_cast<int>(columnsValue.Integer());
|
||||
if(rows<3||rows>15||columns<3||columns>15||spacing<32.0||spacing>120.0
|
||||
||radius<2.0||radius>12.0||height<12.0||height>60.0
|
||||
||std::abs(center[0].Number())>5000.0||std::abs(center[1].Number())>5000)
|
||||
{error="liminal pillar grid dimensions are outside their bounded ranges";return false;}
|
||||
const auto strateIndex=static_cast<std::int32_t>(strateValue.Integer());
|
||||
const auto strate=std::find_if(layout.strates.begin(),layout.strates.end(),
|
||||
[strateIndex](const auto& value){return value.strateIndex==strateIndex
|
||||
&&value.generatorKind=="SurfaceWorld";});
|
||||
if(strate==layout.strates.end())
|
||||
{error="liminal_pillar_grid_override must select a resolved SurfaceWorld strate";return false;}
|
||||
const auto toMicrometres=[](double value)
|
||||
{return static_cast<std::int64_t>(std::llround(value*1000000.0));};
|
||||
const std::int64_t floorUm=static_cast<std::int64_t>(std::llround(
|
||||
static_cast<double>(assembly.recipe.surfaceProfile.h0Subvoxels)
|
||||
*owner.CoreVoxelSizeMicrons/WorldAssemblySubvoxelsPerVoxel));
|
||||
const std::int64_t spacingUm=toMicrometres(spacing);
|
||||
const std::int64_t radiusUm=toMicrometres(radius);
|
||||
const std::int64_t halfHeightUm=toMicrometres(height*0.5);
|
||||
const int rowStart=-(rows/2),columnStart=-(columns/2);
|
||||
for(int row=0;row<rows;++row)
|
||||
for(int column=0;column<columns;++column)
|
||||
{
|
||||
ResolvedLayoutStampRequestV1 request;
|
||||
request.stableId=*id+"/pillar/"+std::to_string(1000+row*columns+column).substr(1);
|
||||
request.strateIndex=strateIndex;
|
||||
request.kind=ResolvedSdfStampKindV1::fillPillar;
|
||||
request.anchorPositionMicrometres={
|
||||
toMicrometres(center[0].Number())+(columnStart+column)*spacingUm,
|
||||
toMicrometres(center[1].Number())+(rowStart+row)*spacingUm,
|
||||
floorUm+halfHeightUm};
|
||||
request.halfExtentsMicrometres={radiusUm,radiusUm,halfHeightUm};
|
||||
requests.push_back(std::move(request));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user