Exact one-owner slabs (33,813 samples audited, controls fire), floor/seams/determinism green, routes already
satisfy R > S (S up to 7.9 m). But near-ready 8-21% behind 4041d4b and UE 30 m/s near 40.2 -> 31.5%.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
1597 lines
91 KiB
Diff
1597 lines
91 KiB
Diff
diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
|
||
index 95223ef..947f994 100644
|
||
--- a/ARCHITECTURE.md
|
||
+++ b/ARCHITECTURE.md
|
||
@@ -2316,6 +2316,37 @@ spatial-property inventory: legacy chunk distances, owner/Core voxel lengths and
|
||
placement vectors, and other centimetre fields remain in their prior units. The U1 report records
|
||
the five BuildHost asset values, hashes, and remaining work.
|
||
|
||
+### 8.56 Reach-bounded ownership slabs for prepared passages — World Assembler M3 L128
|
||
+
|
||
+The support radius varies along the route as `S(s) = r(s) + route_reach + safety`. On the owner
|
||
+world's 3.5 m body bore, `S = 3.5 + 4.335 + 0.05 = 7.885 m`; at the 4.5 m mouths, `S = 8.885 m`
|
||
+and those joints belong to transition owners. `route_reach = 4.0 m sweep + 0.21 m wall relief +
|
||
+0.125 m quarter-corner allowance`; the 0.75 m floor shell and 1 m open-air side wall fit inside
|
||
+that conservative reach. For a smooth centreline with curvature `κ`, the plane coordinate
|
||
+`g_s(x) = (x - c(s)) · T(s)` is strictly decreasing throughout support when `κS < 1`, or bend
|
||
+radius `ρ > S`. On a circular joint with chord length `L` and turn `θ`, the equivalent bound is
|
||
+`L / (2 sin(θ/2)) > S`. The audit also checks doubly-critical nonlocal route pairs against the
|
||
+sum of their two support radii, since a large local bend radius alone cannot prevent a route from
|
||
+folding back across itself.
|
||
+
|
||
+Each normal body frame owns the half-open region between its start and end tangent planes. The
|
||
+outside planes are omitted at the two mouths; tight mouth fans are combined into transition
|
||
+owners. When routes meet at a joint, their passage fields remain independent contributors to the
|
||
+deterministic union. The same plane predicate filters prepared tile sweeps and exact off-grid
|
||
+queries. Floor height remains independent of round-distance selection: `PassageOwnershipFloorT`
|
||
+projects onto the owner's XY route segment because authored `h(s)` uses horizontal progress. The
|
||
+9-station density probe matches L127's root residuals exactly; across its 0.1 m samples, the
|
||
+largest adjacent root change is 1.765 mm.
|
||
+
|
||
+Planning audits local curvature, nonlocal separation, 33,813 owner-world support probes, and
|
||
+negative controls that inject a duplicate claim and a no-owner gap. The owner world has three
|
||
+routes; tight joints are handled by mouth transitions and the body passes the support bound. A
|
||
+pathological legacy two-strata fixture instead receives one full-route composite owner, keeping
|
||
+its established field while retaining one slab claim. Such fallback routes do not use the body-slab
|
||
+shortcut. The evaluator version is 226; no
|
||
+owner route control points are changed. See
|
||
+[`Standalone/Research/WorldAssemblerM3/Linux128/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux128/REPORT.md).
|
||
+
|
||
## 9. Multiplayer model (listen-server first, dedicated-friendly)
|
||
|
||
> **Status: transport is DESIGN ONLY — nothing is networked in-tree yet** (no `Replicated`/`HasAuthority`/RPCs; a
|
||
diff --git a/CODEMAP.md b/CODEMAP.md
|
||
index 56d375f..a59bfb3 100644
|
||
--- a/CODEMAP.md
|
||
+++ b/CODEMAP.md
|
||
@@ -1352,6 +1352,17 @@ tunnel-slope version in `VoxelStrateDefinition.cpp`, plus the standalone `VoxelF
|
||
in `VoxelForgeExploreCommandlet.cpp`. `GetStrateForChunk` still receives layout-chunk Z, not a Core
|
||
tile index. See `Standalone/Research/WorldAssemblerM3/Unreal1/REPORT.md` for BuildHost evidence.
|
||
|
||
+### 3.38 Prepared passage slab ownership — World Assembler M3 L128
|
||
+
|
||
+| Symbol | Canonical file | Role |
|
||
+|---|---|---|
|
||
+| `FSlabReachAudit`, `AuditSlabReach`, `BuildSegments` | `Standalone/Core/include/VoxelForgeCore/PassageSpline.h` | Audits reach-bounded bend curvature, nonlocal route separation, single slab ownership, and injected duplicate/no-owner controls; chooses finite mouth-transition ranges. |
|
||
+| `FPassageOwnershipSlab`, `PassageOwnershipSlabOwns`, `PassageOwnershipFloorT` | `Standalone/Core/src/ShapeWorldInternal.h` | Shares half-open slab-plane ownership between exact point queries and prepared tile sweeps; floor-profile `t` uses horizontal route progress independently of round distance, with a frame-plane fallback for vertical segments. Pathological legacy paths use one full-route composite owner. |
|
||
+| `PassagePlanSlabAudit` | `Standalone/Core/src/ResolvedLayoutInternal.h` | Fails layout planning when neither the normal slabs nor the validated whole-route composite can provide exactly one owner per support probe. |
|
||
+| `WorldAssemblyEvaluatorVersion` | `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h` | Bumped to 226 for slab ownership. |
|
||
+
|
||
+See ARCHITECTURE §8.56 and [`Standalone/Research/WorldAssemblerM3/Linux128/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux128/REPORT.md).
|
||
+
|
||
## 4. The density pipeline (most-edited hot path)
|
||
|
||
### 4.1 `GetDensityWithParams` (TunnelNetwork) — VoxelGenerator.cpp:3749
|
||
diff --git a/Standalone/Core/include/VoxelForgeCore/PassageSpline.h b/Standalone/Core/include/VoxelForgeCore/PassageSpline.h
|
||
index 6b61bb9..93dbe2c 100644
|
||
--- a/Standalone/Core/include/VoxelForgeCore/PassageSpline.h
|
||
+++ b/Standalone/Core/include/VoxelForgeCore/PassageSpline.h
|
||
@@ -2,10 +2,14 @@
|
||
|
||
#include <algorithm>
|
||
#include <array>
|
||
+#include <bit>
|
||
#include <cmath>
|
||
#include <cstddef>
|
||
+#include <cstdint>
|
||
#include <limits>
|
||
+#include <mutex>
|
||
#include <span>
|
||
+#include <unordered_map>
|
||
#include <vector>
|
||
|
||
namespace VoxelForgeCore::PassageSpline
|
||
@@ -23,6 +27,32 @@ namespace VoxelForgeCore::PassageSpline
|
||
double plannedFloorA=0.0,plannedFloorB=0.0;
|
||
};
|
||
|
||
+ struct FSlabReachAudit
|
||
+ {
|
||
+ std::size_t upperMouthLastControlSpan=0;
|
||
+ std::size_t lowerMouthFirstControlSpan=0;
|
||
+ std::size_t upperMouthEndSegment=0;
|
||
+ std::size_t upperMouthTransitionEndSegment=0;
|
||
+ std::size_t lowerMouthStartSegment=0;
|
||
+ std::size_t lowerMouthTransitionStartSegment=0;
|
||
+ std::size_t upperTransitionLastControlSpan=0;
|
||
+ std::size_t tightMouthJoints=0;
|
||
+ std::size_t nonlocalMouthPairs=0;
|
||
+ std::size_t bodyCurvatureFailures=0;
|
||
+ std::size_t bodySelfOverlapFailures=0;
|
||
+ std::size_t ownershipTestSamples=0;
|
||
+ std::size_t zeroOwnerSamples=0,multipleOwnerSamples=0;
|
||
+ bool ownershipNegativeControlsPass=false;
|
||
+ bool usesWholeRouteCompositeSlab=false;
|
||
+ std::size_t firstZeroOwnerProbe=std::numeric_limits<std::size_t>::max();
|
||
+ std::size_t firstZeroOwnerSegment=std::numeric_limits<std::size_t>::max();
|
||
+ std::array<double,3> firstZeroOwnerPoint{};
|
||
+ double firstZeroOwnerStartSide=0.0,firstZeroOwnerEndSide=0.0;
|
||
+ double minimumBodyCurvatureRadiusMetres=std::numeric_limits<double>::infinity();
|
||
+ double minimumBodySelfGapMetres=std::numeric_limits<double>::infinity();
|
||
+ bool valid=false;
|
||
+ };
|
||
+
|
||
inline double KnotSpan(const FPoint& a,const FPoint& b,double unitsPerMetre)
|
||
{
|
||
const double dx=(b[0]-a[0])/unitsPerMetre;
|
||
@@ -252,4 +282,626 @@ namespace VoxelForgeCore::PassageSpline
|
||
}
|
||
return result;
|
||
}
|
||
+
|
||
+ inline FSlabReachAudit ComputeSlabReachAudit(std::span<const FSegment> segments,
|
||
+ std::size_t routeEndControlIndex,double unitsPerMetre,double routeReachMetres,
|
||
+ double safetyMarginMetres=0.05)
|
||
+ {
|
||
+ FSlabReachAudit result;
|
||
+ if(segments.size()<2||!(unitsPerMetre>0.0)||!(routeReachMetres>=0.0))return result;
|
||
+ const std::size_t lastRouteSpan=std::min(routeEndControlIndex,
|
||
+ segments.back().controlSpan);
|
||
+ result.upperMouthLastControlSpan=0;
|
||
+ result.lowerMouthFirstControlSpan=lastRouteSpan;
|
||
+ struct FStation
|
||
+ {
|
||
+ std::array<double,3> position{};
|
||
+ double radius=0.0,along=0.0;
|
||
+ std::size_t controlSpan=0;
|
||
+ };
|
||
+ std::vector<FStation> stations;
|
||
+ stations.reserve(segments.size()+1);
|
||
+ const auto makeStation=[&](const FPoint& point,double along,std::size_t span)
|
||
+ {
|
||
+ FStation station;
|
||
+ for(std::size_t axis=0;axis<3;++axis)
|
||
+ station.position[axis]=point[axis]/unitsPerMetre;
|
||
+ station.radius=std::max(0.0,point[3]/unitsPerMetre);
|
||
+ station.along=along;
|
||
+ station.controlSpan=span;
|
||
+ return station;
|
||
+ };
|
||
+ stations.push_back(makeStation(segments.front().a,segments.front().alongMetres,
|
||
+ segments.front().controlSpan));
|
||
+ for(const auto& segment:segments)
|
||
+ stations.push_back(makeStation(segment.b,
|
||
+ segment.alongMetres+segment.lengthMetres,segment.controlSpan));
|
||
+ double maximumSupport=0.0,maximumSegmentLength=0.0;
|
||
+ for(const auto& station:stations)
|
||
+ maximumSupport=std::max(maximumSupport,station.radius
|
||
+ +routeReachMetres+safetyMarginMetres);
|
||
+ for(const auto& segment:segments)
|
||
+ maximumSegmentLength=std::max(maximumSegmentLength,segment.lengthMetres);
|
||
+
|
||
+ const auto distance=[](const FStation& a,const FStation& b)
|
||
+ {
|
||
+ double squared=0.0;
|
||
+ for(std::size_t axis=0;axis<3;++axis)
|
||
+ {const double delta=a.position[axis]-b.position[axis];squared+=delta*delta;}
|
||
+ return std::sqrt(squared);
|
||
+ };
|
||
+ const auto curvatureRadius=[&](std::size_t index)
|
||
+ {
|
||
+ const auto& a=stations[index-1];
|
||
+ const auto& b=stations[index];
|
||
+ const auto& c=stations[index+1];
|
||
+ const std::array<double,3> ab={b.position[0]-a.position[0],
|
||
+ b.position[1]-a.position[1],b.position[2]-a.position[2]};
|
||
+ const std::array<double,3> bc={c.position[0]-b.position[0],
|
||
+ c.position[1]-b.position[1],c.position[2]-b.position[2]};
|
||
+ const std::array<double,3> cross={ab[1]*bc[2]-ab[2]*bc[1],
|
||
+ ab[2]*bc[0]-ab[0]*bc[2],ab[0]*bc[1]-ab[1]*bc[0]};
|
||
+ const double twiceArea=std::sqrt(cross[0]*cross[0]+cross[1]*cross[1]
|
||
+ +cross[2]*cross[2]);
|
||
+ return twiceArea>1.0e-12
|
||
+ ?distance(a,b)*distance(b,c)*distance(a,c)/(2.0*twiceArea)
|
||
+ :std::numeric_limits<double>::infinity();
|
||
+ };
|
||
+ const auto normalize=[](std::array<double,3> direction)
|
||
+ {
|
||
+ const double length=std::sqrt(direction[0]*direction[0]
|
||
+ +direction[1]*direction[1]+direction[2]*direction[2]);
|
||
+ if(length>1.0e-12)
|
||
+ for(double& component:direction)component/=length;
|
||
+ return direction;
|
||
+ };
|
||
+ std::vector<std::array<double,3>> frameTangents(stations.size());
|
||
+ std::vector<double> tangentSpacingTolerance(stations.size(),0.0);
|
||
+ for(std::size_t frame=0;frame<stations.size();++frame)
|
||
+ {
|
||
+ std::array<double,3> incoming{},outgoing{},tangent{};
|
||
+ if(frame>0)
|
||
+ for(std::size_t axis=0;axis<3;++axis)
|
||
+ incoming[axis]=stations[frame].position[axis]
|
||
+ -stations[frame-1].position[axis];
|
||
+ if(frame+1<stations.size())
|
||
+ for(std::size_t axis=0;axis<3;++axis)
|
||
+ outgoing[axis]=stations[frame+1].position[axis]
|
||
+ -stations[frame].position[axis];
|
||
+ incoming=normalize(incoming);outgoing=normalize(outgoing);
|
||
+ if(frame==0)tangent=outgoing;
|
||
+ else if(frame+1==stations.size())tangent=incoming;
|
||
+ else
|
||
+ for(std::size_t axis=0;axis<3;++axis)
|
||
+ tangent[axis]=incoming[axis]+outgoing[axis];
|
||
+ frameTangents[frame]=normalize(tangent);
|
||
+ if(frameTangents[frame][0]==0.0&&frameTangents[frame][1]==0.0
|
||
+ &&frameTangents[frame][2]==0.0)
|
||
+ frameTangents[frame]=frame<segments.size()?outgoing:incoming;
|
||
+ if(frame>0&&frame+1<stations.size())
|
||
+ tangentSpacingTolerance[frame]=0.5*std::max(
|
||
+ stations[frame].along-stations[frame-1].along,
|
||
+ stations[frame+1].along-stations[frame].along);
|
||
+ else if(frame+1<stations.size())
|
||
+ tangentSpacingTolerance[frame]=0.5*(stations[frame+1].along
|
||
+ -stations[frame].along);
|
||
+ else if(frame>0)
|
||
+ tangentSpacingTolerance[frame]=0.5*(stations[frame].along
|
||
+ -stations[frame-1].along);
|
||
+ }
|
||
+ const auto isDoublyCritical=[&](std::size_t first,std::size_t second)
|
||
+ {
|
||
+ std::array<double,3> delta{};
|
||
+ for(std::size_t axis=0;axis<3;++axis)
|
||
+ delta[axis]=stations[second].position[axis]
|
||
+ -stations[first].position[axis];
|
||
+ const auto alongTangent=[&](std::size_t frame)
|
||
+ {
|
||
+ return delta[0]*frameTangents[frame][0]
|
||
+ +delta[1]*frameTangents[frame][1]
|
||
+ +delta[2]*frameTangents[frame][2];
|
||
+ };
|
||
+ return std::abs(alongTangent(first))<=tangentSpacingTolerance[first]
|
||
+ &&std::abs(alongTangent(second))<=tangentSpacingTolerance[second];
|
||
+ };
|
||
+ // The signed coordinate to a route frame changes monotonically across a
|
||
+ // normal disk while kappa*S < 1, i.e. local support S is below curvature
|
||
+ // radius. Global uniqueness additionally requires separating doubly
|
||
+ // critical (mutually normal) centerline pairs by their support radii.
|
||
+ // Support S is bore radius plus the 4.335 m sweep/relief/corner reach.
|
||
+ const auto requiredAt=[&](std::size_t index)
|
||
+ {
|
||
+ return std::max({stations[index-1].radius,stations[index].radius,
|
||
+ stations[index+1].radius})+routeReachMetres+safetyMarginMetres;
|
||
+ };
|
||
+ constexpr std::size_t MouthSearchControlSpans=8;
|
||
+ for(std::size_t index=1;index+1<stations.size();++index)
|
||
+ {
|
||
+ const double radius=curvatureRadius(index);
|
||
+ if(radius>=requiredAt(index))continue;
|
||
+ ++result.tightMouthJoints;
|
||
+ const std::size_t span=std::min(stations[index].controlSpan,
|
||
+ routeEndControlIndex);
|
||
+ if(span<=MouthSearchControlSpans)
|
||
+ result.upperMouthLastControlSpan=std::max(
|
||
+ result.upperMouthLastControlSpan,span+1);
|
||
+ else if(span+MouthSearchControlSpans>=routeEndControlIndex)
|
||
+ result.lowerMouthFirstControlSpan=std::min(
|
||
+ result.lowerMouthFirstControlSpan,span>0?span-1:0);
|
||
+ }
|
||
+
|
||
+ for(std::size_t first=0;first<stations.size();++first)
|
||
+ for(std::size_t second=first+2;second<stations.size();++second)
|
||
+ {
|
||
+ const auto& a=stations[first];
|
||
+ const auto& b=stations[second];
|
||
+ const double required=a.radius+b.radius+2.0*(routeReachMetres
|
||
+ +safetyMarginMetres);
|
||
+ if(b.along-a.along<=required)continue;
|
||
+ if(!isDoublyCritical(first,second))continue;
|
||
+ const double gap=distance(a,b)-required;
|
||
+ result.minimumBodySelfGapMetres=std::min(
|
||
+ result.minimumBodySelfGapMetres,gap);
|
||
+ if(gap>=0.0)continue;
|
||
+ const bool upperMouth=a.controlSpan<=MouthSearchControlSpans
|
||
+ &&b.controlSpan<=MouthSearchControlSpans;
|
||
+ const bool lowerMouth=a.controlSpan+MouthSearchControlSpans
|
||
+ >=routeEndControlIndex
|
||
+ &&b.controlSpan+MouthSearchControlSpans>=routeEndControlIndex;
|
||
+ if(upperMouth)
|
||
+ {
|
||
+ result.upperMouthLastControlSpan=std::max(
|
||
+ result.upperMouthLastControlSpan,b.controlSpan+1);
|
||
+ ++result.nonlocalMouthPairs;
|
||
+ }
|
||
+ else if(lowerMouth)
|
||
+ {
|
||
+ result.lowerMouthFirstControlSpan=std::min(
|
||
+ result.lowerMouthFirstControlSpan,a.controlSpan>0
|
||
+ ?a.controlSpan-1:0);
|
||
+ ++result.nonlocalMouthPairs;
|
||
+ }
|
||
+ }
|
||
+
|
||
+ if(result.upperMouthLastControlSpan>=result.lowerMouthFirstControlSpan)
|
||
+ {
|
||
+ result.upperMouthLastControlSpan=lastRouteSpan;
|
||
+ result.lowerMouthFirstControlSpan=lastRouteSpan;
|
||
+ }
|
||
+ for(std::size_t index=1;index+1<stations.size();++index)
|
||
+ {
|
||
+ const std::size_t span=std::min(stations[index].controlSpan,
|
||
+ routeEndControlIndex);
|
||
+ const bool mouthOwned=span<=result.upperMouthLastControlSpan
|
||
+ ||span>=result.lowerMouthFirstControlSpan;
|
||
+ if(mouthOwned)continue;
|
||
+ const double radius=curvatureRadius(index);
|
||
+ result.minimumBodyCurvatureRadiusMetres=std::min(
|
||
+ result.minimumBodyCurvatureRadiusMetres,radius);
|
||
+ if(radius<requiredAt(index))++result.bodyCurvatureFailures;
|
||
+ }
|
||
+ for(std::size_t first=0;first<stations.size();++first)
|
||
+ for(std::size_t second=first+2;second<stations.size();++second)
|
||
+ {
|
||
+ const auto& a=stations[first];
|
||
+ const auto& b=stations[second];
|
||
+ const double required=a.radius+b.radius+2.0*(routeReachMetres
|
||
+ +safetyMarginMetres);
|
||
+ if(b.along-a.along<=required||distance(a,b)>=required)continue;
|
||
+ if(!isDoublyCritical(first,second))continue;
|
||
+ const bool sameUpper=a.controlSpan<=result.upperMouthLastControlSpan
|
||
+ &&b.controlSpan<=result.upperMouthLastControlSpan;
|
||
+ const bool sameLower=a.controlSpan>=result.lowerMouthFirstControlSpan
|
||
+ &&b.controlSpan>=result.lowerMouthFirstControlSpan;
|
||
+ if(!sameUpper&&!sameLower)++result.bodySelfOverlapFailures;
|
||
+ }
|
||
+ struct FTestSlab
|
||
+ {
|
||
+ std::size_t first=0,end=0;
|
||
+ bool hasStartPlane=true,hasEndPlane=true;
|
||
+ };
|
||
+ std::vector<FTestSlab> testSlabs;
|
||
+ const auto rebuildTestSlabs=[&]()
|
||
+ {
|
||
+ result.upperMouthEndSegment=std::min(result.upperMouthEndSegment,
|
||
+ segments.size());
|
||
+ result.lowerMouthStartSegment=segments.size();
|
||
+ for(std::size_t index=0;index<segments.size();++index)
|
||
+ if(segments[index].controlSpan>=result.lowerMouthFirstControlSpan)
|
||
+ {result.lowerMouthStartSegment=index;break;}
|
||
+ result.upperMouthTransitionEndSegment=std::max(
|
||
+ result.upperMouthEndSegment,std::min(segments.size(),
|
||
+ result.upperMouthTransitionEndSegment));
|
||
+ result.lowerMouthTransitionStartSegment=std::min(
|
||
+ result.lowerMouthStartSegment,std::max(
|
||
+ result.upperMouthTransitionEndSegment,
|
||
+ result.lowerMouthTransitionStartSegment));
|
||
+ testSlabs.clear();
|
||
+ if(result.upperMouthEndSegment>=result.lowerMouthStartSegment)
|
||
+ testSlabs.push_back({0,segments.size(),false,false});
|
||
+ else
|
||
+ {
|
||
+ if(result.upperMouthEndSegment>0)
|
||
+ testSlabs.push_back({0,result.upperMouthEndSegment,false,true});
|
||
+ if(result.upperMouthEndSegment<result.upperMouthTransitionEndSegment)
|
||
+ testSlabs.push_back({result.upperMouthEndSegment,
|
||
+ result.upperMouthTransitionEndSegment,true,true});
|
||
+ for(std::size_t segment=result.upperMouthTransitionEndSegment;
|
||
+ segment<result.lowerMouthTransitionStartSegment;++segment)
|
||
+ testSlabs.push_back({segment,segment+1,true,true});
|
||
+ if(result.lowerMouthTransitionStartSegment<result.lowerMouthStartSegment)
|
||
+ testSlabs.push_back({result.lowerMouthTransitionStartSegment,
|
||
+ result.lowerMouthStartSegment,true,true});
|
||
+ if(result.lowerMouthStartSegment<segments.size())
|
||
+ testSlabs.push_back({result.lowerMouthStartSegment,
|
||
+ segments.size(),true,false});
|
||
+ }
|
||
+ };
|
||
+ const auto owns=[&](const FTestSlab& slab,
|
||
+ const std::array<double,3>& point)
|
||
+ {
|
||
+ if(slab.hasStartPlane)
|
||
+ {
|
||
+ const std::size_t frame=slab.first;
|
||
+ double side=0.0;
|
||
+ for(std::size_t axis=0;axis<3;++axis)
|
||
+ side+=(point[axis]-stations[frame].position[axis])
|
||
+ *frameTangents[frame][axis];
|
||
+ if(side<0.0)return false;
|
||
+ }
|
||
+ if(slab.hasEndPlane)
|
||
+ {
|
||
+ const std::size_t frame=slab.end;
|
||
+ double side=0.0;
|
||
+ for(std::size_t axis=0;axis<3;++axis)
|
||
+ side+=(point[axis]-stations[frame].position[axis])
|
||
+ *frameTangents[frame][axis];
|
||
+ if(side>=0.0)return false;
|
||
+ }
|
||
+ return true;
|
||
+ };
|
||
+ const auto planeSide=[&](const std::array<double,3>& point,
|
||
+ std::size_t frame)
|
||
+ {
|
||
+ double side=0.0;
|
||
+ for(std::size_t axis=0;axis<3;++axis)
|
||
+ side+=(point[axis]-stations[frame].position[axis])
|
||
+ *frameTangents[frame][axis];
|
||
+ return side;
|
||
+ };
|
||
+ const auto influences=[&](const FTestSlab& slab,
|
||
+ const std::array<double,3>& point)
|
||
+ {
|
||
+ for(std::size_t segment=slab.first;segment<slab.end;++segment)
|
||
+ {
|
||
+ const auto& a=stations[segment];
|
||
+ const auto& b=stations[segment+1];
|
||
+ const std::array<double,3> delta={b.position[0]-a.position[0],
|
||
+ b.position[1]-a.position[1],b.position[2]-a.position[2]};
|
||
+ const double lengthSq=delta[0]*delta[0]+delta[1]*delta[1]
|
||
+ +delta[2]*delta[2];
|
||
+ const std::array<double,3> fromStart={point[0]-a.position[0],
|
||
+ point[1]-a.position[1],point[2]-a.position[2]};
|
||
+ const double supportAtStart=a.radius+routeReachMetres+safetyMarginMetres;
|
||
+ const double supportDelta=b.radius-a.radius;
|
||
+ const auto supportsPoint=[&](double t)
|
||
+ {
|
||
+ const double dx=fromStart[0]-t*delta[0];
|
||
+ const double dy=fromStart[1]-t*delta[1];
|
||
+ const double dz=fromStart[2]-t*delta[2];
|
||
+ const double support=supportAtStart+t*supportDelta;
|
||
+ return dx*dx+dy*dy+dz*dz<=support*support+1.0e-9;
|
||
+ };
|
||
+ if(supportsPoint(0.0)||supportsPoint(1.0))return true;
|
||
+ const double quadratic=lengthSq-supportDelta*supportDelta;
|
||
+ if(quadratic>1.0e-12)
|
||
+ {
|
||
+ const double linear=-2.0*(fromStart[0]*delta[0]
|
||
+ +fromStart[1]*delta[1]+fromStart[2]*delta[2])
|
||
+ -2.0*supportAtStart*supportDelta;
|
||
+ const double closest=std::clamp(-linear/(2.0*quadratic),0.0,1.0);
|
||
+ if(supportsPoint(closest))return true;
|
||
+ }
|
||
+ }
|
||
+ return false;
|
||
+ };
|
||
+ const auto slabState=[&](const FTestSlab& slab,
|
||
+ const std::array<double,3>& point)
|
||
+ {
|
||
+ const bool active=influences(slab,point);
|
||
+ return std::array<bool,2>{active,active&&owns(slab,point)};
|
||
+ };
|
||
+ const auto testOwnershipCrossSection=[&](const std::array<double,3>& center,
|
||
+ const std::array<double,3>& tangent,double boreRadius,
|
||
+ std::size_t probeIndex)
|
||
+ {
|
||
+ const double probeAlong=probeIndex<stations.size()
|
||
+ ?stations[probeIndex].along
|
||
+ :segments[probeIndex-stations.size()].alongMetres
|
||
+ +0.5*segments[probeIndex-stations.size()].lengthMetres;
|
||
+ // The local curvature test handles nearby frames; the doubly-critical
|
||
+ // pair test handles branches farther away. Only slabs whose route interval
|
||
+ // can meet this cross-section's four-support arc window need point tests.
|
||
+ constexpr double LocalSupportArcFactor=4.0;
|
||
+ const double arcWindow=LocalSupportArcFactor*maximumSupport
|
||
+ +maximumSegmentLength;
|
||
+ const std::array<double,3> reference=std::abs(tangent[2])<0.75
|
||
+ ?std::array<double,3>{0.0,0.0,1.0}
|
||
+ :std::array<double,3>{0.0,1.0,0.0};
|
||
+ std::array<double,3> normalA={tangent[1]*reference[2]-tangent[2]*reference[1],
|
||
+ tangent[2]*reference[0]-tangent[0]*reference[2],
|
||
+ tangent[0]*reference[1]-tangent[1]*reference[0]};
|
||
+ normalA=normalize(normalA);
|
||
+ const std::array<double,3> normalB={tangent[1]*normalA[2]-tangent[2]*normalA[1],
|
||
+ tangent[2]*normalA[0]-tangent[0]*normalA[2],
|
||
+ tangent[0]*normalA[1]-tangent[1]*normalA[0]};
|
||
+ const double support=boreRadius+routeReachMetres+safetyMarginMetres;
|
||
+ for(std::size_t ring=0;ring<3;++ring)
|
||
+ {
|
||
+ const double radius=support*static_cast<double>(ring)*0.5;
|
||
+ const std::size_t directions=ring==0?1:8;
|
||
+ for(std::size_t directionIndex=0;directionIndex<directions;
|
||
+ ++directionIndex)
|
||
+ {
|
||
+ const double angle=2.0*3.14159265358979323846
|
||
+ *static_cast<double>(directionIndex)/directions;
|
||
+ const double offsetA=radius*std::cos(angle);
|
||
+ const double offsetB=radius*std::sin(angle);
|
||
+ std::array<double,3> point=center;
|
||
+ for(std::size_t axis=0;axis<3;++axis)
|
||
+ point[axis]+=offsetA*normalA[axis]+offsetB*normalB[axis];
|
||
+ std::size_t claims=0;
|
||
+ std::size_t unownedSegment=std::numeric_limits<std::size_t>::max();
|
||
+ double unownedStartSide=0.0,unownedEndSide=0.0;
|
||
+ if(testSlabs.size()==1&&!testSlabs.front().hasStartPlane
|
||
+ &&!testSlabs.front().hasEndPlane)
|
||
+ {
|
||
+ // A whole-route composite has no ownership boundaries: the
|
||
+ // sampled point is within support of its center frame/segment,
|
||
+ // so that single unbounded slab owns it by construction.
|
||
+ claims=1;
|
||
+ }
|
||
+ else
|
||
+ {
|
||
+ const double firstAlong=probeAlong-arcWindow;
|
||
+ const double lastAlong=probeAlong+arcWindow;
|
||
+ const auto firstSlab=std::lower_bound(testSlabs.begin(),
|
||
+ testSlabs.end(),firstAlong,[&](const FTestSlab& slab,
|
||
+ double value)
|
||
+ {
|
||
+ return stations[slab.end].along<value;
|
||
+ });
|
||
+ for(auto slab=firstSlab;slab!=testSlabs.end()
|
||
+ &&stations[slab->first].along<=lastAlong;++slab)
|
||
+ {
|
||
+ const auto state=slabState(*slab,point);
|
||
+ if(!state[0])continue;
|
||
+ if(state[1])++claims;
|
||
+ else if(unownedSegment==std::numeric_limits<std::size_t>::max())
|
||
+ {
|
||
+ unownedSegment=slab->first;
|
||
+ if(slab->hasStartPlane)
|
||
+ unownedStartSide=planeSide(point,slab->first);
|
||
+ if(slab->hasEndPlane)
|
||
+ unownedEndSide=planeSide(point,slab->end);
|
||
+ }
|
||
+ }
|
||
+ }
|
||
+ ++result.ownershipTestSamples;
|
||
+ if(claims==0)++result.zeroOwnerSamples;
|
||
+ else if(claims>1)++result.multipleOwnerSamples;
|
||
+ if(claims==0&&result.firstZeroOwnerProbe
|
||
+ ==std::numeric_limits<std::size_t>::max())
|
||
+ {
|
||
+ result.firstZeroOwnerProbe=probeIndex;
|
||
+ result.firstZeroOwnerPoint=point;
|
||
+ result.firstZeroOwnerSegment=unownedSegment;
|
||
+ result.firstZeroOwnerStartSide=unownedStartSide;
|
||
+ result.firstZeroOwnerEndSide=unownedEndSide;
|
||
+ }
|
||
+ }
|
||
+ }
|
||
+ };
|
||
+ const std::size_t initialUpperMouthLastSpan=result.upperMouthLastControlSpan;
|
||
+ const std::size_t initialLowerMouthFirstSpan=result.lowerMouthFirstControlSpan;
|
||
+ result.upperTransitionLastControlSpan=initialUpperMouthLastSpan;
|
||
+ const auto segmentAfterSpan=[&](std::size_t span)
|
||
+ {
|
||
+ std::size_t end=0;
|
||
+ while(end<segments.size()&&segments[end].controlSpan<=span)++end;
|
||
+ return end;
|
||
+ };
|
||
+ const std::size_t naturalUpperMouthEndSegment=segmentAfterSpan(
|
||
+ initialUpperMouthLastSpan);
|
||
+ result.upperMouthTransitionEndSegment=naturalUpperMouthEndSegment;
|
||
+ result.lowerMouthTransitionStartSegment=segments.size();
|
||
+ for(std::size_t segment=0;segment<segments.size();++segment)
|
||
+ if(segments[segment].controlSpan>=initialLowerMouthFirstSpan)
|
||
+ {result.lowerMouthTransitionStartSegment=segment;break;}
|
||
+ for(std::size_t attempt=0;attempt<=naturalUpperMouthEndSegment;++attempt)
|
||
+ {
|
||
+ result.upperMouthEndSegment=naturalUpperMouthEndSegment-attempt;
|
||
+ result.upperMouthLastControlSpan=result.upperMouthEndSegment>0
|
||
+ ?segments[result.upperMouthEndSegment-1].controlSpan:0;
|
||
+ result.lowerMouthFirstControlSpan=initialLowerMouthFirstSpan;
|
||
+ result.ownershipTestSamples=0;
|
||
+ result.zeroOwnerSamples=0;
|
||
+ result.multipleOwnerSamples=0;
|
||
+ result.firstZeroOwnerProbe=std::numeric_limits<std::size_t>::max();
|
||
+ result.firstZeroOwnerSegment=std::numeric_limits<std::size_t>::max();
|
||
+ rebuildTestSlabs();
|
||
+ for(std::size_t frame=0;frame<stations.size();++frame)
|
||
+ testOwnershipCrossSection(stations[frame].position,
|
||
+ frameTangents[frame],stations[frame].radius,frame);
|
||
+ for(std::size_t segment=0;segment<segments.size();++segment)
|
||
+ {
|
||
+ std::array<double,3> center{},tangent{};
|
||
+ for(std::size_t axis=0;axis<3;++axis)
|
||
+ {
|
||
+ center[axis]=(stations[segment].position[axis]
|
||
+ +stations[segment+1].position[axis])*0.5;
|
||
+ tangent[axis]=frameTangents[segment][axis]
|
||
+ +frameTangents[segment+1][axis];
|
||
+ }
|
||
+ tangent=normalize(tangent);
|
||
+ if(tangent[0]==0.0&&tangent[1]==0.0&&tangent[2]==0.0)
|
||
+ tangent=frameTangents[segment];
|
||
+ testOwnershipCrossSection(center,tangent,
|
||
+ 0.5*(stations[segment].radius+stations[segment+1].radius),
|
||
+ stations.size()+segment);
|
||
+ }
|
||
+ if(result.zeroOwnerSamples==0&&result.multipleOwnerSamples==0)break;
|
||
+ }
|
||
+ const std::size_t controlFrame=std::clamp<std::size_t>(
|
||
+ result.upperMouthEndSegment,1,segments.size()-1);
|
||
+ std::array<double,3> negativeControlPoint=stations[controlFrame].position;
|
||
+ for(std::size_t axis=0;axis<3;++axis)
|
||
+ negativeControlPoint[axis]-=0.1*frameTangents[controlFrame][axis];
|
||
+ const std::vector<FTestSlab> duplicateControlSlabs={
|
||
+ {0,controlFrame,false,true},{controlFrame,controlFrame+1,false,true}};
|
||
+ const std::vector<FTestSlab> gapControlSlabs={
|
||
+ {controlFrame,controlFrame+1,true,false}};
|
||
+ const auto ownerCount=[&](const std::vector<FTestSlab>& slabs,
|
||
+ const std::array<double,3>& point)
|
||
+ {
|
||
+ std::size_t claims=0;
|
||
+ for(const auto& slab:slabs)
|
||
+ {
|
||
+ const auto state=slabState(slab,point);
|
||
+ if(state[0]&&state[1])++claims;
|
||
+ }
|
||
+ return claims;
|
||
+ };
|
||
+ result.ownershipNegativeControlsPass=
|
||
+ ownerCount(duplicateControlSlabs,negativeControlPoint)==2
|
||
+ &&ownerCount(gapControlSlabs,negativeControlPoint)==0;
|
||
+ result.bodyCurvatureFailures=0;
|
||
+ result.bodySelfOverlapFailures=0;
|
||
+ result.minimumBodyCurvatureRadiusMetres=
|
||
+ std::numeric_limits<double>::infinity();
|
||
+ for(std::size_t index=1;index+1<stations.size();++index)
|
||
+ {
|
||
+ const std::size_t span=std::min(stations[index].controlSpan,
|
||
+ routeEndControlIndex);
|
||
+ if(span<=result.upperTransitionLastControlSpan
|
||
+ ||span>=result.lowerMouthFirstControlSpan)continue;
|
||
+ const double radius=curvatureRadius(index);
|
||
+ result.minimumBodyCurvatureRadiusMetres=std::min(
|
||
+ result.minimumBodyCurvatureRadiusMetres,radius);
|
||
+ if(radius<requiredAt(index))++result.bodyCurvatureFailures;
|
||
+ }
|
||
+ for(std::size_t first=0;first<stations.size();++first)
|
||
+ for(std::size_t second=first+2;second<stations.size();++second)
|
||
+ {
|
||
+ const auto& a=stations[first];
|
||
+ const auto& b=stations[second];
|
||
+ const double required=a.radius+b.radius+2.0*(routeReachMetres
|
||
+ +safetyMarginMetres);
|
||
+ if(b.along-a.along<=required||distance(a,b)>=required
|
||
+ ||!isDoublyCritical(first,second))continue;
|
||
+ const bool sameUpper=a.controlSpan<=result.upperTransitionLastControlSpan
|
||
+ &&b.controlSpan<=result.upperTransitionLastControlSpan;
|
||
+ const bool sameLower=a.controlSpan>=result.lowerMouthFirstControlSpan
|
||
+ &&b.controlSpan>=result.lowerMouthFirstControlSpan;
|
||
+ if(!sameUpper&&!sameLower)++result.bodySelfOverlapFailures;
|
||
+ }
|
||
+ result.usesWholeRouteCompositeSlab=result.bodyCurvatureFailures>0
|
||
+ ||result.bodySelfOverlapFailures>0||result.zeroOwnerSamples>0
|
||
+ ||result.multipleOwnerSamples>0;
|
||
+ if(result.usesWholeRouteCompositeSlab)
|
||
+ {
|
||
+ // Pathological legacy routes can fold back across themselves. Keep their
|
||
+ // established union field by assigning the complete route to one owner;
|
||
+ // the normal path retains one slab per safe body segment.
|
||
+ testSlabs.assign(1,FTestSlab{0,segments.size(),false,false});
|
||
+ result.ownershipTestSamples=0;
|
||
+ result.zeroOwnerSamples=0;
|
||
+ result.multipleOwnerSamples=0;
|
||
+ result.firstZeroOwnerProbe=std::numeric_limits<std::size_t>::max();
|
||
+ result.firstZeroOwnerSegment=std::numeric_limits<std::size_t>::max();
|
||
+ for(std::size_t frame=0;frame<stations.size();++frame)
|
||
+ testOwnershipCrossSection(stations[frame].position,
|
||
+ frameTangents[frame],stations[frame].radius,frame);
|
||
+ for(std::size_t segment=0;segment<segments.size();++segment)
|
||
+ {
|
||
+ std::array<double,3> center{},tangent{};
|
||
+ for(std::size_t axis=0;axis<3;++axis)
|
||
+ {
|
||
+ center[axis]=(stations[segment].position[axis]
|
||
+ +stations[segment+1].position[axis])*0.5;
|
||
+ tangent[axis]=frameTangents[segment][axis]
|
||
+ +frameTangents[segment+1][axis];
|
||
+ }
|
||
+ tangent=normalize(tangent);
|
||
+ if(tangent[0]==0.0&&tangent[1]==0.0&&tangent[2]==0.0)
|
||
+ tangent=frameTangents[segment];
|
||
+ testOwnershipCrossSection(center,tangent,
|
||
+ 0.5*(stations[segment].radius+stations[segment+1].radius),
|
||
+ stations.size()+segment);
|
||
+ }
|
||
+ }
|
||
+ if(result.upperMouthEndSegment>=result.lowerMouthStartSegment)
|
||
+ result.usesWholeRouteCompositeSlab=true;
|
||
+ result.valid=(result.usesWholeRouteCompositeSlab
|
||
+ ||(result.bodyCurvatureFailures==0
|
||
+ &&result.bodySelfOverlapFailures==0))
|
||
+ &&result.zeroOwnerSamples==0&&result.multipleOwnerSamples==0
|
||
+ &&result.ownershipNegativeControlsPass;
|
||
+ return result;
|
||
+ }
|
||
+
|
||
+ struct FSlabAuditKeyHash
|
||
+ {
|
||
+ std::size_t operator()(const std::vector<std::uint64_t>& key) const noexcept
|
||
+ {
|
||
+ std::uint64_t hash=1469598103934665603ULL;
|
||
+ for(const std::uint64_t value:key)
|
||
+ {
|
||
+ hash^=value;
|
||
+ hash*=1099511628211ULL;
|
||
+ }
|
||
+ return static_cast<std::size_t>(hash);
|
||
+ }
|
||
+ };
|
||
+
|
||
+ inline FSlabReachAudit AuditSlabReach(std::span<const FSegment> segments,
|
||
+ std::size_t routeEndControlIndex,double unitsPerMetre,double routeReachMetres,
|
||
+ double safetyMarginMetres=0.05)
|
||
+ {
|
||
+ using FCache=std::unordered_map<std::vector<std::uint64_t>,
|
||
+ FSlabReachAudit,FSlabAuditKeyHash>;
|
||
+ static FCache cache;
|
||
+ static std::mutex cacheMutex;
|
||
+ std::vector<std::uint64_t> key;
|
||
+ key.reserve(4+segments.size()*11);
|
||
+ key.push_back(static_cast<std::uint64_t>(routeEndControlIndex));
|
||
+ key.push_back(std::bit_cast<std::uint64_t>(unitsPerMetre));
|
||
+ key.push_back(std::bit_cast<std::uint64_t>(routeReachMetres));
|
||
+ key.push_back(std::bit_cast<std::uint64_t>(safetyMarginMetres));
|
||
+ for(const auto& segment:segments)
|
||
+ {
|
||
+ for(const double value:segment.a)
|
||
+ key.push_back(std::bit_cast<std::uint64_t>(value));
|
||
+ for(const double value:segment.b)
|
||
+ key.push_back(std::bit_cast<std::uint64_t>(value));
|
||
+ key.push_back(std::bit_cast<std::uint64_t>(segment.alongMetres));
|
||
+ key.push_back(std::bit_cast<std::uint64_t>(segment.lengthMetres));
|
||
+ key.push_back(static_cast<std::uint64_t>(segment.controlSpan));
|
||
+ }
|
||
+ {
|
||
+ std::lock_guard<std::mutex> lock(cacheMutex);
|
||
+ if(const auto found=cache.find(key);found!=cache.end())return found->second;
|
||
+ }
|
||
+
|
||
+ const FSlabReachAudit result=ComputeSlabReachAudit(segments,
|
||
+ routeEndControlIndex,unitsPerMetre,routeReachMetres,safetyMarginMetres);
|
||
+ {
|
||
+ std::lock_guard<std::mutex> lock(cacheMutex);
|
||
+ constexpr std::size_t MaximumCachedRoutes=32;
|
||
+ if(cache.size()>=MaximumCachedRoutes)cache.clear();
|
||
+ const auto [found,inserted]=cache.emplace(std::move(key),result);
|
||
+ if(!inserted)return found->second;
|
||
+ }
|
||
+ return result;
|
||
+ }
|
||
}
|
||
diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h
|
||
index dc4ca32..bdc3910 100644
|
||
--- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h
|
||
+++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h
|
||
@@ -494,7 +494,8 @@ namespace VoxelForgeCore
|
||
// Version 223 projects the floor onto one continuous arc-length profile and groups
|
||
// dense passage samples into coarse tile candidates. Version 224 keeps the same
|
||
// continuous floor while relaxing the spline chord tolerance and bounding shell work.
|
||
- inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 225;
|
||
+ // Version 226 assigns route samples to reach-bounded slabs, with explicit mouth ownership.
|
||
+ inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 226;
|
||
|
||
enum class FieldChannel : std::uint32_t
|
||
{
|
||
diff --git a/Standalone/Core/src/ResolvedLayoutInternal.h b/Standalone/Core/src/ResolvedLayoutInternal.h
|
||
index 5fcb719..fc570c7 100644
|
||
--- a/Standalone/Core/src/ResolvedLayoutInternal.h
|
||
+++ b/Standalone/Core/src/ResolvedLayoutInternal.h
|
||
@@ -900,19 +900,22 @@ bool ResolvedLayoutSnapshotV3::finalize(std::string& error)
|
||
}
|
||
const std::int64_t playerCenterAboveFloorQ=static_cast<std::int64_t>(std::llround(
|
||
1.13*WorldAssemblySubvoxelsPerVoxel*1000000.0/coreVoxelSizeMicrometres));
|
||
- for(auto& passage:passages)
|
||
+ const auto lowerMouthIndexFor=[&](const ResolvedLayoutPassageV1& passage)
|
||
{
|
||
- passage.lowerMouthControlIndex=passage.controlPoints.empty()?0:passage.controlPoints.size()-1;
|
||
- for(std::size_t i=1;i<passage.controlPoints.size();++i)
|
||
+ std::size_t index=passage.controlPoints.empty()?0:passage.controlPoints.size()-1;
|
||
+ for(std::size_t pointIndex=1;pointIndex<passage.controlPoints.size();++pointIndex)
|
||
{
|
||
- const auto& point=passage.controlPoints[i];
|
||
+ const auto& point=passage.controlPoints[pointIndex];
|
||
if(point.positionSubvoxels[0]==passage.lowerMouthSubvoxels[0]
|
||
&&point.positionSubvoxels[1]==passage.lowerMouthSubvoxels[1]
|
||
&&point.positionSubvoxels[2]-point.radiusSubvoxels+playerCenterAboveFloorQ
|
||
==passage.lowerMouthSubvoxels[2])
|
||
- passage.lowerMouthControlIndex=i;
|
||
+ index=pointIndex;
|
||
}
|
||
- }
|
||
+ return index;
|
||
+ };
|
||
+ for(auto& passage:passages)
|
||
+ passage.lowerMouthControlIndex=lowerMouthIndexFor(passage);
|
||
stableIdentity.clear();const std::string body=LayoutBodyJson(*this);
|
||
stableIdentity="resolved-layout-v3-fnv64:"+LayoutHex(LayoutHash(body));
|
||
std::vector<ResolvedLayoutIssueV2> issues;
|
||
@@ -3239,6 +3242,34 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||
passage.controlPoints[lowerMouthIndex].positionSubvoxels[2]=lowerMouth[2]
|
||
+passage.controlPoints[lowerMouthIndex].radiusSubvoxels
|
||
-playerCenterAboveFloorQ;
|
||
+ std::vector<VoxelForgeCore::PassageSpline::FPoint> slabControls;
|
||
+ slabControls.reserve(passage.controlPoints.size());
|
||
+ for(const auto& point:passage.controlPoints)
|
||
+ slabControls.push_back({static_cast<double>(point.positionSubvoxels[0]),
|
||
+ static_cast<double>(point.positionSubvoxels[1]),
|
||
+ static_cast<double>(point.positionSubvoxels[2]),
|
||
+ static_cast<double>(point.radiusSubvoxels)});
|
||
+ constexpr double PassageSlabReachMetres=4.0+0.21+0.125;
|
||
+ const auto slabSegments=VoxelForgeCore::PassageSpline::BuildSegments(
|
||
+ slabControls,slabControls.size()-1,8.0,qPerMetre,{},0.100);
|
||
+ const auto slabAudit=VoxelForgeCore::PassageSpline::AuditSlabReach(
|
||
+ slabSegments,lowerMouthIndex,qPerMetre,PassageSlabReachMetres);
|
||
+ std::printf("[PassagePlanSlabAudit] id=%s frames=%zu reach_m=%.3f upper_mouth_spans=%zu lower_mouth_spans=%zu upper_end_segment=%zu lower_start_segment=%zu tight_mouth_joints=%zu nonlocal_mouth_pairs=%zu body_min_curvature_m=%.3f body_min_self_gap_m=%.3f body_curvature_failures=%zu body_self_overlap_failures=%zu ownership_test_samples=%zu zero_owners=%zu multiple_owners=%zu ownership_mode=%s negative_controls=%s status=%s\n",
|
||
+ passage.stableId.c_str(),slabSegments.size()+1,PassageSlabReachMetres,
|
||
+ slabAudit.upperMouthLastControlSpan+1,
|
||
+ slabAudit.lowerMouthFirstControlSpan<=lowerMouthIndex
|
||
+ ?lowerMouthIndex-slabAudit.lowerMouthFirstControlSpan+1:0,
|
||
+ slabAudit.upperMouthEndSegment,slabAudit.lowerMouthStartSegment,
|
||
+ slabAudit.tightMouthJoints,slabAudit.nonlocalMouthPairs,
|
||
+ slabAudit.minimumBodyCurvatureRadiusMetres,
|
||
+ slabAudit.minimumBodySelfGapMetres,slabAudit.bodyCurvatureFailures,
|
||
+ slabAudit.bodySelfOverlapFailures,slabAudit.ownershipTestSamples,
|
||
+ slabAudit.zeroOwnerSamples,slabAudit.multipleOwnerSamples,
|
||
+ slabAudit.usesWholeRouteCompositeSlab?"whole-route-composite":"per-segment",
|
||
+ slabAudit.ownershipNegativeControlsPass?"PASS":"FAIL",
|
||
+ slabAudit.valid?"PASS":"FAIL");
|
||
+ if(!slabAudit.valid)
|
||
+ {error=passage.stableId+" route cannot form unique reach-bounded ownership slabs";return false;}
|
||
layout.passages.push_back(std::move(passage));
|
||
}
|
||
}
|
||
@@ -3268,9 +3299,10 @@ ResolvedLayoutValidatorAuditV3 AuditResolvedLayoutValidatorV3()
|
||
auto base=ResolvedLayoutSnapshotV3{};base.worldIdentity="synthetic";base.ownerVoxelSizeMicrometres=125000;
|
||
base.coreVoxelSizeMicrometres=250000;base.strates={make("a",0,10000000,0),make("b",1,-2000000,-12000000)};
|
||
ResolvedLayoutPassageConfigV1 passageConfig;passageConfig.upperStrateIndex=0;passageConfig.connections=1;
|
||
- passageConfig.mouthRadiusSubvoxels=1280;passageConfig.midRadiusSubvoxels=1280;
|
||
+ passageConfig.style=0;passageConfig.mouthRadiusSubvoxels=1280;
|
||
+ passageConfig.midRadiusSubvoxels=1280;
|
||
passageConfig.reachMinimumSubvoxels=2048;passageConfig.reachMaximumSubvoxels=2048;
|
||
- passageConfig.distanceMinimumSubvoxels=1024;passageConfig.distanceMaximumSubvoxels=10240;
|
||
+ passageConfig.distanceMinimumSubvoxels=1024;passageConfig.distanceMaximumSubvoxels=64*1024;
|
||
std::string planError;
|
||
if(!PlanResolvedLayoutPassagesV1(base,19,{passageConfig},planError))
|
||
result.failures.push_back({"synthetic_passage_plan_failed",planError});
|
||
diff --git a/Standalone/Core/src/ShapeWorldInternal.h b/Standalone/Core/src/ShapeWorldInternal.h
|
||
index 517f375..229f57d 100644
|
||
--- a/Standalone/Core/src/ShapeWorldInternal.h
|
||
+++ b/Standalone/Core/src/ShapeWorldInternal.h
|
||
@@ -546,13 +546,13 @@ public:
|
||
return it==ResolvedLayout->strates.end()?nullptr:&*it;
|
||
}
|
||
|
||
- void BindResolvedStratumFields()
|
||
+ bool BindResolvedStratumFields(std::string& Error)
|
||
{
|
||
BoundStratumFields.clear();
|
||
BoundManagerLayoutChunkWidthVoxels=Owner&&Owner->Manager
|
||
?Owner->Manager->GetStrateLayoutChunkWidthVoxels():0.0f;
|
||
if(!ResolvedLayout||ResolvedLayout->strateLayoutChunkSizeMicrometres<=0)
|
||
- {BuildPassageComposerConstants();return;}
|
||
+ return BuildPassageComposerConstants(Error);
|
||
const int64 ChunkWidthMicrometres=ResolvedLayout->strateLayoutChunkSizeMicrometres;
|
||
const auto FloorDiv64=[](int64 Value,int64 Divisor)
|
||
{
|
||
@@ -637,7 +637,7 @@ public:
|
||
std::sort(BoundStratumFields.begin(),BoundStratumFields.end(),
|
||
[](const auto& A,const auto& B){return A.BottomChunkZ<B.BottomChunkZ;});
|
||
BuildOreVeinSpatialIndex();
|
||
- BuildPassageComposerConstants();
|
||
+ return BuildPassageComposerConstants(Error);
|
||
}
|
||
|
||
const FBoundStratumField* FindBoundStratumForChunk(int32 ChunkZ) const
|
||
@@ -1212,6 +1212,7 @@ public:
|
||
std::max<std::uint32_t>(1,ResolvedLayout->coreVoxelSizeMicrometres);
|
||
const double qx=static_cast<double>(X)*Q,qy=static_cast<double>(Y)*Q,
|
||
qz=static_cast<double>(Z)*Q;
|
||
+ const std::array<double,3> passageQueryPoint={qx,qy,qz};
|
||
const double shellThickness=FloorShellThicknessMetres*qPerMetre;
|
||
const auto* passageTileQueries=ActivePassageTileQueries;
|
||
const bool passageTileScopeMatches=passageTileQueries
|
||
@@ -1289,8 +1290,13 @@ public:
|
||
double bestFloorCenterDistanceSq=std::numeric_limits<double>::infinity();
|
||
const auto visitSegment=[&](std::size_t segmentIndex,int)
|
||
{
|
||
- if(passageProfile)++passageProfile->passageCarveSegmentVisits;
|
||
const auto& segment=PassageSegmentData[passageIndex][segmentIndex];
|
||
+ const auto& ownershipSlabs=PassageOwnershipSlabs[passageIndex];
|
||
+ if(segment.ownershipSlabIndex>=ownershipSlabs.size()
|
||
+ ||!PassageOwnershipSlabOwns(
|
||
+ ownershipSlabs[segment.ownershipSlabIndex],passageQueryPoint))
|
||
+ return;
|
||
+ if(passageProfile)++passageProfile->passageCarveSegmentVisits;
|
||
if(segment.lengthSq<=0.0)return;
|
||
const double projection=(qx-segment.a[0])*segment.delta[0]
|
||
+(qy-segment.a[1])*segment.delta[1]
|
||
@@ -1303,10 +1309,7 @@ public:
|
||
const double ex=qx-cx,ey=qy-cy,ez=qz-cz;
|
||
const double roundDistance=DeterministicSqrt(
|
||
ex*ex+ey*ey+ez*ez)-radius;
|
||
- const double floorT=segment.horizontalLengthSq>1.0e-12
|
||
- ?(((qx-segment.a[0])*segment.delta[0]
|
||
- +(qy-segment.a[1])*segment.delta[1])
|
||
- /segment.horizontalLengthSq):t;
|
||
+ const double floorT=PassageOwnershipFloorT(segment,passageQueryPoint);
|
||
const double floorTClamped=std::clamp(floorT,0.0,1.0);
|
||
const double floorCenterDistanceSq=ex*ex+ey*ey+ez*ez;
|
||
const double floorRadius=segment.radiusA
|
||
@@ -1363,6 +1366,11 @@ public:
|
||
const auto visitShellSegment=[&](std::size_t segmentIndex,int)
|
||
{
|
||
const auto& segment=PassageSegmentData[passageIndex][segmentIndex];
|
||
+ const auto& ownershipSlabs=PassageOwnershipSlabs[passageIndex];
|
||
+ if(segment.ownershipSlabIndex>=ownershipSlabs.size()
|
||
+ ||!PassageOwnershipSlabOwns(
|
||
+ ownershipSlabs[segment.ownershipSlabIndex],passageQueryPoint))
|
||
+ return;
|
||
if(segment.lengthSq<=0.0)return;
|
||
const double projection=(qx-segment.a[0])*segment.delta[0]
|
||
+(qy-segment.a[1])*segment.delta[1]
|
||
@@ -1374,10 +1382,7 @@ public:
|
||
const double radius=segment.radiusA+t*segment.radiusDelta;
|
||
const double ex=qx-cx,ey=qy-cy,ez=qz-cz;
|
||
const double roundDistance=DeterministicSqrt(ex*ex+ey*ey+ez*ez)-radius;
|
||
- const double floorT=segment.horizontalLengthSq>1.0e-12
|
||
- ?(((qx-segment.a[0])*segment.delta[0]
|
||
- +(qy-segment.a[1])*segment.delta[1])
|
||
- /segment.horizontalLengthSq):t;
|
||
+ const double floorT=PassageOwnershipFloorT(segment,passageQueryPoint);
|
||
const double floorTClamped=std::clamp(floorT,0.0,1.0);
|
||
const auto& segments=PassageSegmentData[passageIndex];
|
||
const double floorRadius=segment.radiusA
|
||
@@ -2235,7 +2240,7 @@ public:
|
||
Scheduler=InScheduler;RoundTunnelsEnabled=bInRoundTunnels;
|
||
bUseNativeManifestFields=true;
|
||
UnbinnedSpatialPrimitiveIds.clear();
|
||
- BindResolvedStratumFields();
|
||
+ if(!BindResolvedStratumFields(Error))return false;
|
||
bAnalyticOnly=false;
|
||
bProfileDetailBand=bInProfileDetailBand;
|
||
bParallelSignEdgeRepair=bInParallelSignEdgeRepair;
|
||
@@ -2441,7 +2446,7 @@ public:
|
||
// Analytic pages retain their bounded owner bins for route bookkeeping. Once published,
|
||
// the Core world binds each page to its base shape as the single density evaluator.
|
||
bUseNativeManifestFields=true;
|
||
- BindResolvedStratumFields();
|
||
+ if(!BindResolvedStratumFields(Error))return false;
|
||
RoundTunnelsEnabled=bInRoundTunnels;bAnalyticOnly=true;bProfileDetailBand=false;
|
||
for(int32 A=0;A<3;++A)
|
||
{
|
||
@@ -2478,8 +2483,11 @@ public:
|
||
{
|
||
Owner=&World;WorldAssembly=InWorldAssembly;ResolvedLayout=InResolvedLayout;
|
||
NativeOnlyMinimum=InMinimum;NativeOnlyMaximum=InMaximum;
|
||
- bUseNativeManifestFields=true;bNativeOnly=true;bAnalyticOnly=true;Built=true;
|
||
- BindResolvedStratumFields();
|
||
+ bUseNativeManifestFields=true;bNativeOnly=true;bAnalyticOnly=true;
|
||
+ std::string ownershipError;
|
||
+ if(!BindResolvedStratumFields(ownershipError))
|
||
+ {bNativeOnly=false;bAnalyticOnly=false;Built=false;return;}
|
||
+ Built=true;
|
||
NativeOnlyViolations.store(0,std::memory_order_relaxed);
|
||
}
|
||
|
||
@@ -3422,6 +3430,9 @@ private:
|
||
double floorDepthMetres=0.0,totalPathLengthMetres=0.0;
|
||
double authoredTotalPathLengthMetres=0.0;
|
||
std::size_t routeProfileSegmentCount=0;
|
||
+ std::size_t upperMouthEndSegment=0,upperMouthTransitionEndSegment=0;
|
||
+ std::size_t lowerMouthStartSegment=0,lowerMouthTransitionStartSegment=0;
|
||
+ bool wholeRouteCompositeSlab=false;
|
||
double upperMouthX=0.0,upperMouthY=0.0;
|
||
double upperMouthPlannedFloor=0.0;
|
||
double upperMouthTangentX=0.0,upperMouthTangentY=0.0;
|
||
@@ -3431,14 +3442,114 @@ private:
|
||
struct FPassageSegmentData
|
||
{
|
||
std::array<double,3> a{},b{},delta{};
|
||
+ std::array<double,3> frameStartTangent{},frameEndTangent{};
|
||
std::array<std::int64_t,3> sweepMinimumSubvoxels{},sweepMaximumSubvoxels{};
|
||
double authoredFloorZA=0.0,authoredFloorZDelta=0.0;
|
||
double authoredAlongMetres=0.0,authoredLengthMetres=0.0;
|
||
double radiusA=0.0,radiusB=0.0,radiusDelta=0.0;
|
||
double lengthSq=0.0,horizontalLengthSq=0.0;
|
||
double lengthSubvoxels=0.0,horizontalLength=0.0;
|
||
+ std::size_t controlSpan=0,ownershipSlabIndex=0;
|
||
bool routeSegment=false;
|
||
};
|
||
+ struct FPassageOwnershipSlab
|
||
+ {
|
||
+ std::array<double,3> startPosition{},endPosition{};
|
||
+ std::array<double,3> startTangent{},endTangent{};
|
||
+ std::size_t firstSegment=0,endSegment=0;
|
||
+ bool hasStartPlane=true,hasEndPlane=true;
|
||
+ };
|
||
+ static double PassageOwnershipPlaneSide(
|
||
+ const std::array<double,3>& point,const std::array<double,3>& origin,
|
||
+ const std::array<double,3>& tangent)
|
||
+ {
|
||
+ return (point[0]-origin[0])*tangent[0]
|
||
+ +(point[1]-origin[1])*tangent[1]
|
||
+ +(point[2]-origin[2])*tangent[2];
|
||
+ }
|
||
+ static bool PassageOwnershipSlabOwns(const FPassageOwnershipSlab& slab,
|
||
+ const std::array<double,3>& point)
|
||
+ {
|
||
+ return (!slab.hasStartPlane
|
||
+ ||PassageOwnershipPlaneSide(point,slab.startPosition,
|
||
+ slab.startTangent)>=0.0)
|
||
+ &&(!slab.hasEndPlane
|
||
+ ||PassageOwnershipPlaneSide(point,slab.endPosition,
|
||
+ slab.endTangent)<0.0);
|
||
+ }
|
||
+ static bool PassageClipBoundsToOwnershipPlane(std::array<double,3>& minimum,
|
||
+ std::array<double,3>& maximum,const std::array<double,3>& origin,
|
||
+ const std::array<double,3>& tangent,bool keepPositive)
|
||
+ {
|
||
+ std::array<std::array<double,3>,8> corners{};
|
||
+ std::array<double,8> sides{};
|
||
+ for(std::size_t mask=0;mask<corners.size();++mask)
|
||
+ {
|
||
+ for(std::size_t axis=0;axis<3;++axis)
|
||
+ corners[mask][axis]=(mask&(std::size_t{1}<<axis))
|
||
+ ?maximum[axis]:minimum[axis];
|
||
+ sides[mask]=PassageOwnershipPlaneSide(corners[mask],origin,tangent);
|
||
+ }
|
||
+ std::array<double,3> clippedMinimum{
|
||
+ std::numeric_limits<double>::infinity(),
|
||
+ std::numeric_limits<double>::infinity(),
|
||
+ std::numeric_limits<double>::infinity()};
|
||
+ std::array<double,3> clippedMaximum{
|
||
+ -std::numeric_limits<double>::infinity(),
|
||
+ -std::numeric_limits<double>::infinity(),
|
||
+ -std::numeric_limits<double>::infinity()};
|
||
+ bool hasPoint=false;
|
||
+ const auto include=[&](const std::array<double,3>& point)
|
||
+ {
|
||
+ hasPoint=true;
|
||
+ for(std::size_t axis=0;axis<3;++axis)
|
||
+ {
|
||
+ clippedMinimum[axis]=std::min(clippedMinimum[axis],point[axis]);
|
||
+ clippedMaximum[axis]=std::max(clippedMaximum[axis],point[axis]);
|
||
+ }
|
||
+ };
|
||
+ const auto inside=[&](double side)
|
||
+ {return keepPositive?side>=0.0:side<=0.0;};
|
||
+ for(std::size_t corner=0;corner<corners.size();++corner)
|
||
+ if(inside(sides[corner]))include(corners[corner]);
|
||
+ for(std::size_t axis=0;axis<3;++axis)
|
||
+ for(std::size_t first=0;first<corners.size();++first)
|
||
+ {
|
||
+ if(first&(std::size_t{1}<<axis))continue;
|
||
+ const std::size_t second=first|(std::size_t{1}<<axis);
|
||
+ const double firstSide=sides[first],secondSide=sides[second];
|
||
+ if(!((firstSide<0.0&&secondSide>0.0)
|
||
+ ||(firstSide>0.0&&secondSide<0.0)))continue;
|
||
+ const double t=firstSide/(firstSide-secondSide);
|
||
+ std::array<double,3> intersection{};
|
||
+ for(std::size_t component=0;component<3;++component)
|
||
+ intersection[component]=std::lerp(corners[first][component],
|
||
+ corners[second][component],t);
|
||
+ include(intersection);
|
||
+ }
|
||
+ if(!hasPoint)return false;
|
||
+ minimum=clippedMinimum;
|
||
+ maximum=clippedMaximum;
|
||
+ return true;
|
||
+ }
|
||
+ static double PassageOwnershipFloorT(const FPassageSegmentData& segment,
|
||
+ const std::array<double,3>& point)
|
||
+ {
|
||
+ // Route floors are authored against horizontal distance along the path.
|
||
+ // Ownership remains a 3D plane decision; use XY projection here so a
|
||
+ // downhill floor point's vertical offset cannot move h(s) along the route.
|
||
+ if(segment.horizontalLengthSq>1.0e-12)
|
||
+ return ((point[0]-segment.a[0])*segment.delta[0]
|
||
+ +(point[1]-segment.a[1])*segment.delta[1])
|
||
+ /segment.horizontalLengthSq;
|
||
+ const double fromStart=PassageOwnershipPlaneSide(point,segment.a,
|
||
+ segment.frameStartTangent);
|
||
+ const double fromEnd=PassageOwnershipPlaneSide(point,segment.b,
|
||
+ segment.frameEndTangent);
|
||
+ const double denominator=fromStart-fromEnd;
|
||
+ if(std::abs(denominator)<=1.0e-12)return 0.0;
|
||
+ return fromStart/denominator;
|
||
+ }
|
||
static double PassageFlatFloorDepthSubvoxels(
|
||
const FPassageComposerConstants& constants,std::size_t segmentIndex,double t,
|
||
double radiusSubvoxels,double qPerMetre)
|
||
@@ -4587,13 +4698,14 @@ private:
|
||
return result;
|
||
}
|
||
|
||
- void BuildPassageComposerConstants()
|
||
+ bool BuildPassageComposerConstants(std::string& Error)
|
||
{
|
||
const bool tracePreparation=VF_IsL98PassagePrepareTraceEnabled();
|
||
const auto preparationStart=tracePreparation
|
||
?std::chrono::steady_clock::now():std::chrono::steady_clock::time_point{};
|
||
PassageComposerConstants.clear();
|
||
PassageSegmentData.clear();
|
||
+ PassageOwnershipSlabs.clear();
|
||
PassageSegmentOffsets.clear();
|
||
if(!ResolvedLayout)
|
||
{
|
||
@@ -4601,10 +4713,11 @@ private:
|
||
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;
|
||
+ return true;
|
||
}
|
||
PassageComposerConstants.resize(ResolvedLayout->passages.size());
|
||
PassageSegmentData.resize(ResolvedLayout->passages.size());
|
||
+ PassageOwnershipSlabs.resize(ResolvedLayout->passages.size());
|
||
PassageSegmentOffsets.resize(ResolvedLayout->passages.size()+1,0);
|
||
constexpr double Q=static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
|
||
const double qPerMetre=Q*1000000.0/
|
||
@@ -4670,6 +4783,39 @@ private:
|
||
const auto sampledSegments=VoxelForgeCore::PassageSpline::BuildSegments(
|
||
controls,controls.size()-1,PassageSegmentSpacingMetres,qPerMetre,
|
||
plannedFloorProfile,PassageSegmentChordErrorMetres);
|
||
+ constexpr double PassageSlabReachMetres=4.0+0.21+0.125;
|
||
+ const auto slabAudit=VoxelForgeCore::PassageSpline::AuditSlabReach(
|
||
+ sampledSegments,routeEnd,qPerMetre,PassageSlabReachMetres);
|
||
+ constants.upperMouthEndSegment=slabAudit.upperMouthEndSegment;
|
||
+ constants.upperMouthTransitionEndSegment=
|
||
+ slabAudit.upperMouthTransitionEndSegment;
|
||
+ constants.lowerMouthStartSegment=slabAudit.lowerMouthStartSegment;
|
||
+ constants.lowerMouthTransitionStartSegment=
|
||
+ slabAudit.lowerMouthTransitionStartSegment;
|
||
+ constants.wholeRouteCompositeSlab=slabAudit.usesWholeRouteCompositeSlab;
|
||
+ std::printf("[PassagePreparedSlabAudit] id=%s frames=%zu reach_m=%.3f upper_end_segment=%zu upper_transition_end_segment=%zu lower_transition_start_segment=%zu lower_start_segment=%zu tight_mouth_joints=%zu nonlocal_mouth_pairs=%zu body_min_curvature_m=%.3f body_min_self_gap_m=%.3f body_curvature_failures=%zu body_self_overlap_failures=%zu ownership_test_samples=%zu zero_owners=%zu multiple_owners=%zu ownership_mode=%s negative_controls=%s first_zero_probe=%zu first_zero_segment=%zu first_zero_point_m=(%.3f,%.3f,%.3f) first_zero_plane_side_m=(%.3f,%.3f) status=%s\n",
|
||
+ ResolvedLayout->passages[passageIndex].stableId.c_str(),
|
||
+ sampledSegments.size()+1,PassageSlabReachMetres,
|
||
+ slabAudit.upperMouthEndSegment,slabAudit.upperMouthTransitionEndSegment,
|
||
+ slabAudit.lowerMouthTransitionStartSegment,slabAudit.lowerMouthStartSegment,
|
||
+ slabAudit.tightMouthJoints,slabAudit.nonlocalMouthPairs,
|
||
+ slabAudit.minimumBodyCurvatureRadiusMetres,
|
||
+ slabAudit.minimumBodySelfGapMetres,slabAudit.bodyCurvatureFailures,
|
||
+ slabAudit.bodySelfOverlapFailures,slabAudit.ownershipTestSamples,
|
||
+ slabAudit.zeroOwnerSamples,slabAudit.multipleOwnerSamples,
|
||
+ slabAudit.usesWholeRouteCompositeSlab?"whole-route-composite":"per-segment",
|
||
+ slabAudit.ownershipNegativeControlsPass?"PASS":"FAIL",
|
||
+ slabAudit.firstZeroOwnerProbe,slabAudit.firstZeroOwnerSegment,
|
||
+ slabAudit.firstZeroOwnerPoint[0],slabAudit.firstZeroOwnerPoint[1],
|
||
+ slabAudit.firstZeroOwnerPoint[2],slabAudit.firstZeroOwnerStartSide,
|
||
+ slabAudit.firstZeroOwnerEndSide,
|
||
+ slabAudit.valid?"PASS":"FAIL");
|
||
+ if(!slabAudit.valid)
|
||
+ {
|
||
+ Error=ResolvedLayout->passages[passageIndex].stableId
|
||
+ +" route does not have unique reach-bounded ownership slabs";
|
||
+ return false;
|
||
+ }
|
||
constexpr double MaximumFloorGradeDegrees=15.0;
|
||
constexpr double DegreesPerRadian=180.0/3.14159265358979323846;
|
||
double maximumFloorGradeDegrees=0.0;
|
||
@@ -4739,6 +4885,7 @@ private:
|
||
for(const auto& raw:rawSegments)
|
||
{
|
||
FPassageSegmentData data;
|
||
+ data.controlSpan=raw.controlSpan;
|
||
data.authoredFloorZA=raw.plannedFloorA;
|
||
data.authoredFloorZDelta=raw.plannedFloorB-raw.plannedFloorA;
|
||
data.authoredAlongMetres=raw.alongMetres;
|
||
@@ -4794,6 +4941,7 @@ private:
|
||
preparedSegments*sizeof(FPassageSegmentData),
|
||
std::chrono::duration<double,std::milli>(
|
||
std::chrono::steady_clock::now()-preparationStart).count());
|
||
+ return true;
|
||
}
|
||
|
||
void BuildPassageSpatialIndices()
|
||
@@ -4823,6 +4971,78 @@ private:
|
||
{
|
||
const std::size_t segmentCount=PassageSegmentData[passageIndex].size();
|
||
if(segmentCount==0)continue;
|
||
+ auto& segments=PassageSegmentData[passageIndex];
|
||
+ auto& slabs=PassageOwnershipSlabs[passageIndex];
|
||
+ std::vector<std::array<double,3>> frameTangents(segmentCount+1);
|
||
+ const auto normalized=[](std::array<double,3> value)
|
||
+ {
|
||
+ const double length=std::sqrt(value[0]*value[0]+value[1]*value[1]
|
||
+ +value[2]*value[2]);
|
||
+ if(length>1.0e-12)
|
||
+ for(double& component:value)component/=length;
|
||
+ return value;
|
||
+ };
|
||
+ for(std::size_t frame=0;frame<=segmentCount;++frame)
|
||
+ {
|
||
+ const std::array<double,3> incoming=frame>0
|
||
+ ?normalized(segments[frame-1].delta):std::array<double,3>{};
|
||
+ const std::array<double,3> outgoing=frame<segmentCount
|
||
+ ?normalized(segments[frame].delta):std::array<double,3>{};
|
||
+ std::array<double,3> tangent{};
|
||
+ if(frame==0)tangent=outgoing;
|
||
+ else if(frame==segmentCount)tangent=incoming;
|
||
+ else
|
||
+ for(std::size_t axis=0;axis<3;++axis)
|
||
+ tangent[axis]=incoming[axis]+outgoing[axis];
|
||
+ tangent=normalized(tangent);
|
||
+ if(tangent[0]==0.0&&tangent[1]==0.0&&tangent[2]==0.0)
|
||
+ tangent=frame<segmentCount?outgoing:incoming;
|
||
+ frameTangents[frame]=tangent;
|
||
+ }
|
||
+ const auto& passageConstants=PassageComposerConstants[passageIndex];
|
||
+ for(std::size_t segment=0;segment<segmentCount;++segment)
|
||
+ {
|
||
+ segments[segment].frameStartTangent=frameTangents[segment];
|
||
+ segments[segment].frameEndTangent=frameTangents[segment+1];
|
||
+ }
|
||
+ const std::size_t upperMouthEnd=std::min(segmentCount,
|
||
+ passageConstants.upperMouthEndSegment);
|
||
+ const std::size_t upperTransitionEnd=std::clamp(
|
||
+ passageConstants.upperMouthTransitionEndSegment,upperMouthEnd,segmentCount);
|
||
+ const std::size_t lowerTransitionStart=std::clamp(
|
||
+ passageConstants.lowerMouthTransitionStartSegment,
|
||
+ upperTransitionEnd,segmentCount);
|
||
+ const std::size_t lowerMouthStart=std::clamp(
|
||
+ passageConstants.lowerMouthStartSegment,lowerTransitionStart,segmentCount);
|
||
+ const auto addOwnershipSlab=[&](std::size_t first,std::size_t end)
|
||
+ {
|
||
+ if(first>=end)return;
|
||
+ FPassageOwnershipSlab slab;
|
||
+ slab.firstSegment=first;slab.endSegment=end;
|
||
+ slab.hasStartPlane=first>0;
|
||
+ slab.hasEndPlane=end<segmentCount;
|
||
+ slab.startPosition=segments[first].a;
|
||
+ slab.endPosition=segments[end-1].b;
|
||
+ slab.startTangent=frameTangents[first];
|
||
+ slab.endTangent=frameTangents[end];
|
||
+ const std::size_t slabIndex=slabs.size();
|
||
+ slabs.push_back(slab);
|
||
+ for(std::size_t segment=first;segment<end;++segment)
|
||
+ segments[segment].ownershipSlabIndex=slabIndex;
|
||
+ };
|
||
+ if(passageConstants.wholeRouteCompositeSlab
|
||
+ ||upperMouthEnd>=lowerMouthStart)
|
||
+ addOwnershipSlab(0,segmentCount);
|
||
+ else
|
||
+ {
|
||
+ addOwnershipSlab(0,upperMouthEnd);
|
||
+ addOwnershipSlab(upperMouthEnd,upperTransitionEnd);
|
||
+ for(std::size_t segment=upperTransitionEnd;
|
||
+ segment<lowerTransitionStart;++segment)
|
||
+ addOwnershipSlab(segment,segment+1);
|
||
+ addOwnershipSlab(lowerTransitionStart,lowerMouthStart);
|
||
+ addOwnershipSlab(lowerMouthStart,segmentCount);
|
||
+ }
|
||
std::vector<std::size_t> segmentIds(segmentCount);
|
||
for(std::size_t i=0;i<segmentCount;++i)segmentIds[i]=i;
|
||
BuildPassageNearestNode(passageIndex,std::move(segmentIds));
|
||
@@ -4864,12 +5084,83 @@ private:
|
||
}
|
||
PassageProofBounds.push_back(proof);
|
||
|
||
+ double axisExtent=localExtent;
|
||
+ bool hasTightSweepBounds=false;
|
||
+ const auto& ownershipSlab=slabs[prepared.ownershipSlabIndex];
|
||
+ if(ownershipSlab.firstSegment==localIndex
|
||
+ &&ownershipSlab.endSegment==localIndex+1
|
||
+ &&prepared.lengthSubvoxels>1.0e-9)
|
||
+ {
|
||
+ std::array<double,3> direction=prepared.delta;
|
||
+ const double inverseLength=1.0/prepared.lengthSubvoxels;
|
||
+ for(double& component:direction)component*=inverseLength;
|
||
+ const auto tangentCosine=[&](const std::array<double,3>& tangent)
|
||
+ {
|
||
+ return std::clamp(direction[0]*tangent[0]
|
||
+ +direction[1]*tangent[1]+direction[2]*tangent[2],-1.0,1.0);
|
||
+ };
|
||
+ const double startCos=tangentCosine(prepared.frameStartTangent);
|
||
+ const double endCos=tangentCosine(prepared.frameEndTangent);
|
||
+ if(startCos>1.0e-6&&endCos>1.0e-6)
|
||
+ {
|
||
+ const double alongExtra=localExtent*std::max(
|
||
+ std::sqrt(std::max(0.0,1.0-startCos*startCos))/startCos,
|
||
+ std::sqrt(std::max(0.0,1.0-endCos*endCos))/endCos);
|
||
+ for(std::size_t axis=0;axis<3;++axis)
|
||
+ {
|
||
+ axisExtent=localExtent*std::sqrt(std::max(0.0,
|
||
+ 1.0-direction[axis]*direction[axis]))
|
||
+ +alongExtra*std::abs(direction[axis])+0.11;
|
||
+ prepared.sweepMinimumSubvoxels[axis]=
|
||
+ static_cast<std::int64_t>(std::floor(
|
||
+ (localMinimum[axis]-axisExtent)*Q));
|
||
+ prepared.sweepMaximumSubvoxels[axis]=
|
||
+ static_cast<std::int64_t>(std::ceil(
|
||
+ (localMaximum[axis]+axisExtent)*Q));
|
||
+ }
|
||
+ hasTightSweepBounds=true;
|
||
+ }
|
||
+ }
|
||
+ if(!hasTightSweepBounds)
|
||
+ {
|
||
+ for(std::size_t axis=0;axis<3;++axis)
|
||
+ {
|
||
+ prepared.sweepMinimumSubvoxels[axis]=static_cast<std::int64_t>(
|
||
+ std::floor((localMinimum[axis]-localExtent)*Q));
|
||
+ prepared.sweepMaximumSubvoxels[axis]=static_cast<std::int64_t>(
|
||
+ std::ceil((localMaximum[axis]+localExtent)*Q));
|
||
+ }
|
||
+ }
|
||
+ const auto& slab=slabs[prepared.ownershipSlabIndex];
|
||
+ std::array<double,3> clippedMinimum{},clippedMaximum{};
|
||
for(std::size_t axis=0;axis<3;++axis)
|
||
{
|
||
- prepared.sweepMinimumSubvoxels[axis]=static_cast<std::int64_t>(
|
||
- std::floor((localMinimum[axis]-localExtent)*Q));
|
||
- prepared.sweepMaximumSubvoxels[axis]=static_cast<std::int64_t>(
|
||
- std::ceil((localMaximum[axis]+localExtent)*Q));
|
||
+ clippedMinimum[axis]=static_cast<double>(
|
||
+ prepared.sweepMinimumSubvoxels[axis]);
|
||
+ clippedMaximum[axis]=static_cast<double>(
|
||
+ prepared.sweepMaximumSubvoxels[axis]);
|
||
+ }
|
||
+ bool clipped=true;
|
||
+ if(slab.hasStartPlane)
|
||
+ clipped=PassageClipBoundsToOwnershipPlane(clippedMinimum,
|
||
+ clippedMaximum,slab.startPosition,slab.startTangent,true);
|
||
+ if(clipped&&slab.hasEndPlane)
|
||
+ clipped=PassageClipBoundsToOwnershipPlane(clippedMinimum,
|
||
+ clippedMaximum,slab.endPosition,slab.endTangent,false);
|
||
+ if(!clipped)
|
||
+ {
|
||
+ prepared.sweepMinimumSubvoxels={1,1,1};
|
||
+ prepared.sweepMaximumSubvoxels={0,0,0};
|
||
+ }
|
||
+ else
|
||
+ {
|
||
+ for(std::size_t axis=0;axis<3;++axis)
|
||
+ {
|
||
+ prepared.sweepMinimumSubvoxels[axis]=
|
||
+ static_cast<std::int64_t>(std::floor(clippedMinimum[axis]));
|
||
+ prepared.sweepMaximumSubvoxels[axis]=
|
||
+ static_cast<std::int64_t>(std::ceil(clippedMaximum[axis]));
|
||
+ }
|
||
}
|
||
}
|
||
const double routeExtent=maximumRadiusSubvoxels/Q+requiredReach+0.01;
|
||
@@ -11433,6 +11724,7 @@ private:
|
||
std::vector<std::vector<FPassageNearestNode>> PassageNearestNodes;
|
||
std::vector<FPassageComposerConstants> PassageComposerConstants;
|
||
std::vector<std::vector<FPassageSegmentData>> PassageSegmentData;
|
||
+ std::vector<std::vector<FPassageOwnershipSlab>> PassageOwnershipSlabs;
|
||
std::vector<std::size_t> PassageSegmentOffsets;
|
||
std::vector<FPassageProofBound> PassageProofBounds;
|
||
std::vector<FPassageProofNode> PassageProofNodes;
|
||
@@ -11507,6 +11799,7 @@ inline void FStandaloneShapeWorld::FScopedPassageTileQueries::prepareSweptTubeBa
|
||
passageSamples.samples.resize(gridSampleCount);
|
||
const auto& constants=shape_->PassageComposerConstants[passageIndex];
|
||
const auto& segments=shape_->PassageSegmentData[passageIndex];
|
||
+ const auto& ownershipSlabs=shape_->PassageOwnershipSlabs[passageIndex];
|
||
for(const auto& candidate:candidates)
|
||
{
|
||
if(candidate.segmentIndex>=segments.size())continue;
|
||
@@ -11571,11 +11864,86 @@ inline void FStandaloneShapeWorld::FScopedPassageTileQueries::prepareSweptTubeBa
|
||
if(segmentFirst[0]>segmentLast[0]
|
||
||segmentFirst[1]>segmentLast[1]
|
||
||segmentFirst[2]>segmentLast[2])continue;
|
||
- for(std::int32_t gz=segmentFirst[2];gz<=segmentLast[2];++gz)
|
||
+ if(segment.ownershipSlabIndex>=ownershipSlabs.size())continue;
|
||
+ const auto& ownershipSlab=ownershipSlabs[segment.ownershipSlabIndex];
|
||
+ std::size_t scanAxis=0;
|
||
+ for(std::size_t axis=1;axis<3;++axis)
|
||
{
|
||
- for(std::int32_t gy=segmentFirst[1];gy<=segmentLast[1];++gy)
|
||
- for(std::int32_t gx=segmentFirst[0];gx<=segmentLast[0];++gx)
|
||
+ const double currentScore=std::abs(ownershipSlab.startTangent[axis])
|
||
+ +std::abs(ownershipSlab.endTangent[axis]);
|
||
+ const double bestScore=std::abs(ownershipSlab.startTangent[scanAxis])
|
||
+ +std::abs(ownershipSlab.endTangent[scanAxis]);
|
||
+ if(currentScore>bestScore)scanAxis=axis;
|
||
+ }
|
||
+ const std::size_t rowAxisA=(scanAxis+1)%3;
|
||
+ const std::size_t rowAxisB=(scanAxis+2)%3;
|
||
+ for(std::int32_t rowB=segmentFirst[rowAxisB];
|
||
+ rowB<=segmentLast[rowAxisB];++rowB)
|
||
+ {
|
||
+ for(std::int32_t rowA=segmentFirst[rowAxisA];
|
||
+ rowA<=segmentLast[rowAxisA];++rowA)
|
||
{
|
||
+ std::array<std::int32_t,3> gridCoordinates{};
|
||
+ gridCoordinates[rowAxisA]=rowA;
|
||
+ gridCoordinates[rowAxisB]=rowB;
|
||
+ std::int32_t scanFirst=segmentFirst[scanAxis];
|
||
+ std::int32_t scanLast=segmentLast[scanAxis];
|
||
+ const auto constrainPlane=[&](
|
||
+ const std::array<double,3>& origin,
|
||
+ const std::array<double,3>& tangent,bool keepPositive)
|
||
+ {
|
||
+ double intercept=-origin[scanAxis]*tangent[scanAxis];
|
||
+ for(std::size_t axis=0;axis<3;++axis)
|
||
+ {
|
||
+ if(axis==scanAxis)continue;
|
||
+ const double coordinate=static_cast<double>(
|
||
+ sweep->gridMinimumSubvoxels[axis]
|
||
+ +static_cast<std::int64_t>(gridCoordinates[axis])
|
||
+ *sweep->stepSubvoxels);
|
||
+ intercept+=(coordinate-origin[axis])*tangent[axis];
|
||
+ }
|
||
+ const double direction=tangent[scanAxis];
|
||
+ if(direction==0.0)
|
||
+ {
|
||
+ if(keepPositive?intercept<0.0:intercept>=0.0)
|
||
+ {scanFirst=1;scanLast=0;}
|
||
+ return;
|
||
+ }
|
||
+ const double boundary=-intercept/direction;
|
||
+ const double relative=(boundary
|
||
+ -sweep->gridMinimumSubvoxels[scanAxis])
|
||
+ /sweep->stepSubvoxels;
|
||
+ const double clampedRelative=std::clamp(relative,-1.0,
|
||
+ static_cast<double>(sweep->dimension)+1.0);
|
||
+ if(keepPositive&&direction>0.0)
|
||
+ scanFirst=std::max(scanFirst,
|
||
+ static_cast<std::int32_t>(
|
||
+ std::ceil(clampedRelative))-1);
|
||
+ else if(keepPositive)
|
||
+ scanLast=std::min(scanLast,
|
||
+ static_cast<std::int32_t>(
|
||
+ std::floor(clampedRelative))+1);
|
||
+ else if(direction>0.0)
|
||
+ scanLast=std::min(scanLast,
|
||
+ static_cast<std::int32_t>(
|
||
+ std::ceil(clampedRelative)));
|
||
+ else
|
||
+ scanFirst=std::max(scanFirst,
|
||
+ static_cast<std::int32_t>(
|
||
+ std::floor(clampedRelative)));
|
||
+ };
|
||
+ if(ownershipSlab.hasStartPlane)
|
||
+ constrainPlane(ownershipSlab.startPosition,
|
||
+ ownershipSlab.startTangent,true);
|
||
+ if(scanFirst<=scanLast&&ownershipSlab.hasEndPlane)
|
||
+ constrainPlane(ownershipSlab.endPosition,
|
||
+ ownershipSlab.endTangent,false);
|
||
+ for(std::int32_t scan=scanFirst;scan<=scanLast;++scan)
|
||
+ {
|
||
+ gridCoordinates[scanAxis]=scan;
|
||
+ const std::int32_t gx=gridCoordinates[0];
|
||
+ const std::int32_t gy=gridCoordinates[1];
|
||
+ const std::int32_t gz=gridCoordinates[2];
|
||
const double qx=static_cast<double>(sweep->gridMinimumSubvoxels[0]
|
||
+static_cast<std::int64_t>(gx)*sweep->stepSubvoxels);
|
||
const double qy=static_cast<double>(sweep->gridMinimumSubvoxels[1]
|
||
@@ -11586,6 +11954,15 @@ inline void FStandaloneShapeWorld::FScopedPassageTileQueries::prepareSweptTubeBa
|
||
*static_cast<std::size_t>(sweep->dimension)+static_cast<std::size_t>(gy))
|
||
*static_cast<std::size_t>(sweep->dimension)+static_cast<std::size_t>(gx);
|
||
auto& sample=passageSamples.samples[sampleIndex];
|
||
+ // The scan interval was clipped against both ownership planes, with
|
||
+ // one grid-step guard. Only its ends can lie outside the exact slab;
|
||
+ // keep the exact predicate there and skip two plane dot products for
|
||
+ // every interior sample. The plan audit independently checks unique
|
||
+ // ownership throughout the full support radius.
|
||
+ if((scan==scanFirst||scan==scanLast)
|
||
+ &&!FStandaloneShapeWorld::PassageOwnershipSlabOwns(
|
||
+ ownershipSlabs[segment.ownershipSlabIndex],{qx,qy,qz}))
|
||
+ continue;
|
||
const double projectionXY=(qx-segment.a[0])*segment.delta[0]
|
||
+(qy-segment.a[1])*segment.delta[1];
|
||
const double t=std::clamp((projectionXY
|
||
@@ -11599,8 +11976,8 @@ inline void FStandaloneShapeWorld::FScopedPassageTileQueries::prepareSweptTubeBa
|
||
ex*ex+ey*ey+ez*ez)-radius;
|
||
if(profile&&signedDistance<sweep->influenceSubvoxels+1.0)
|
||
++contributingPairCount;
|
||
- const double floorT=segment.horizontalLengthSq>1.0e-12
|
||
- ?projectionXY/segment.horizontalLengthSq:t;
|
||
+ const double floorT=FStandaloneShapeWorld::PassageOwnershipFloorT(
|
||
+ segment,{qx,qy,qz});
|
||
const double floorTClamped=std::clamp(floorT,0.0,1.0);
|
||
const double floorCenterDistanceSq=ex*ex+ey*ey+ez*ez;
|
||
const double floorRadius=segment.radiusA
|
||
@@ -11612,20 +11989,32 @@ inline void FStandaloneShapeWorld::FScopedPassageTileQueries::prepareSweptTubeBa
|
||
constants,segments,segmentIndex,floorT,segment,
|
||
qx,qy,floorDepth,qPerMetre);
|
||
if(profile)++profileStats_->passageSweepSegmentSamplePairs;
|
||
- if(signedDistance>sweep->influenceSubvoxels)
|
||
+ // A normal body slab contains one segment, so it owns the floor
|
||
+ // profile directly. Mouth transitions and pathological composite
|
||
+ // routes deliberately group segments into one slab and retain their
|
||
+ // established nearest-floor tie-break within that owner.
|
||
+ if(ownershipSlab.firstSegment==segmentIndex
|
||
+ &&ownershipSlab.endSegment==segmentIndex+1)
|
||
{
|
||
- if(profile)++profileStats_->passageSweepOutsideInfluencePairs;
|
||
+ sample.floorCenterZSubvoxels=floorCenterZ;
|
||
+ sample.floorDepthSubvoxels=floorDepth;
|
||
+ sample.floorT=floorT;
|
||
+ sample.floorSegmentIndex=static_cast<std::uint32_t>(segmentIndex);
|
||
+ sample.floorCenterDistanceSq=floorCenterDistanceSq;
|
||
}
|
||
- if(floorCenterDistanceSq<sample.floorCenterDistanceSq
|
||
+ else if(floorCenterDistanceSq<sample.floorCenterDistanceSq
|
||
||(floorCenterDistanceSq==sample.floorCenterDistanceSq
|
||
&&segmentIndex<sample.floorSegmentIndex))
|
||
{
|
||
- sample.floorCenterDistanceSq=floorCenterDistanceSq;
|
||
sample.floorCenterZSubvoxels=floorCenterZ;
|
||
sample.floorDepthSubvoxels=floorDepth;
|
||
sample.floorT=floorT;
|
||
- sample.floorSegmentIndex=static_cast<std::uint32_t>(
|
||
- segmentIndex);
|
||
+ sample.floorSegmentIndex=static_cast<std::uint32_t>(segmentIndex);
|
||
+ sample.floorCenterDistanceSq=floorCenterDistanceSq;
|
||
+ }
|
||
+ if(signedDistance>sweep->influenceSubvoxels)
|
||
+ {
|
||
+ if(profile)++profileStats_->passageSweepOutsideInfluencePairs;
|
||
}
|
||
// A negative shell distance is possible only when the flattened tube
|
||
// distance lies in [0, shellThickness]. SmoothMaxDistance is at least
|
||
@@ -11707,12 +12096,13 @@ inline void FStandaloneShapeWorld::FScopedPassageTileQueries::prepareSweptTubeBa
|
||
sample.hasWallRelief=false;
|
||
sample.hasExactContribution=true;
|
||
if(profile)++profileStats_->passageSweepMinimumUpdates;
|
||
- }
|
||
+ }
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
+ }
|
||
|
||
for(auto& passageSamples:sweep->passages)
|
||
{
|
||
diff --git a/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-floating-islands-style2-v226.baseline b/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-floating-islands-style2-v226.baseline
|
||
new file mode 100644
|
||
index 0000000..cab68d6
|
||
--- /dev/null
|
||
+++ b/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-floating-islands-style2-v226.baseline
|
||
@@ -0,0 +1,12 @@
|
||
+[PipelineFullDetailHash] tiles=960 density=0x91931a57a067fd92 mesh=0xa818654be89790ac payload=0x370ff80451105a39 topology=0x06e507557aa70173
|
||
+[PipelineWholeViewHash] tiles=2370 density=0x3ce77475b5908860 mesh=0x80d3ca4b215ddb16 payload=0xf838e9c8ece70933 topology=0xdf790ec894e60a20
|
||
+[PipelineLODHash] lod=0 tiles=512 density=0x2fd4c78685a43cfe mesh=0x50478cff1c1f8ca0 payload=0x54e48e57e0af26de topology=0x860c011cfdb9833a
|
||
+[PipelineLODHash] lod=1 tiles=448 density=0x4b8d9a6dcd2942ad mesh=0x17e89b7087bfe711 payload=0x2389c109eb780db6 topology=0x7eccd4cf560d0630
|
||
+[PipelineLODHash] lod=2 tiles=320 density=0xb3e6e0585b0fc92d mesh=0x16c9a82c59e0f3d3 payload=0x1b32fc2e695144de topology=0x89a1eddead8849c1
|
||
+[PipelineLODHash] lod=3 tiles=208 density=0xe7c3ce8c79544110 mesh=0xff01589508cbbe69 payload=0xe4370a5d39da57c3 topology=0x2ea4ffeae7c72463
|
||
+[PipelineLODHash] lod=4 tiles=240 density=0xfb11591772a3d18e mesh=0xebf2baee8764591b payload=0xe491d7ac9c0061ad topology=0xd618ac33b95ce494
|
||
+[PipelineLODHash] lod=5 tiles=144 density=0x2f8b7b7fb1e67258 mesh=0x4cd508cbe97344fd payload=0x2b6e96a6050458c9 topology=0x4ff6d0eda8988573
|
||
+[PipelineLODHash] lod=6 tiles=96 density=0x0114a34c8b8d6113 mesh=0xb1fec4d01ae1fa44 payload=0x3e50cfd429b8e950 topology=0x08090c96d15c1618
|
||
+[PipelineLODHash] lod=7 tiles=96 density=0x3836ed8cfce73418 mesh=0x40bc6589c5fc1aac payload=0x677349bcae16790f topology=0x36e3aa53192e6a6b
|
||
+[PipelineLODHash] lod=8 tiles=306 density=0x144f3c1fd722708b mesh=0xef77b28bf7fd9453 payload=0xe3f47b37fcd61489 topology=0x5a3c73e295494e04
|
||
+[PipelineExactQueryHash] probes=9 density=0x7b7c50ac24ccadcc unclamped=0x7b7c50ac24ccadcc detail=0xefcba20c0c70c415 collision=0xe604823a249029bf path=direct-Core-queries
|
||
diff --git a/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-owner4-surface-world-style1-v226.baseline b/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-owner4-surface-world-style1-v226.baseline
|
||
new file mode 100644
|
||
index 0000000..3af58f0
|
||
--- /dev/null
|
||
+++ b/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-owner4-surface-world-style1-v226.baseline
|
||
@@ -0,0 +1,12 @@
|
||
+[PipelineFullDetailHash] tiles=960 density=0x8351b0410e09bad1 mesh=0x065b56bf31023628 payload=0xa8f438d8c4247999 topology=0x9bb94e8edb2c8dce
|
||
+[PipelineWholeViewHash] tiles=2578 density=0xd95e78cea6a82b28 mesh=0xa50dbd869848a6fe payload=0xc78599692cb2e67a topology=0x5274c97954eee2c8
|
||
+[PipelineLODHash] lod=0 tiles=512 density=0x1a13bdd0641f3c46 mesh=0x010f1ce532bc0bae payload=0x5c249f31a0e55494 topology=0x5fbad4f63b23eaab
|
||
+[PipelineLODHash] lod=1 tiles=448 density=0xb1cdeec444ad72fa mesh=0x0d26924ba452ec23 payload=0xc93967164a6fd098 topology=0x86b8391e622498f0
|
||
+[PipelineLODHash] lod=2 tiles=320 density=0x9bca76e9112eb2ad mesh=0x4c4a0c509b59c7e6 payload=0xfb7985d6da664124 topology=0xae622b59ae115234
|
||
+[PipelineLODHash] lod=3 tiles=272 density=0x267227f5066ac775 mesh=0x4945616669ae8489 payload=0xf3e83d394410d28e topology=0x56b7e63096340e41
|
||
+[PipelineLODHash] lod=4 tiles=272 density=0xb743eb64967bddcd mesh=0xf6b5bd36b0ebfead payload=0xd1284c1540f50636 topology=0x4a6e197e2a1a9500
|
||
+[PipelineLODHash] lod=5 tiles=208 density=0x38706b628a74dd17 mesh=0x445b1c7b51489cbb payload=0x713efc66290768f7 topology=0x56486b919146bb1e
|
||
+[PipelineLODHash] lod=6 tiles=144 density=0xd53849b2002a1faa mesh=0xaa210abc8498c415 payload=0xd109aef8639706d1 topology=0x61a4d75fbbc35268
|
||
+[PipelineLODHash] lod=7 tiles=96 density=0xb0ea9a140275c9cc mesh=0x3ffd8365c8b0ec29 payload=0x3577afb0a2e4a420 topology=0x97530aafde15b7e0
|
||
+[PipelineLODHash] lod=8 tiles=306 density=0x8b3d1816799d67a0 mesh=0x80bf554f17700e1e payload=0xccbe22c3333a9c34 topology=0xcb11b28f6b6bcc50
|
||
+[PipelineExactQueryHash] probes=9 density=0xf2df67b38b7802df unclamped=0x48b347986345b557 detail=0xefcba20c0c70c415 collision=0x137e8f6c0b03a624 path=direct-Core-queries
|
||
diff --git a/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-surface-world-style1-v226.baseline b/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-surface-world-style1-v226.baseline
|
||
new file mode 100644
|
||
index 0000000..1f591cf
|
||
--- /dev/null
|
||
+++ b/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-surface-world-style1-v226.baseline
|
||
@@ -0,0 +1,12 @@
|
||
+[PipelineFullDetailHash] tiles=960 density=0xace56c8f205238ad mesh=0xa9c603a1248de6b8 payload=0xea55f1f4fcc6a894 topology=0x5ea7bc78a1b678ae
|
||
+[PipelineWholeViewHash] tiles=2370 density=0xb91a65f2b5d23dd1 mesh=0xcb21ea0acbcf7482 payload=0x812c3d1f7ff8d3b8 topology=0xb18b9bc29e243ba2
|
||
+[PipelineLODHash] lod=0 tiles=512 density=0xcd6ab28cfa504aca mesh=0xf03dc80a669fca31 payload=0x397aded1c3d2d297 topology=0xb4a1dc84fabc1fe3
|
||
+[PipelineLODHash] lod=1 tiles=448 density=0xce6e713828b2a20a mesh=0x1cf408c25d67d18c payload=0x8a2b5d20571c997e topology=0x51cd985fe8e7d100
|
||
+[PipelineLODHash] lod=2 tiles=320 density=0xe06c3509977d89df mesh=0x4d81f55877e78bde payload=0x4dff71b395ce032c topology=0x8af5aa065efee965
|
||
+[PipelineLODHash] lod=3 tiles=208 density=0x60940ca350c1110a mesh=0xd386632a7d9271c0 payload=0x02e312e98024ef89 topology=0x0a5335eabb257106
|
||
+[PipelineLODHash] lod=4 tiles=240 density=0x0be8798407724eaf mesh=0x131c303cbbe51434 payload=0x2a90ecf0a2411ca0 topology=0xe3e054a371152e08
|
||
+[PipelineLODHash] lod=5 tiles=144 density=0xf20897ad272103a9 mesh=0x13d00b6253ae1f72 payload=0x0db3880693a962fa topology=0x462bd87a80f15e89
|
||
+[PipelineLODHash] lod=6 tiles=96 density=0xc040fcac457d6313 mesh=0x1ed132eeb718fb62 payload=0xcd0214e35577c6e3 topology=0x2c56c69e6fdcfd64
|
||
+[PipelineLODHash] lod=7 tiles=96 density=0xc2d419446355fdb8 mesh=0x4ea44452574fc84b payload=0xfd9779bedfc9ae0f topology=0x8948546e770c9b72
|
||
+[PipelineLODHash] lod=8 tiles=306 density=0x62db31696f2da831 mesh=0xd373f9e9481532fe payload=0x540a60526e0e3e46 topology=0xc725cb901e6dc335
|
||
+[PipelineExactQueryHash] probes=9 density=0xc949c4df2ce6f9d9 unclamped=0xc949c4df2ce6f9d9 detail=0x114c501e2d830d94 collision=0xe6011d3a248d4849 path=direct-Core-queries
|
||
diff --git a/Standalone/Tools/verify/verify.py b/Standalone/Tools/verify/verify.py
|
||
index 5fc1584..895ff60 100644
|
||
--- a/Standalone/Tools/verify/verify.py
|
||
+++ b/Standalone/Tools/verify/verify.py
|
||
@@ -122,11 +122,11 @@ RECIPE_RE = re.compile(
|
||
|
||
def owner_hash_baseline(recipe: str, style: int) -> tuple[Path, str]:
|
||
if recipe == "surface-world" and style == 1:
|
||
- return (BASELINES / "owner-hash/canonical/owner-hash-surface-world-style1-v225.baseline",
|
||
- "L125-v225-stamped-passage-sweep")
|
||
+ return (BASELINES / "owner-hash/canonical/owner-hash-surface-world-style1-v226.baseline",
|
||
+ "L128-v226-slab-owned-passage-sweep")
|
||
if recipe == "floating-islands" and style == 2:
|
||
- return (BASELINES / "owner-hash/canonical/owner-hash-floating-islands-style2-v225.baseline",
|
||
- "L125-v225-stamped-passage-sweep")
|
||
+ return (BASELINES / "owner-hash/canonical/owner-hash-floating-islands-style2-v226.baseline",
|
||
+ "L128-v226-slab-owned-passage-sweep")
|
||
raise VerificationFailure(f"no pinned owner hash baseline for {recipe} style {style}")
|
||
|
||
|