Files
VoxelForge/Standalone/Research/WorldAssemblerM3/Linux128/l128-slab-ownership.patch
T
Fr0zkaandClaude Opus 5.5 7022d02e6e research: slab ownership for passage samples (L128, correct but slower; kept as patch)
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
2026-10-30 21:21:24 +01:00

1597 lines
91 KiB
Diff
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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}")