feat(core): deterministic bounded SDF landmark stamps, off by default (L43); research R10
Stamp vocabulary v1 (carve ellipsoid/arch, fill pillar/dais) with smooth CSG and seeded relief; the director resolves records (stable ID, strate, micrometre position, conservative bounds) from hash(seed, strate, ID) with up to 512 candidates and rejects any that touch a passage mouth, walk corridor, content face or ceiling/bedrock band; a stamp BVH skips tiles outside all bounds. No requests: canonical JSON and owner hashes unchanged. --landmark-chamber-demo adds a SW chamber (buried; see review). fast/medium/full PASS on clean builds. Research R10 (read-only): ore/resource channel design. Review also records a pre-existing W1/W12 determinism mismatch at a non-spawn camera (L37+, not S14). 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
497e974178
commit
422dd7761a
12 files changed
+1138
-2
No files matched your search
@@ -144,6 +144,145 @@ namespace
|
||||
for(unsigned char c:value){hash^=c;hash*=1099511628211ull;}
|
||||
return hash;
|
||||
}
|
||||
std::uint64_t LayoutStampMix(std::uint64_t value)
|
||||
{
|
||||
value^=value>>30;value*=0xbf58476d1ce4e5b9ull;
|
||||
value^=value>>27;value*=0x94d049bb133111ebull;
|
||||
return value^(value>>31);
|
||||
}
|
||||
std::uint64_t LayoutStampSeed(std::int32_t seed,std::int32_t strate,
|
||||
std::string_view stableId)
|
||||
{
|
||||
std::uint64_t hash=14695981039346656037ull;
|
||||
TransitionHashWord(hash,static_cast<std::uint32_t>(seed));
|
||||
TransitionHashWord(hash,static_cast<std::uint32_t>(strate));
|
||||
for(unsigned char c:stableId){hash^=c;hash*=1099511628211ull;}
|
||||
return LayoutStampMix(hash);
|
||||
}
|
||||
std::uint64_t LayoutStampNext(std::uint64_t& state)
|
||||
{state=LayoutStampMix(state+0x9e3779b97f4a7c15ull);return state;}
|
||||
bool LayoutStampKindValid(ResolvedSdfStampKindV1 kind)
|
||||
{
|
||||
const auto value=static_cast<std::uint8_t>(kind);
|
||||
return value>=static_cast<std::uint8_t>(ResolvedSdfStampKindV1::carveEllipsoid)
|
||||
&&value<=static_cast<std::uint8_t>(ResolvedSdfStampKindV1::fillDais);
|
||||
}
|
||||
double LayoutStampPointSegmentDistance(const std::array<std::int64_t,3>& point,
|
||||
const std::array<std::int64_t,3>& a,const std::array<std::int64_t,3>& b)
|
||||
{
|
||||
double lengthSquared=0.0,dot=0.0;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
const double delta=static_cast<double>(b[axis])-a[axis];
|
||||
lengthSquared+=delta*delta;
|
||||
dot+=(static_cast<double>(point[axis])-a[axis])*delta;
|
||||
}
|
||||
const double t=lengthSquared>0.0?std::clamp(dot/lengthSquared,0.0,1.0):0.0;
|
||||
double distanceSquared=0.0;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
const double nearest=static_cast<double>(a[axis])
|
||||
+t*(static_cast<double>(b[axis])-a[axis]);
|
||||
const double delta=static_cast<double>(point[axis])-nearest;
|
||||
distanceSquared+=delta*delta;
|
||||
}
|
||||
return std::sqrt(distanceSquared);
|
||||
}
|
||||
double LayoutStampBoundRadius(const ResolvedLayoutStampV1& stamp)
|
||||
{
|
||||
double radiusSquared=0.0;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
const double extent=(static_cast<double>(stamp.boundsMaximumMicrometres[axis])
|
||||
-stamp.boundsMinimumMicrometres[axis])*0.5;
|
||||
radiusSquared+=extent*extent;
|
||||
}
|
||||
return std::sqrt(radiusSquared);
|
||||
}
|
||||
bool LayoutStampSafe(const ResolvedLayoutSnapshotV3& layout,
|
||||
const ResolvedLayoutStampV1& stamp,std::string* reason=nullptr)
|
||||
{
|
||||
const auto strate=std::find_if(layout.strates.begin(),layout.strates.end(),
|
||||
[&](const auto& value){return value.strateIndex==stamp.strateIndex;});
|
||||
if(strate==layout.strates.end()||!strate->content.presentInCurrentField)
|
||||
{if(reason)*reason="stamp strate has no current content band";return false;}
|
||||
if(stamp.boundsMinimumMicrometres[2]<=strate->content.bottomMicrometres
|
||||
||stamp.boundsMaximumMicrometres[2]>=strate->content.topMicrometres)
|
||||
{if(reason)*reason="stamp bounds touch or cross the ceiling/bedrock content faces";return false;}
|
||||
for(const auto* band:{&strate->ceiling,&strate->bedrock})
|
||||
if(band->presentInCurrentField
|
||||
&&stamp.boundsMinimumMicrometres[2]<band->topMicrometres
|
||||
&&stamp.boundsMaximumMicrometres[2]>band->bottomMicrometres)
|
||||
{if(reason)*reason="stamp bounds overlap a ceiling or bedrock band";return false;}
|
||||
|
||||
const double boundRadius=LayoutStampBoundRadius(stamp);
|
||||
const double coreMicrometres=std::max<std::uint32_t>(1,layout.coreVoxelSizeMicrometres);
|
||||
constexpr double RouteClearanceMicrometres=1000000.0;
|
||||
const auto routeSafe=[&](const std::array<std::int64_t,3>& a,
|
||||
const std::array<std::int64_t,3>& b,double radiusSubvoxels)
|
||||
{
|
||||
const double radius=radiusSubvoxels*coreMicrometres
|
||||
/WorldAssemblySubvoxelsPerVoxel;
|
||||
return LayoutStampPointSegmentDistance(stamp.positionMicrometres,a,b)
|
||||
>boundRadius+radius+RouteClearanceMicrometres;
|
||||
};
|
||||
const auto qToMicrometres=[&](const std::array<std::int64_t,3>& q)
|
||||
{
|
||||
std::array<std::int64_t,3> result{};
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
result[axis]=static_cast<std::int64_t>(std::llround(
|
||||
static_cast<double>(q[axis])*coreMicrometres
|
||||
/WorldAssemblySubvoxelsPerVoxel));
|
||||
return result;
|
||||
};
|
||||
for(const auto& passage:layout.passages)
|
||||
{
|
||||
if(passage.upperStrateIndex!=stamp.strateIndex
|
||||
&&passage.lowerStrateIndex!=stamp.strateIndex)continue;
|
||||
const auto upper=qToMicrometres(passage.upperMouthSubvoxels);
|
||||
const auto lower=qToMicrometres(passage.lowerMouthSubvoxels);
|
||||
const double mouthRadius=static_cast<double>(passage.mouthRadiusSubvoxels)
|
||||
*coreMicrometres/WorldAssemblySubvoxelsPerVoxel;
|
||||
if(LayoutStampPointSegmentDistance(stamp.positionMicrometres,upper,upper)
|
||||
<=boundRadius+mouthRadius+RouteClearanceMicrometres
|
||||
||LayoutStampPointSegmentDistance(stamp.positionMicrometres,lower,lower)
|
||||
<=boundRadius+mouthRadius+RouteClearanceMicrometres)
|
||||
{if(reason)*reason="stamp is inside a passage mouth safety volume";return false;}
|
||||
for(std::size_t i=0;i+1<passage.controlPoints.size();++i)
|
||||
{
|
||||
const auto a=qToMicrometres(passage.controlPoints[i].positionSubvoxels);
|
||||
const auto b=qToMicrometres(passage.controlPoints[i+1].positionSubvoxels);
|
||||
const double radius=std::max(passage.controlPoints[i].radiusSubvoxels,
|
||||
passage.controlPoints[i+1].radiusSubvoxels);
|
||||
if(!routeSafe(a,b,radius))
|
||||
{if(reason)*reason="stamp overlaps a resolved passage walk corridor";return false;}
|
||||
}
|
||||
}
|
||||
std::vector<const ResolvedLayoutAnchorV2*> route;
|
||||
for(const auto& anchor:strate->anchors)
|
||||
if(anchor.kind=="route_point"&&anchor.values.size()>=3)
|
||||
route.push_back(&anchor);
|
||||
for(std::size_t i=0;i<route.size();++i)
|
||||
{
|
||||
const auto makePoint=[&](const ResolvedLayoutAnchorV2& anchor)
|
||||
{return std::array<std::int64_t,3>{
|
||||
static_cast<std::int64_t>(std::llround(static_cast<double>(anchor.values[0])
|
||||
*coreMicrometres/WorldAssemblySubvoxelsPerVoxel)),
|
||||
static_cast<std::int64_t>(std::llround(static_cast<double>(anchor.values[1])
|
||||
*coreMicrometres/WorldAssemblySubvoxelsPerVoxel)),
|
||||
static_cast<std::int64_t>(std::llround(static_cast<double>(anchor.values[2])
|
||||
*coreMicrometres/WorldAssemblySubvoxelsPerVoxel))};};
|
||||
const auto a=makePoint(*route[i]);
|
||||
const auto b=i+1<route.size()?makePoint(*route[i+1]):a;
|
||||
const double radius=i+1<route.size()
|
||||
?std::max(route[i]->values.size()>=4?route[i]->values[3]:0,
|
||||
route[i+1]->values.size()>=4?route[i+1]->values[3]:0)
|
||||
:route[i]->values.size()>=4?route[i]->values[3]:0;
|
||||
if(!routeSafe(a,b,radius))
|
||||
{if(reason)*reason="stamp overlaps a resolved strate walk route";return false;}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
std::string LayoutHex(std::uint64_t value)
|
||||
{std::ostringstream out;out<<std::hex<<std::setfill('0')<<std::setw(16)<<value;return out.str();}
|
||||
std::string LayoutBandJson(const ResolvedLayoutBandV2& band)
|
||||
@@ -238,7 +377,34 @@ namespace
|
||||
}
|
||||
out<<"]}";
|
||||
}
|
||||
out<<"]}";return out.str();
|
||||
out<<']';
|
||||
if(!s.stamps.empty())
|
||||
{
|
||||
out<<",\"stamp_vocabulary_version\":"<<s.stampVocabularyVersion
|
||||
<<",\"stamps\":[";
|
||||
for(std::size_t i=0;i<s.stamps.size();++i)
|
||||
{
|
||||
if(i)out<<',';const auto& stamp=s.stamps[i];
|
||||
out<<"{\"stable_id\":"<<LayoutEscape(stamp.stableId)
|
||||
<<",\"strate_index\":"<<stamp.strateIndex
|
||||
<<",\"kind\":"<<static_cast<unsigned>(stamp.kind)
|
||||
<<",\"position_um\":["<<stamp.positionMicrometres[0]<<','
|
||||
<<stamp.positionMicrometres[1]<<','<<stamp.positionMicrometres[2]
|
||||
<<"],\"bounds_min_um\":["<<stamp.boundsMinimumMicrometres[0]<<','
|
||||
<<stamp.boundsMinimumMicrometres[1]<<','<<stamp.boundsMinimumMicrometres[2]
|
||||
<<"],\"bounds_max_um\":["<<stamp.boundsMaximumMicrometres[0]<<','
|
||||
<<stamp.boundsMaximumMicrometres[1]<<','<<stamp.boundsMaximumMicrometres[2]
|
||||
<<"],\"half_extents_um\":["<<stamp.halfExtentsMicrometres[0]<<','
|
||||
<<stamp.halfExtentsMicrometres[1]<<','<<stamp.halfExtentsMicrometres[2]
|
||||
<<"],\"smooth_width_um\":"<<stamp.smoothWidthMicrometres
|
||||
<<",\"irregularity_amplitude_um\":"<<stamp.irregularityAmplitudeMicrometres
|
||||
<<",\"irregularity_wavelength_um\":"<<stamp.irregularityWavelengthMicrometres
|
||||
<<",\"irregularity_seed\":"<<stamp.irregularitySeed
|
||||
<<",\"yaw_quarter_turns\":"<<static_cast<unsigned>(stamp.yawQuarterTurns)<<'}';
|
||||
}
|
||||
out<<']';
|
||||
}
|
||||
out<<'}';return out.str();
|
||||
}
|
||||
std::int64_t LayoutInt(const VFJson::Value& obj,std::string_view key)
|
||||
{return obj.Get(key).Integer();}
|
||||
@@ -659,6 +825,50 @@ bool ResolvedLayoutSnapshotV3::validate(ResolvedLayoutValidationModeV3 mode,
|
||||
add("passage_grade",passage.stableId+" is not strictly descending or exceeds the 15 degree grade limit");
|
||||
}
|
||||
}
|
||||
if(stamps.empty())
|
||||
{
|
||||
if(stampVocabularyVersion!=0)
|
||||
add("stamp_vocabulary_version","stamp vocabulary version is set without any stamp records");
|
||||
}
|
||||
else if(stampVocabularyVersion!=ResolvedLayoutStampVocabularyVersionV1)
|
||||
add("stamp_vocabulary_version","resolved stamps require vocabulary version 1");
|
||||
std::vector<std::string> stampIds;
|
||||
for(const auto& stamp:stamps)
|
||||
{
|
||||
if(stamp.stableId.empty()
|
||||
||std::find(stampIds.begin(),stampIds.end(),stamp.stableId)!=stampIds.end())
|
||||
add("stamp_identity","stamp has an empty or duplicate stable identity");
|
||||
else stampIds.push_back(stamp.stableId);
|
||||
if(!LayoutStampKindValid(stamp.kind)||stamp.yawQuarterTurns>3)
|
||||
add("stamp_kind","stamp has an unsupported kind or orientation");
|
||||
bool validExtents=true;
|
||||
for(const auto extent:stamp.halfExtentsMicrometres)
|
||||
validExtents=validExtents&&extent>=500000&&extent<=100000000;
|
||||
if(!validExtents||stamp.smoothWidthMicrometres<0
|
||||
||stamp.smoothWidthMicrometres>10000000
|
||||
||stamp.irregularityAmplitudeMicrometres<0
|
||||
||stamp.irregularityAmplitudeMicrometres>10000000
|
||||
||stamp.irregularityWavelengthMicrometres<0
|
||||
||stamp.irregularityWavelengthMicrometres>100000000
|
||||
||(stamp.irregularityAmplitudeMicrometres>0
|
||||
&&stamp.irregularityWavelengthMicrometres<=0))
|
||||
add("stamp_parameters",stamp.stableId+" has invalid or unbounded geometry parameters");
|
||||
const std::int64_t pad=stamp.smoothWidthMicrometres
|
||||
+stamp.irregularityAmplitudeMicrometres;
|
||||
const std::array<std::int64_t,3> extents={
|
||||
stamp.halfExtentsMicrometres[stamp.yawQuarterTurns%2?1:0]+pad,
|
||||
stamp.halfExtentsMicrometres[stamp.yawQuarterTurns%2?0:1]+pad,
|
||||
stamp.halfExtentsMicrometres[2]+pad};
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
if(stamp.boundsMinimumMicrometres[axis]
|
||||
!=stamp.positionMicrometres[axis]-extents[axis]
|
||||
||stamp.boundsMaximumMicrometres[axis]
|
||||
!=stamp.positionMicrometres[axis]+extents[axis])
|
||||
add("stamp_bounds",stamp.stableId+" conservative bounds do not match its parameters");
|
||||
std::string safetyReason;
|
||||
if(!LayoutStampSafe(*this,stamp,&safetyReason))
|
||||
add("stamp_placement",stamp.stableId+" is unsafe: "+safetyReason);
|
||||
}
|
||||
if(strict&&!issues.empty()){error="strict resolved-layout validation rejected "+issues.front().code;return false;}
|
||||
return true;
|
||||
}
|
||||
@@ -674,6 +884,7 @@ std::optional<ResolvedLayoutSnapshotV3> ResolvedLayoutSnapshotV3::FromJson(
|
||||
s.coreVoxelSizeMicrometres=static_cast<std::uint32_t>(LayoutInt(root,"core_voxel_size_um"));
|
||||
s.strateLayoutChunkSizeMicrometres=static_cast<std::uint32_t>(LayoutInt(root,"strate_layout_chunk_size_um"));
|
||||
s.strateLayoutChunkWidthSubvoxels=LayoutInt(root,"strate_layout_chunk_width_subvoxels");
|
||||
s.stampVocabularyVersion=static_cast<std::uint32_t>(LayoutInt(root,"stamp_vocabulary_version"));
|
||||
for(const auto& v:root.Get("strates").AsArray())
|
||||
{
|
||||
ResolvedLayoutStratumV2 x;x.stableId=LayoutString(v,"stable_id");
|
||||
@@ -721,12 +932,197 @@ std::optional<ResolvedLayoutSnapshotV3> ResolvedLayoutSnapshotV3::FromJson(
|
||||
}
|
||||
s.passages.push_back(std::move(p));
|
||||
}
|
||||
for(const auto& v:root.Get("stamps").AsArray())
|
||||
{
|
||||
ResolvedLayoutStampV1 stamp;stamp.stableId=LayoutString(v,"stable_id");
|
||||
stamp.strateIndex=static_cast<std::int32_t>(LayoutInt(v,"strate_index"));
|
||||
stamp.kind=static_cast<ResolvedSdfStampKindV1>(LayoutInt(v,"kind"));
|
||||
const auto readTriplet=[&](std::string_view key,std::array<std::int64_t,3>& target)
|
||||
{
|
||||
const auto& values=v.Get(key).AsArray();
|
||||
for(std::size_t axis=0;axis<3&&axis<values.size();++axis)
|
||||
target[axis]=values[axis].Integer();
|
||||
};
|
||||
readTriplet("position_um",stamp.positionMicrometres);
|
||||
readTriplet("bounds_min_um",stamp.boundsMinimumMicrometres);
|
||||
readTriplet("bounds_max_um",stamp.boundsMaximumMicrometres);
|
||||
readTriplet("half_extents_um",stamp.halfExtentsMicrometres);
|
||||
stamp.smoothWidthMicrometres=LayoutInt(v,"smooth_width_um");
|
||||
stamp.irregularityAmplitudeMicrometres=LayoutInt(v,"irregularity_amplitude_um");
|
||||
stamp.irregularityWavelengthMicrometres=LayoutInt(v,"irregularity_wavelength_um");
|
||||
stamp.irregularitySeed=static_cast<std::uint64_t>(LayoutInt(v,"irregularity_seed"));
|
||||
stamp.yawQuarterTurns=static_cast<std::uint8_t>(LayoutInt(v,"yaw_quarter_turns"));
|
||||
s.stamps.push_back(std::move(stamp));
|
||||
}
|
||||
const std::string serializedIdentity=LayoutString(root,"stable_identity");
|
||||
if(!s.finalize(error))return std::nullopt;
|
||||
if(s.stableIdentity!=serializedIdentity){error="resolved layout stable identity does not match its canonical payload";return std::nullopt;}
|
||||
return s;
|
||||
}
|
||||
|
||||
bool ResolveResolvedLayoutStampsV1(ResolvedLayoutSnapshotV3& layout,std::int32_t seed,
|
||||
std::span<const ResolvedLayoutStampRequestV1> requests,std::string& error)
|
||||
{
|
||||
error.clear();
|
||||
if(requests.empty())
|
||||
{
|
||||
ResolvedLayoutSnapshotV3 candidate=layout;
|
||||
candidate.stampVocabularyVersion=0;candidate.stamps.clear();
|
||||
if(!candidate.finalize(error))return false;
|
||||
layout=std::move(candidate);return true;
|
||||
}
|
||||
std::vector<ResolvedLayoutStampRequestV1> ordered(requests.begin(),requests.end());
|
||||
std::sort(ordered.begin(),ordered.end(),[](const auto& a,const auto& b)
|
||||
{return a.stableId<b.stableId;});
|
||||
ResolvedLayoutSnapshotV3 candidate=layout;
|
||||
candidate.stampVocabularyVersion=ResolvedLayoutStampVocabularyVersionV1;
|
||||
candidate.stamps.clear();candidate.stamps.reserve(ordered.size());
|
||||
for(std::size_t requestIndex=0;requestIndex<ordered.size();++requestIndex)
|
||||
{
|
||||
const auto& request=ordered[requestIndex];
|
||||
if(request.stableId.empty()||(requestIndex>0
|
||||
&&ordered[requestIndex-1].stableId==request.stableId))
|
||||
{error="stamp requests require non-empty unique stable IDs";return false;}
|
||||
if(!LayoutStampKindValid(request.kind)||request.yawQuarterTurns>3
|
||||
||request.placementRadiusMicrometres<0
|
||||
||request.placementRadiusMicrometres>100000000
|
||||
||request.smoothWidthMicrometres<0||request.smoothWidthMicrometres>10000000
|
||||
||request.irregularityAmplitudeMicrometres<0
|
||||
||request.irregularityAmplitudeMicrometres>10000000
|
||||
||request.irregularityWavelengthMicrometres<0
|
||||
||request.irregularityWavelengthMicrometres>100000000
|
||||
||(request.irregularityAmplitudeMicrometres>0
|
||||
&&request.irregularityWavelengthMicrometres<=0))
|
||||
{error="stamp request "+request.stableId+" has invalid or unbounded parameters";return false;}
|
||||
if(std::any_of(request.halfExtentsMicrometres.begin(),request.halfExtentsMicrometres.end(),
|
||||
[](std::int64_t extent){return extent<500000||extent>100000000;}))
|
||||
{error="stamp request "+request.stableId+" has an extent outside 0.5..100 m";return false;}
|
||||
const auto stratum=std::find_if(candidate.strates.begin(),candidate.strates.end(),
|
||||
[&](const auto& value){return value.strateIndex==request.strateIndex;});
|
||||
if(stratum==candidate.strates.end())
|
||||
{error="stamp request "+request.stableId+" names an unknown strate";return false;}
|
||||
for(const auto coordinate:request.anchorPositionMicrometres)
|
||||
if(coordinate<-1000000000000ll||coordinate>1000000000000ll)
|
||||
{error="stamp request "+request.stableId+" has an unbounded anchor";return false;}
|
||||
|
||||
// The shared JSON value type stores numbers as IEEE-754 doubles; keep the
|
||||
// serialized seed in its exact integer range so the canonical round trip holds.
|
||||
const std::uint64_t baseSeed=LayoutStampSeed(seed,request.strateIndex,request.stableId)
|
||||
&0x001fffffffffffffull;
|
||||
std::uint64_t state=baseSeed;
|
||||
const auto randomOffset=[&](std::uint64_t bits)
|
||||
{
|
||||
const std::int64_t radius=request.placementRadiusMicrometres;
|
||||
if(radius==0)return std::int64_t{0};
|
||||
const std::uint64_t span=static_cast<std::uint64_t>(radius)*2+1;
|
||||
return static_cast<std::int64_t>(bits%span)-radius;
|
||||
};
|
||||
bool placed=false;
|
||||
for(std::uint32_t attempt=0;attempt<(request.placementRadiusMicrometres==0?1u:512u);++attempt)
|
||||
{
|
||||
ResolvedLayoutStampV1 stamp;stamp.stableId=request.stableId;
|
||||
stamp.strateIndex=request.strateIndex;stamp.kind=request.kind;
|
||||
stamp.halfExtentsMicrometres=request.halfExtentsMicrometres;
|
||||
stamp.smoothWidthMicrometres=request.smoothWidthMicrometres;
|
||||
stamp.irregularityAmplitudeMicrometres=request.irregularityAmplitudeMicrometres;
|
||||
stamp.irregularityWavelengthMicrometres=request.irregularityWavelengthMicrometres;
|
||||
stamp.irregularitySeed=baseSeed;stamp.yawQuarterTurns=request.yawQuarterTurns;
|
||||
std::array<std::int64_t,3> offset{};
|
||||
if(request.placementRadiusMicrometres>0)
|
||||
{
|
||||
offset={randomOffset(LayoutStampNext(state)),
|
||||
randomOffset(LayoutStampNext(state)),
|
||||
randomOffset(LayoutStampNext(state))/3};
|
||||
const long double radius=static_cast<long double>(request.placementRadiusMicrometres);
|
||||
const long double distanceSquared=static_cast<long double>(offset[0])*offset[0]
|
||||
+static_cast<long double>(offset[1])*offset[1]
|
||||
+static_cast<long double>(offset[2])*offset[2];
|
||||
if(distanceSquared>radius*radius)continue;
|
||||
}
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
stamp.positionMicrometres[axis]=request.anchorPositionMicrometres[axis]+offset[axis];
|
||||
const std::int64_t pad=stamp.smoothWidthMicrometres
|
||||
+stamp.irregularityAmplitudeMicrometres;
|
||||
const std::array<std::int64_t,3> extents={
|
||||
stamp.halfExtentsMicrometres[stamp.yawQuarterTurns%2?1:0]+pad,
|
||||
stamp.halfExtentsMicrometres[stamp.yawQuarterTurns%2?0:1]+pad,
|
||||
stamp.halfExtentsMicrometres[2]+pad};
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
stamp.boundsMinimumMicrometres[axis]=stamp.positionMicrometres[axis]-extents[axis];
|
||||
stamp.boundsMaximumMicrometres[axis]=stamp.positionMicrometres[axis]+extents[axis];
|
||||
}
|
||||
if(!LayoutStampSafe(candidate,stamp))continue;
|
||||
candidate.stamps.push_back(std::move(stamp));placed=true;break;
|
||||
}
|
||||
if(!placed)
|
||||
{error="stamp request "+request.stableId+" has no deterministic passage-safe content placement";return false;}
|
||||
}
|
||||
if(!candidate.finalize(error))return false;
|
||||
std::vector<ResolvedLayoutIssueV2> issues;std::string strictError;
|
||||
if(!candidate.validate(ResolvedLayoutValidationModeV3::strict,issues,strictError))
|
||||
{error="strict stamp placement validation failed: "+strictError;return false;}
|
||||
layout=std::move(candidate);return true;
|
||||
}
|
||||
|
||||
ResolvedLayoutStampPlacementAuditV1 AuditResolvedLayoutStampPlacementV1(
|
||||
const ResolvedLayoutSnapshotV3& layout)
|
||||
{
|
||||
ResolvedLayoutStampPlacementAuditV1 result;
|
||||
const auto makeControl=[&](const std::array<std::int64_t,3>& position,
|
||||
std::int32_t strateIndex,std::array<std::int64_t,3> minimum,
|
||||
std::array<std::int64_t,3> maximum,std::string id,
|
||||
std::string_view expectedReason)
|
||||
{
|
||||
ResolvedLayoutSnapshotV3 candidate=layout;
|
||||
ResolvedLayoutStampV1 stamp;stamp.stableId=std::move(id);
|
||||
stamp.strateIndex=strateIndex;stamp.kind=ResolvedSdfStampKindV1::carveEllipsoid;
|
||||
stamp.positionMicrometres=position;stamp.boundsMinimumMicrometres=minimum;
|
||||
stamp.boundsMaximumMicrometres=maximum;
|
||||
stamp.halfExtentsMicrometres={250000,250000,250000};
|
||||
stamp.irregularitySeed=1;
|
||||
candidate.stampVocabularyVersion=ResolvedLayoutStampVocabularyVersionV1;
|
||||
candidate.stamps={stamp};
|
||||
std::vector<ResolvedLayoutIssueV2> issues;std::string error;
|
||||
++result.negativeControlsRun;
|
||||
if(!candidate.validate(ResolvedLayoutValidationModeV3::strict,issues,error)
|
||||
&&std::any_of(issues.begin(),issues.end(),[&](const auto& issue)
|
||||
{return issue.code=="stamp_placement"
|
||||
&&issue.detail.find(expectedReason)!=std::string::npos;}))
|
||||
++result.negativeControlsRejected;
|
||||
};
|
||||
const auto passage=std::find_if(layout.passages.begin(),layout.passages.end(),
|
||||
[](const auto& value){return value.controlPoints.size()>=2;});
|
||||
if(passage!=layout.passages.end())
|
||||
{
|
||||
const double scale=static_cast<double>(layout.coreVoxelSizeMicrometres)
|
||||
/WorldAssemblySubvoxelsPerVoxel;
|
||||
const std::size_t routeControlIndex=passage->lowerMouthControlIndex>4
|
||||
?passage->lowerMouthControlIndex-4:0;
|
||||
std::array<std::int64_t,3> position{};
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
position[axis]=static_cast<std::int64_t>(std::llround(
|
||||
passage->controlPoints[routeControlIndex].positionSubvoxels[axis]*scale));
|
||||
makeControl(position,passage->lowerStrateIndex,
|
||||
{position[0]-250000,position[1]-250000,position[2]-250000},
|
||||
{position[0]+250000,position[1]+250000,position[2]+250000},
|
||||
"negative-control/forced-on-route","resolved passage walk corridor");
|
||||
}
|
||||
const auto stratum=std::find_if(layout.strates.begin(),layout.strates.end(),
|
||||
[](const auto& value){return value.content.presentInCurrentField;});
|
||||
if(stratum!=layout.strates.end())
|
||||
{
|
||||
const std::array<std::int64_t,3> position={0,0,stratum->content.topMicrometres};
|
||||
makeControl(position,stratum->strateIndex,
|
||||
{position[0]-250000,position[1]-250000,position[2]-250000},
|
||||
{position[0]+250000,position[1]+250000,position[2]+250000},
|
||||
"negative-control/forced-into-ceiling-band","touch or cross the ceiling/bedrock");
|
||||
}
|
||||
result.passed=result.negativeControlsRun==2
|
||||
&&result.negativeControlsRejected==result.negativeControlsRun;
|
||||
return result;
|
||||
}
|
||||
|
||||
bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t seed,
|
||||
const std::vector<ResolvedLayoutPassageConfigV1>& configs,std::string& error)
|
||||
{
|
||||
|
||||
@@ -168,6 +168,7 @@ public:
|
||||
minimumSubvoxels_,maximumSubvoxels_);
|
||||
featureIndices_=shape.BuildPassageFeatureTileList(
|
||||
minimumSubvoxels_,maximumSubvoxels_);
|
||||
stampIndices_=shape.BuildStampTileList(minimumSubvoxels_,maximumSubvoxels_);
|
||||
passageRoutes_.assign(shape.ResolvedLayout?shape.ResolvedLayout->passages.size():0,0);
|
||||
passageColumns_=passageRoutes_;
|
||||
passageMouthSupports_=passageRoutes_;
|
||||
@@ -216,11 +217,14 @@ public:
|
||||
&&shape_->ResolvedBoundaryBandMayAffectAtZMicrometres(zMicrometres);
|
||||
}
|
||||
std::size_t featureCount() const{return featureIndices_.size();}
|
||||
bool hasStampCandidates() const{return !stampIndices_.empty();}
|
||||
const std::vector<std::uint32_t>& stampCandidates() const{return stampIndices_;}
|
||||
|
||||
private:
|
||||
const FStandaloneShapeWorld* shape_;
|
||||
std::array<std::int64_t,3> minimumSubvoxels_,maximumSubvoxels_;
|
||||
std::vector<std::uint32_t> featureIndices_;
|
||||
std::vector<std::uint32_t> stampIndices_;
|
||||
std::vector<std::uint8_t> passageRoutes_,passageColumns_,passageMouthSupports_;
|
||||
bool routeFeatureOverlap_=false,columnZOverlap_=false;
|
||||
bool mayContainResolvedBoundaryBand_=true;
|
||||
@@ -1775,7 +1779,11 @@ public:
|
||||
}
|
||||
|
||||
float ApplyResolvedStrateComposition(float density,float X,float Y,float Z) const
|
||||
{return ApplyResolvedStratePassageCarves(ApplyResolvedStrateBoundaryBand(density,X,Y,Z),X,Y,Z);}
|
||||
{
|
||||
const float composed=ApplyResolvedStratePassageCarves(
|
||||
ApplyResolvedStrateBoundaryBand(density,X,Y,Z),X,Y,Z);
|
||||
return StampRuntime.empty()?composed:ApplyResolvedSdfStamps(composed,X,Y,Z);
|
||||
}
|
||||
|
||||
bool ResolvedPassageIntersectsBox(const int32 minimum[3],const int32 maximum[3]) const
|
||||
{
|
||||
@@ -3170,6 +3178,169 @@ private:
|
||||
std::size_t first=0,end=0;
|
||||
int left=-1,right=-1;
|
||||
};
|
||||
struct FStampSpatialBound
|
||||
{
|
||||
std::array<std::int64_t,3> minimumSubvoxels{},maximumSubvoxels{};
|
||||
};
|
||||
struct FStampSpatialNode
|
||||
{
|
||||
std::array<std::int64_t,3> minimumSubvoxels{},maximumSubvoxels{};
|
||||
std::size_t first=0,end=0;
|
||||
int left=-1,right=-1;
|
||||
};
|
||||
struct FStampRuntime
|
||||
{
|
||||
const VoxelForgeCore::ResolvedLayoutStampV1* record=nullptr;
|
||||
std::array<float,3> positionVoxels{},halfExtentsVoxels{};
|
||||
float smoothWidthVoxels=0.0f;
|
||||
float irregularityAmplitudeVoxels=0.0f;
|
||||
float irregularityWavelengthVoxels=0.0f;
|
||||
std::uint64_t irregularitySeed=0;
|
||||
std::uint8_t quarterTurns=0;
|
||||
};
|
||||
|
||||
int BuildStampSpatialNode(std::size_t first,std::size_t end)
|
||||
{
|
||||
FStampSpatialNode node;node.first=first;node.end=end;
|
||||
node.minimumSubvoxels.fill(std::numeric_limits<std::int64_t>::max());
|
||||
node.maximumSubvoxels.fill(std::numeric_limits<std::int64_t>::min());
|
||||
for(std::size_t offset=first;offset<end;++offset)
|
||||
{
|
||||
const auto& bound=StampSpatialBounds[StampSpatialOrder[offset]];
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
node.minimumSubvoxels[axis]=std::min(node.minimumSubvoxels[axis],
|
||||
bound.minimumSubvoxels[axis]);
|
||||
node.maximumSubvoxels[axis]=std::max(node.maximumSubvoxels[axis],
|
||||
bound.maximumSubvoxels[axis]);
|
||||
}
|
||||
}
|
||||
const int nodeIndex=static_cast<int>(StampSpatialNodes.size());
|
||||
StampSpatialNodes.push_back(node);
|
||||
if(end-first>8)
|
||||
{
|
||||
std::size_t splitAxis=0;
|
||||
for(std::size_t axis=1;axis<3;++axis)
|
||||
if(static_cast<long double>(node.maximumSubvoxels[axis])
|
||||
-node.minimumSubvoxels[axis]
|
||||
>static_cast<long double>(node.maximumSubvoxels[splitAxis])
|
||||
-node.minimumSubvoxels[splitAxis])splitAxis=axis;
|
||||
const std::size_t middle=first+(end-first)/2;
|
||||
std::nth_element(StampSpatialOrder.begin()+first,
|
||||
StampSpatialOrder.begin()+middle,StampSpatialOrder.begin()+end,
|
||||
[this,splitAxis](std::uint32_t a,std::uint32_t b)
|
||||
{
|
||||
const auto& left=StampSpatialBounds[a];const auto& right=StampSpatialBounds[b];
|
||||
const long double leftCenter=static_cast<long double>(
|
||||
left.minimumSubvoxels[splitAxis])+left.maximumSubvoxels[splitAxis];
|
||||
const long double rightCenter=static_cast<long double>(
|
||||
right.minimumSubvoxels[splitAxis])+right.maximumSubvoxels[splitAxis];
|
||||
return leftCenter==rightCenter?a<b:leftCenter<rightCenter;
|
||||
});
|
||||
StampSpatialNodes[nodeIndex].left=BuildStampSpatialNode(first,middle);
|
||||
StampSpatialNodes[nodeIndex].right=BuildStampSpatialNode(middle,end);
|
||||
}
|
||||
return nodeIndex;
|
||||
}
|
||||
|
||||
void QueryStampSpatialNode(int nodeIndex,
|
||||
const std::array<std::int64_t,3>& minimum,
|
||||
const std::array<std::int64_t,3>& maximum,
|
||||
std::vector<std::uint32_t>& result) const
|
||||
{
|
||||
if(nodeIndex<0)return;
|
||||
const auto& node=StampSpatialNodes[static_cast<std::size_t>(nodeIndex)];
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
if(node.maximumSubvoxels[axis]<minimum[axis]
|
||||
||node.minimumSubvoxels[axis]>maximum[axis])return;
|
||||
if(node.left<0)
|
||||
{
|
||||
for(std::size_t offset=node.first;offset<node.end;++offset)
|
||||
{
|
||||
const auto index=StampSpatialOrder[offset];
|
||||
const auto& bound=StampSpatialBounds[index];bool overlap=true;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
overlap=overlap&&bound.minimumSubvoxels[axis]<=maximum[axis]
|
||||
&&bound.maximumSubvoxels[axis]>=minimum[axis];
|
||||
if(overlap)result.push_back(index);
|
||||
}
|
||||
return;
|
||||
}
|
||||
QueryStampSpatialNode(node.left,minimum,maximum,result);
|
||||
QueryStampSpatialNode(node.right,minimum,maximum,result);
|
||||
}
|
||||
|
||||
void BuildStampSpatialIndex()
|
||||
{
|
||||
StampSpatialBounds.clear();StampSpatialOrder.clear();StampSpatialNodes.clear();
|
||||
StampRuntime.clear();
|
||||
if(!ResolvedLayout||ResolvedLayout->stamps.empty()||!Owner
|
||||
||Owner->CoreVoxelSizeMicrons==0)return;
|
||||
constexpr long double Q=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
||||
const long double scale=Q/Owner->CoreVoxelSizeMicrons;
|
||||
StampSpatialBounds.reserve(ResolvedLayout->stamps.size());
|
||||
StampRuntime.reserve(ResolvedLayout->stamps.size());
|
||||
for(const auto& stamp:ResolvedLayout->stamps)
|
||||
{
|
||||
FStampSpatialBound bound;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
bound.minimumSubvoxels[axis]=static_cast<std::int64_t>(std::floor(
|
||||
static_cast<long double>(stamp.boundsMinimumMicrometres[axis])*scale));
|
||||
bound.maximumSubvoxels[axis]=static_cast<std::int64_t>(std::ceil(
|
||||
static_cast<long double>(stamp.boundsMaximumMicrometres[axis])*scale));
|
||||
}
|
||||
StampSpatialBounds.push_back(bound);
|
||||
FStampRuntime runtime;runtime.record=&stamp;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
runtime.positionVoxels[axis]=static_cast<float>(
|
||||
static_cast<double>(stamp.positionMicrometres[axis])
|
||||
/Owner->CoreVoxelSizeMicrons);
|
||||
const std::size_t xAxis=stamp.yawQuarterTurns%2?1:0;
|
||||
const std::size_t yAxis=stamp.yawQuarterTurns%2?0:1;
|
||||
runtime.halfExtentsVoxels={
|
||||
static_cast<float>(static_cast<double>(stamp.halfExtentsMicrometres[0])
|
||||
/Owner->CoreVoxelSizeMicrons),
|
||||
static_cast<float>(static_cast<double>(stamp.halfExtentsMicrometres[1])
|
||||
/Owner->CoreVoxelSizeMicrons),
|
||||
static_cast<float>(static_cast<double>(stamp.halfExtentsMicrometres[2])
|
||||
/Owner->CoreVoxelSizeMicrons)};
|
||||
(void)xAxis;(void)yAxis;
|
||||
runtime.smoothWidthVoxels=static_cast<float>(
|
||||
static_cast<double>(stamp.smoothWidthMicrometres)/Owner->CoreVoxelSizeMicrons);
|
||||
runtime.irregularityAmplitudeVoxels=static_cast<float>(
|
||||
static_cast<double>(stamp.irregularityAmplitudeMicrometres)
|
||||
/Owner->CoreVoxelSizeMicrons);
|
||||
runtime.irregularityWavelengthVoxels=static_cast<float>(
|
||||
static_cast<double>(stamp.irregularityWavelengthMicrometres)
|
||||
/Owner->CoreVoxelSizeMicrons);
|
||||
runtime.irregularitySeed=stamp.irregularitySeed;
|
||||
runtime.quarterTurns=stamp.yawQuarterTurns;
|
||||
StampRuntime.push_back(runtime);
|
||||
}
|
||||
StampSpatialOrder.resize(StampSpatialBounds.size());
|
||||
for(std::size_t i=0;i<StampSpatialOrder.size();++i)
|
||||
StampSpatialOrder[i]=static_cast<std::uint32_t>(i);
|
||||
if(!StampSpatialOrder.empty())BuildStampSpatialNode(0,StampSpatialOrder.size());
|
||||
}
|
||||
|
||||
std::vector<std::uint32_t> BuildStampTileList(
|
||||
const std::array<std::int64_t,3>& minimum,
|
||||
const std::array<std::int64_t,3>& maximum) const
|
||||
{
|
||||
std::vector<std::uint32_t> result;
|
||||
if(StampSpatialBounds.empty())return result;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
if(minimum[axis]>maximum[axis])
|
||||
{
|
||||
result.resize(StampSpatialBounds.size());
|
||||
for(std::size_t i=0;i<result.size();++i)
|
||||
result[i]=static_cast<std::uint32_t>(i);
|
||||
return result;
|
||||
}
|
||||
QueryStampSpatialNode(0,minimum,maximum,result);
|
||||
return result;
|
||||
}
|
||||
|
||||
int BuildPassageNearestNode(std::size_t passageIndex,std::vector<std::size_t> segments)
|
||||
{
|
||||
@@ -3343,6 +3514,7 @@ private:
|
||||
|
||||
void BuildPassageSpatialIndices()
|
||||
{
|
||||
BuildStampSpatialIndex();
|
||||
PassageTileFeatures.clear();PassageFeatureBounds.clear();
|
||||
PassageFeatureOrder.clear();PassageFeatureNodes.clear();
|
||||
PassageFeatureBoundsFailOpen=false;
|
||||
@@ -4270,6 +4442,19 @@ public:
|
||||
const int32 Pad=std::max(Step,1);
|
||||
const int32 BMin=Origin.Z-Pad;
|
||||
const int32 BMax=Origin.Z+(Cells+1)*Step;
|
||||
if(!StampSpatialBounds.empty())
|
||||
{
|
||||
constexpr std::int64_t Q=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
||||
const std::array<std::int64_t,3> minimum={
|
||||
static_cast<std::int64_t>(Origin.X-Pad)*Q,
|
||||
static_cast<std::int64_t>(Origin.Y-Pad)*Q,
|
||||
static_cast<std::int64_t>(BMin)*Q};
|
||||
const std::array<std::int64_t,3> maximum={
|
||||
static_cast<std::int64_t>(Origin.X+(Cells+1)*Step)*Q,
|
||||
static_cast<std::int64_t>(Origin.Y+(Cells+1)*Step)*Q,
|
||||
static_cast<std::int64_t>(BMax)*Q};
|
||||
if(!BuildStampTileList(minimum,maximum).empty())return false;
|
||||
}
|
||||
const float LayoutWidth=static_cast<float>(GetResolvedLayoutChunkWidthVoxels());
|
||||
const int32 FirstChunk=FMath::FloorToInt(static_cast<float>(BMin)/LayoutWidth);
|
||||
const int32 LastChunk=FMath::FloorToInt(static_cast<float>(BMax)/LayoutWidth);
|
||||
@@ -4360,6 +4545,20 @@ public:
|
||||
else ++Counts.MixedBoxes;
|
||||
return Result;
|
||||
};
|
||||
if(!StampSpatialBounds.empty())
|
||||
{
|
||||
constexpr std::int64_t Q=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
||||
const std::array<std::int64_t,3> minimum={
|
||||
static_cast<std::int64_t>(BMin[0])*Q,
|
||||
static_cast<std::int64_t>(BMin[1])*Q,
|
||||
static_cast<std::int64_t>(BMin[2])*Q};
|
||||
const std::array<std::int64_t,3> maximum={
|
||||
static_cast<std::int64_t>(BMax[0])*Q,
|
||||
static_cast<std::int64_t>(BMax[1])*Q,
|
||||
static_cast<std::int64_t>(BMax[2])*Q};
|
||||
if(!BuildStampTileList(minimum,maximum).empty())
|
||||
return Finish(EVoxelTileClass::Mixed,true);
|
||||
}
|
||||
if(ResolvedPassageIntersectsBox(BMin,BMax))
|
||||
return Finish(EVoxelTileClass::Mixed,true);
|
||||
if(!EditBrushes.empty()&&EditBoundsMin.X<=BMax[0]&&EditBoundsMax.X>=BMin[0]
|
||||
@@ -5368,6 +5567,157 @@ private:
|
||||
}
|
||||
static float SmoothMaxDistance(float A,float B,float Width)
|
||||
{return -SmoothMinDistance(-A,-B,Width);}
|
||||
static std::uint64_t StampMix64(std::uint64_t value)
|
||||
{
|
||||
value^=value>>30;value*=0xbf58476d1ce4e5b9ull;
|
||||
value^=value>>27;value*=0x94d049bb133111ebull;
|
||||
return value^(value>>31);
|
||||
}
|
||||
static float StampLatticeValue(std::uint64_t seed,std::int64_t x,
|
||||
std::int64_t y,std::int64_t z)
|
||||
{
|
||||
std::uint64_t hash=StampMix64(seed^StampMix64(static_cast<std::uint64_t>(x)));
|
||||
hash=StampMix64(hash^StampMix64(static_cast<std::uint64_t>(y)));
|
||||
hash=StampMix64(hash^StampMix64(static_cast<std::uint64_t>(z)));
|
||||
const double unit=static_cast<double>(hash>>11)
|
||||
/static_cast<double>((std::uint64_t{1}<<53)-1);
|
||||
return static_cast<float>(unit*2.0-1.0);
|
||||
}
|
||||
static float StampValueNoise(float x,float y,float z,float wavelength,
|
||||
std::uint64_t seed)
|
||||
{
|
||||
if(!(wavelength>0.0f))return 0.0f;
|
||||
const double sx=static_cast<double>(x)/wavelength;
|
||||
const double sy=static_cast<double>(y)/wavelength;
|
||||
const double sz=static_cast<double>(z)/wavelength;
|
||||
const std::int64_t ix=static_cast<std::int64_t>(std::floor(sx));
|
||||
const std::int64_t iy=static_cast<std::int64_t>(std::floor(sy));
|
||||
const std::int64_t iz=static_cast<std::int64_t>(std::floor(sz));
|
||||
const auto smooth=[](float t){return t*t*(3.0f-2.0f*t);};
|
||||
const float tx=smooth(static_cast<float>(sx-ix));
|
||||
const float ty=smooth(static_cast<float>(sy-iy));
|
||||
const float tz=smooth(static_cast<float>(sz-iz));
|
||||
const auto lerp=[](float a,float b,float t){return a+(b-a)*t;};
|
||||
const float z0=lerp(lerp(StampLatticeValue(seed,ix,iy,iz),
|
||||
StampLatticeValue(seed,ix+1,iy,iz),tx),
|
||||
lerp(StampLatticeValue(seed,ix,iy+1,iz),
|
||||
StampLatticeValue(seed,ix+1,iy+1,iz),tx),ty);
|
||||
const float z1=lerp(lerp(StampLatticeValue(seed,ix,iy,iz+1),
|
||||
StampLatticeValue(seed,ix+1,iy,iz+1),tx),
|
||||
lerp(StampLatticeValue(seed,ix,iy+1,iz+1),
|
||||
StampLatticeValue(seed,ix+1,iy+1,iz+1),tx),ty);
|
||||
return lerp(z0,z1,tz);
|
||||
}
|
||||
static float StampEllipsoidSdf(float x,float y,float z,
|
||||
const std::array<float,3>& radius)
|
||||
{
|
||||
const float minimum=std::min({radius[0],radius[1],radius[2]});
|
||||
if(!(minimum>0.0f))return std::numeric_limits<float>::infinity();
|
||||
const float k0=std::sqrt((x/radius[0])*(x/radius[0])
|
||||
+(y/radius[1])*(y/radius[1])+(z/radius[2])*(z/radius[2]));
|
||||
const float k1=std::sqrt((x/(radius[0]*radius[0]))*(x/(radius[0]*radius[0]))
|
||||
+(y/(radius[1]*radius[1]))*(y/(radius[1]*radius[1]))
|
||||
+(z/(radius[2]*radius[2]))*(z/(radius[2]*radius[2])));
|
||||
return k1>1.0e-8f?k0*(k0-1.0f)/k1:-minimum;
|
||||
}
|
||||
static float StampRoundedBoxSdf(float x,float y,float z,
|
||||
const std::array<float,3>& halfSize,float rounding)
|
||||
{
|
||||
const float qx=std::abs(x)-halfSize[0]+rounding;
|
||||
const float qy=std::abs(y)-halfSize[1]+rounding;
|
||||
const float qz=std::abs(z)-halfSize[2]+rounding;
|
||||
const float outside=std::sqrt(std::max(qx,0.0f)*std::max(qx,0.0f)
|
||||
+std::max(qy,0.0f)*std::max(qy,0.0f)
|
||||
+std::max(qz,0.0f)*std::max(qz,0.0f));
|
||||
return outside+std::min(std::max({qx,qy,qz}),0.0f)-rounding;
|
||||
}
|
||||
static float StampPillarSdf(float x,float y,float z,
|
||||
const std::array<float,3>& halfSize)
|
||||
{
|
||||
const float radial=(std::sqrt((x/halfSize[0])*(x/halfSize[0])
|
||||
+(y/halfSize[1])*(y/halfSize[1]))-1.0f)
|
||||
*std::min(halfSize[0],halfSize[1]);
|
||||
const float vertical=std::abs(z)-halfSize[2];
|
||||
const float outside=std::sqrt(std::max(radial,0.0f)*std::max(radial,0.0f)
|
||||
+std::max(vertical,0.0f)*std::max(vertical,0.0f));
|
||||
return outside+std::min(std::max(radial,vertical),0.0f);
|
||||
}
|
||||
static float StampArchSdf(float x,float y,float z,
|
||||
const std::array<float,3>& halfSize)
|
||||
{
|
||||
const float round=std::min(0.25f,std::min(halfSize[0],halfSize[1])*0.08f);
|
||||
const float lower=StampRoundedBoxSdf(x,y,z+halfSize[2]*0.5f,
|
||||
{halfSize[0],halfSize[1],halfSize[2]*0.5f},round);
|
||||
const float top=std::max(StampEllipsoidSdf(x,y,z,halfSize),-z);
|
||||
return SmoothMinDistance(lower,top,round*2.0f);
|
||||
}
|
||||
float StampSdf(const FStampRuntime& stamp,float x,float y,float z) const
|
||||
{
|
||||
float localX=x-stamp.positionVoxels[0];
|
||||
float localY=y-stamp.positionVoxels[1];
|
||||
const float localZ=z-stamp.positionVoxels[2];
|
||||
switch(stamp.quarterTurns&3u)
|
||||
{
|
||||
case 1:{const float oldX=localX;localX=localY;localY=-oldX;break;}
|
||||
case 2:localX=-localX;localY=-localY;break;
|
||||
case 3:{const float oldX=localX;localX=-localY;localY=oldX;break;}
|
||||
default:break;
|
||||
}
|
||||
float distance=0.0f;
|
||||
switch(stamp.record->kind)
|
||||
{
|
||||
case VoxelForgeCore::ResolvedSdfStampKindV1::carveEllipsoid:
|
||||
distance=StampEllipsoidSdf(localX,localY,localZ,stamp.halfExtentsVoxels);break;
|
||||
case VoxelForgeCore::ResolvedSdfStampKindV1::carveArch:
|
||||
distance=StampArchSdf(localX,localY,localZ,stamp.halfExtentsVoxels);break;
|
||||
case VoxelForgeCore::ResolvedSdfStampKindV1::fillPillar:
|
||||
distance=StampPillarSdf(localX,localY,localZ,stamp.halfExtentsVoxels);break;
|
||||
case VoxelForgeCore::ResolvedSdfStampKindV1::fillDais:
|
||||
distance=StampRoundedBoxSdf(localX,localY,localZ,stamp.halfExtentsVoxels,
|
||||
std::min(0.35f,std::min({stamp.halfExtentsVoxels[0],
|
||||
stamp.halfExtentsVoxels[1],stamp.halfExtentsVoxels[2]})*0.15f));break;
|
||||
default:return std::numeric_limits<float>::infinity();
|
||||
}
|
||||
if(stamp.irregularityAmplitudeVoxels>0.0f)
|
||||
distance+=StampValueNoise(localX,localY,localZ,
|
||||
stamp.irregularityWavelengthVoxels,stamp.irregularitySeed)
|
||||
*stamp.irregularityAmplitudeVoxels;
|
||||
return distance;
|
||||
}
|
||||
float ApplyResolvedSdfStamps(float density,float X,float Y,float Z) const
|
||||
{
|
||||
if(StampRuntime.empty()||!Owner||Owner->CoreVoxelSizeMicrons==0)return density;
|
||||
const auto* tileQueries=ActivePassageTileQueries;
|
||||
const bool scoped=tileQueries&&tileQueries->matches(*this);
|
||||
if(scoped&&!tileQueries->hasStampCandidates())return density;
|
||||
const auto* bound=FindBoundStratumForManagerZ(Z);
|
||||
if(!bound)return density;
|
||||
const double micronsPerVoxel=Owner->CoreVoxelSizeMicrons;
|
||||
const std::array<double,3> worldMicrometres={X*micronsPerVoxel,
|
||||
Y*micronsPerVoxel,Z*micronsPerVoxel};
|
||||
float composed=density;
|
||||
const auto apply=[&](std::uint32_t index)
|
||||
{
|
||||
if(index>=StampRuntime.size())return;
|
||||
const auto& runtime=StampRuntime[index];const auto& record=*runtime.record;
|
||||
if(record.strateIndex!=bound->StrateIndex)return;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
if(worldMicrometres[axis]<record.boundsMinimumMicrometres[axis]
|
||||
||worldMicrometres[axis]>record.boundsMaximumMicrometres[axis])return;
|
||||
const float distance=StampSdf(runtime,X,Y,Z);
|
||||
if(record.kind==VoxelForgeCore::ResolvedSdfStampKindV1::carveEllipsoid
|
||||
||record.kind==VoxelForgeCore::ResolvedSdfStampKindV1::carveArch)
|
||||
composed=SmoothMaxDistance(composed,-distance,runtime.smoothWidthVoxels);
|
||||
else
|
||||
composed=SmoothMinDistance(composed,distance,runtime.smoothWidthVoxels);
|
||||
};
|
||||
if(scoped)
|
||||
for(const auto index:tileQueries->stampCandidates())apply(index);
|
||||
else
|
||||
for(std::size_t index=0;index<StampRuntime.size();++index)
|
||||
apply(static_cast<std::uint32_t>(index));
|
||||
return composed;
|
||||
}
|
||||
static float SmoothUnit(float V)
|
||||
{
|
||||
V=std::clamp(V,0.0f,1.0f);
|
||||
@@ -8614,6 +8964,10 @@ private:
|
||||
FBuildStats Stats;
|
||||
bool bAnalyticOnly=false;
|
||||
inline static thread_local const FScopedPassageTileQueries* ActivePassageTileQueries=nullptr;
|
||||
std::vector<FStampSpatialBound> StampSpatialBounds;
|
||||
std::vector<FStampSpatialNode> StampSpatialNodes;
|
||||
std::vector<std::uint32_t> StampSpatialOrder;
|
||||
std::vector<FStampRuntime> StampRuntime;
|
||||
std::vector<VoxelForgeCore::WorldAssemblyFeatureBoundsV1> PassageFeatureBounds;
|
||||
std::vector<FPassageTileFeature> PassageTileFeatures;
|
||||
std::vector<std::uint32_t> PassageFeatureOrder;
|
||||
|
||||
@@ -1230,6 +1230,31 @@ namespace VoxelForgeCore
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ResolvedStampTouchesBox(const ResolvedLayoutSnapshotV3& layout,
|
||||
const FIntVector& minimum,const FIntVector& maximum,
|
||||
std::uint32_t coreVoxelSizeMicrons)
|
||||
{
|
||||
if(layout.stamps.empty()||coreVoxelSizeMicrons==0)return false;
|
||||
const double tileMinimum[3]={static_cast<double>(minimum.X),
|
||||
static_cast<double>(minimum.Y),static_cast<double>(minimum.Z)};
|
||||
const double tileMaximum[3]={static_cast<double>(maximum.X),
|
||||
static_cast<double>(maximum.Y),static_cast<double>(maximum.Z)};
|
||||
for(const auto& stamp:layout.stamps)
|
||||
{
|
||||
bool overlap=true;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
const double low=static_cast<double>(stamp.boundsMinimumMicrometres[axis])
|
||||
/coreVoxelSizeMicrons;
|
||||
const double high=static_cast<double>(stamp.boundsMaximumMicrometres[axis])
|
||||
/coreVoxelSizeMicrons;
|
||||
overlap=overlap&&high>=tileMinimum[axis]&&low<=tileMaximum[axis];
|
||||
}
|
||||
if(overlap)return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool World::compile(const ShapeBuildRequest& request,const JobScheduler& scheduler,std::string& error)
|
||||
@@ -1312,6 +1337,8 @@ namespace VoxelForgeCore
|
||||
if(IntersectsBox(feature.minimumSubvoxels,feature.maximumSubvoxels,
|
||||
subMinimum,subMaximum))return false;
|
||||
if(RouteTouchesBox(*selectedPlan,subMinimum,subMaximum))return false;
|
||||
if(ResolvedStampTouchesBox(*impl_->resolvedLayout,boundsMin,boundsMax,
|
||||
impl_->owner->CoreVoxelSizeMicrons))return false;
|
||||
if(ResolvedPassageTouchesBox(*impl_->resolvedLayout,boundsMin,boundsMax,
|
||||
impl_->owner->CoreVoxelSizeMicrons))return false;
|
||||
if(OwnerPassageTouchesBox(*impl_->owner,boundsMin,boundsMax))return false;
|
||||
|
||||
Reference in new issue
Block a user