fix(measure): ask the primordial law the right question

Three defects, two of them mine.

1. The anchor was a guess. LargestComponentAnchor() returned a hardcoded
   point near the (0,0) spine and the comment admitted it, so
   NOT_CONNECTED meant "not connected to a guessed point". The flood fill
   already knows the answer; it now returns LargestComponentPoint (tie-
   broken by lowest cell index, so it stays deterministic),
   LargestComponentCells and NumComponentsAtLeast1Pct.

2. "Connected to the largest component" is NOT the primordial law. The
   law is that a player must not descend into a place they cannot leave
   -- a question about MOUTHS. A strate can be 57 disconnected shafts and
   be fine, provided the shaft you arrive in is the shaft you leave from.
   The test now asks arrival -> departure per strate: the LowerPoint of
   the passage arriving, to the UpperPoint of the passage departing.

3. A false verdict and a true one were indistinguishable. VF_AreConnected
   returned false both when no route existed and when the endpoint's own
   coarse cell sampled solid -- and a passage mouth is a carved tube whose
   cell centre lands in adjacent rock easily. It now returns
   EVoxelConnectivityResult and snaps to an adjacent air cell first,
   reporting the repair.

RESULT: the earlier "5 of 15 endpoints disconnected" was an ARTIFACT of
the guessed anchor plus the wrong question. Corrected: 13 of 15 connected,
0 StartCellSolid, 0 GoalCellSolid.

One real finding survives. arrival -> departure passes for 5 of 6
applicable strates; VerticalShafts FAILS -- its arrival mouth is in the
largest component, its departure mouth is not. NOT yet reported as a bug:
a coarse grid can falsely SEPARATE as well as falsely join, and a
connecting corridor thinner than SampleStep vanishes. Next task settles
it at a finer step.

Also noted: GetPassages() contains a self-referential surface entry
(0 -> 0), excluded from the chain law.

Build clean, 18 succeeded / 0 warnings / 0 failed, verified in VoxelM.log.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bp7FxoQcSoiuhxjuLr6aBM
This commit is contained in:
Fr0zkaandClaude Opus 5 committed 2026-08-30 14:06:43 +02:00
1 parent 13399529b5
commit efcc8fcb65
4 files changed
+636 -150

No files matched your search

+8 -5
View File
@@ -307,15 +307,18 @@ Headless, read-only Tier 2 measurement pass. `VF_MeasureStrate` samples one stra
boundary-seal-derived (or explicitly overridden) interior margin into a bounded coarse grid, boundary-seal-derived (or explicitly overridden) interior margin into a bounded coarse grid,
performs one deterministic 6-connected air flood fill, and derives fractions, components, performs one deterministic 6-connected air flood fill, and derives fractions, components,
walkability, feature scale, and clearance from that grid. Metrics report the resolved margin and walkability, feature scale, and clearance from that grid. Metrics report the resolved margin and
the inclusive/exclusive voxel Z window used. the inclusive/exclusive voxel Z window used, plus the largest component's deterministic lowest-cell
representative point, cell count, and count of components holding at least 1% of the air.
`VF_AreConnected` recovers a deterministic BFS parent path from the same kind of grid and checks `VF_AreConnected` recovers a deterministic BFS parent path from the same kind of grid and checks
only that route at full voxel resolution before returning true; a coarse false positive sets only that route at full voxel resolution before returning `Connected`. It returns an explicit
`bOutCoarseLied`. No UObject state, cache, actor, world, or PIE is required. `EVoxelConnectivityResult`; a solid endpoint cell is repaired by snapping to the nearest air cell
among its 26 neighbours when possible, and reports the snap through its out flags. No UObject state,
cache, actor, world, or PIE is required.
| Symbol | Role | | Symbol | Role |
|--------|------| |--------|------|
| `FVoxelStrateMeasureSettings` / `FVoxelStrateMetrics` | Plain settings/result structs for bounded strate sampling and derived measurements; callers may override the interior margin, and results identify the resolved margin/Z window. | | `FVoxelStrateMeasureSettings` / `FVoxelStrateMetrics` | Plain settings/result structs for bounded strate sampling and derived measurements; callers may override the interior margin, and results identify the resolved margin/Z window and deterministic component representatives. |
| `VF_MeasureStrate` | One-grid/one-flood-fill strate metrics; refuses invalid bounds or a grid over `MaxCells`. | | `VF_MeasureStrate` | One-grid/one-flood-fill strate metrics; refuses invalid bounds or a grid over `MaxCells`. |
| `VF_AreConnected` | Coarse 6-connected BFS plus one full-resolution recheck of its recovered route. | | `VF_AreConnected` | Coarse 6-connected BFS plus one full-resolution recheck of its recovered route, with explicit endpoint-solid, out-of-window, coarse-lie, and connectivity outcomes. |
### 3.7 Cave morphology (SDF rooms/tunnels) — `Public/VoxelCaveMorphology.h` + `.cpp` ### 3.7 Cave morphology (SDF rooms/tunnels) — `Public/VoxelCaveMorphology.h` + `.cpp`
Header is rich with inline docs. Two namespaces + a per-chunk cache system. Header is rich with inline docs. Two namespaces + a per-chunk cache system.
@@ -25,16 +25,27 @@ namespace
FVoxelStrateMetrics DerivedWindowRepeat; FVoxelStrateMetrics DerivedWindowRepeat;
}; };
struct FPassageReport struct FConnectivityProbe
{
bool bChecked = false;
EVoxelConnectivityResult Result = EVoxelConnectivityResult::OutOfWindow;
bool bStartSnapped = false;
bool bGoalSnapped = false;
};
struct FStrateConnectivityReport
{ {
int32 Index = INDEX_NONE; int32 Index = INDEX_NONE;
FString Name; FString Name;
int32 UpperStrateIndex = INDEX_NONE; int32 ArrivalPassageIndex = INDEX_NONE;
int32 LowerStrateIndex = INDEX_NONE; int32 DeparturePassageIndex = INDEX_NONE;
FVector UpperPoint = FVector::ZeroVector; FVector ArrivalPoint = FVector::ZeroVector;
FVector LowerPoint = FVector::ZeroVector; FVector DeparturePoint = FVector::ZeroVector;
FString UpperResult; bool bHasArrival = false;
FString LowerResult; bool bHasDeparture = false;
FConnectivityProbe ArrivalToDeparture;
FConnectivityProbe ArrivalToLargest;
FConnectivityProbe DepartureToLargest;
}; };
FString ArchetypeName(const FStrateSlot& Slot) FString ArchetypeName(const FStrateSlot& Slot)
@@ -55,6 +66,13 @@ namespace
return FMemory::Memcmp(&A, &B, sizeof(float)) == 0; return FMemory::Memcmp(&A, &B, sizeof(float)) == 0;
} }
bool SameVectorBits(const FVector& A, const FVector& B)
{
return SameFloatBits(A.X, B.X)
&& SameFloatBits(A.Y, B.Y)
&& SameFloatBits(A.Z, B.Z);
}
bool MetricsAreBitIdentical(const FVoxelStrateMetrics& A, const FVoxelStrateMetrics& B) bool MetricsAreBitIdentical(const FVoxelStrateMetrics& A, const FVoxelStrateMetrics& B)
{ {
return A.bValid == B.bValid return A.bValid == B.bValid
@@ -65,6 +83,9 @@ namespace
&& SameFloatBits(A.AirFraction, B.AirFraction) && SameFloatBits(A.AirFraction, B.AirFraction)
&& A.NumAirComponents == B.NumAirComponents && A.NumAirComponents == B.NumAirComponents
&& SameFloatBits(A.LargestComponentShare, B.LargestComponentShare) && SameFloatBits(A.LargestComponentShare, B.LargestComponentShare)
&& SameVectorBits(A.LargestComponentPoint, B.LargestComponentPoint)
&& A.LargestComponentCells == B.LargestComponentCells
&& A.NumComponentsAtLeast1Pct == B.NumComponentsAtLeast1Pct
&& SameFloatBits(A.WalkableFraction, B.WalkableFraction) && SameFloatBits(A.WalkableFraction, B.WalkableFraction)
&& SameFloatBits(A.MedianFeatureScale, B.MedianFeatureScale) && SameFloatBits(A.MedianFeatureScale, B.MedianFeatureScale)
&& A.MedianVerticalClearance == B.MedianVerticalClearance && A.MedianVerticalClearance == B.MedianVerticalClearance
@@ -73,34 +94,45 @@ namespace
&& A.SampledMaxZ == B.SampledMaxZ; && A.SampledMaxZ == B.SampledMaxZ;
} }
bool IsInsideMeasuredWindow( const TCHAR* ConnectivityResultName(EVoxelConnectivityResult Result)
const FVoxelStrateMetrics& Metrics,
const FVoxelStrateMeasureSettings& Settings,
const FVector& Point)
{ {
const float MinX = Settings.CenterXY.X - static_cast<float>(Settings.RadiusInVoxels); switch (Result)
const float MaxX = Settings.CenterXY.X + static_cast<float>(Settings.RadiusInVoxels); {
const float MinY = Settings.CenterXY.Y - static_cast<float>(Settings.RadiusInVoxels); case EVoxelConnectivityResult::Connected: return TEXT("CONNECTED");
const float MaxY = Settings.CenterXY.Y + static_cast<float>(Settings.RadiusInVoxels); case EVoxelConnectivityResult::NotConnected: return TEXT("NOT_CONNECTED");
return Point.X >= MinX && Point.X < MaxX case EVoxelConnectivityResult::StartCellSolid: return TEXT("START_CELL_SOLID");
&& Point.Y >= MinY && Point.Y < MaxY case EVoxelConnectivityResult::GoalCellSolid: return TEXT("GOAL_CELL_SOLID");
&& Point.Z >= static_cast<float>(Metrics.SampledMinZ) case EVoxelConnectivityResult::OutOfWindow: return TEXT("OUT_OF_WINDOW");
&& Point.Z < static_cast<float>(Metrics.SampledMaxZ); case EVoxelConnectivityResult::CoarseLied: return TEXT("COARSE_LIED");
default: return TEXT("UNKNOWN");
}
} }
FVector LargestComponentAnchor( FString ConnectivityProbeText(const FConnectivityProbe& Probe)
const FStrateSlot& Slot,
const FVector2D& CenterXY,
int32 SampleStep)
{ {
// The fixture's origin spine is the deterministic hub/void anchor. The measurement API if (!Probe.bChecked)
// intentionally returns metrics rather than a component coordinate, so this report probes {
// the known spine point that represents the largest reachable component in this fixture. return TEXT("NOT_CHECKED");
// Keep the established anchor one full chunk above the strate floor. The derived window }
// includes it, while the first derived sample cell may still be floor rock for slab fields.
const float AnchorZ = (static_cast<float>(Slot.BottomChunkZ) + 1.0f) * CHUNK_SIZE FString Text = ConnectivityResultName(Probe.Result);
+ 0.5f * static_cast<float>(SampleStep); if (Probe.bStartSnapped || Probe.bGoalSnapped)
return FVector(CenterXY.X + 2.0f, CenterXY.Y + 2.0f, AnchorZ); {
Text += TEXT(" [");
bool bNeedSeparator = false;
if (Probe.bStartSnapped)
{
Text += TEXT("start snapped");
bNeedSeparator = true;
}
if (Probe.bGoalSnapped)
{
if (bNeedSeparator) Text += TEXT(", ");
Text += TEXT("goal snapped");
}
Text += TEXT("]");
}
return Text;
} }
bool FullResolutionDirectSegmentHasSolid( bool FullResolutionDirectSegmentHasSolid(
@@ -239,8 +271,8 @@ bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters)
* 2. Vacuity: a main measurement with no samples, no air, or no solid measures nothing. * 2. Vacuity: a main measurement with no samples, no air, or no solid measures nothing.
* 3. Range sanity: fractions/components must describe an actual air field. * 3. Range sanity: fractions/components must describe an actual air field.
* 4. Determinism: identical inputs must produce bit-identical metrics. * 4. Determinism: identical inputs must produce bit-identical metrics.
* 5. Coarse-lie guard: a coarse route must never be reported connected after a full-resolution * 5. Coarse-lie guard: a coarse route must never be reported CONNECTED after a
* solid sample; the controlled 3-voxel wall below must make bOutCoarseLied fire. * full-resolution solid sample; the controlled 3-voxel wall below must return COARSE_LIED.
*/ */
int32 GapTopChunkZ = 0; int32 GapTopChunkZ = 0;
@@ -349,6 +381,15 @@ bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters)
|| !FMath::IsFinite(Metrics.WalkableFraction) || !FMath::IsFinite(Metrics.WalkableFraction)
|| Metrics.WalkableFraction < 0.0f || Metrics.WalkableFraction > 1.0f || Metrics.WalkableFraction < 0.0f || Metrics.WalkableFraction > 1.0f
|| (Metrics.NumAir > 0 && Metrics.NumAirComponents == 0) || (Metrics.NumAir > 0 && Metrics.NumAirComponents == 0)
|| Metrics.LargestComponentCells < 0
|| Metrics.LargestComponentCells > Metrics.NumAir
|| Metrics.NumComponentsAtLeast1Pct < 0
|| Metrics.NumComponentsAtLeast1Pct > Metrics.NumAirComponents
|| (Metrics.NumAir > 0 && Metrics.LargestComponentCells == 0)
|| (Metrics.NumAir > 0
&& (!FMath::IsFinite(Metrics.LargestComponentPoint.X)
|| !FMath::IsFinite(Metrics.LargestComponentPoint.Y)
|| !FMath::IsFinite(Metrics.LargestComponentPoint.Z)))
|| (Metrics.LargestComponentShare == 0.0f && Metrics.AirFraction > 0.05f) || (Metrics.LargestComponentShare == 0.0f && Metrics.AirFraction > 0.05f)
|| Metrics.ResolvedMarginVoxels < 1 || Metrics.ResolvedMarginVoxels < 1
|| Metrics.SampledMinZ >= Metrics.SampledMaxZ) || Metrics.SampledMinZ >= Metrics.SampledMaxZ)
@@ -408,20 +449,22 @@ bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters)
GapAirSamples, GapAirSamples,
GapSamples); GapSamples);
Summary += TEXT("Archetypes (old one-chunk vs derived window; derived InteriorMarginVoxels<0 => 2x BoundarySealThickness, clamped):\n"); Summary += TEXT("Archetypes (old one-chunk vs derived window; derived InteriorMarginVoxels<0 => 2x BoundarySealThickness, clamped):\n");
Summary += TEXT(" name | old margin | old Z span [min,max) | old Air | old Largest | old Walkable | old FeatureScale vox | old Clearance vox | old Components | old Samples | old Solid | derived margin | derived Z span [min,max) | derived Air | derived Largest | derived Walkable | derived FeatureScale vox | derived Clearance vox | derived Components | derived Samples | derived Solid\n"); Summary += TEXT(" name | old margin | old Z span [min,max) | old Air | old Largest | old LargestCells | old >=1% | old Walkable | old FeatureScale vox | old Clearance vox | old Components | old Samples | old Solid | derived margin | derived Z span [min,max) | derived Air | derived Largest | derived LargestCells | derived >=1% | derived Walkable | derived FeatureScale vox | derived Clearance vox | derived Components | derived Samples | derived Solid\n");
for (const FArchetypeReport& Report : ArchetypeReports) for (const FArchetypeReport& Report : ArchetypeReports)
{ {
const FVoxelStrateMetrics& Old = Report.OldWindow; const FVoxelStrateMetrics& Old = Report.OldWindow;
const FVoxelStrateMetrics& Derived = Report.DerivedWindow; const FVoxelStrateMetrics& Derived = Report.DerivedWindow;
Summary += FString::Printf( Summary += FString::Printf(
TEXT(" %s | %d | [%d,%d) | %.9g | %.9g | %.9g | %.9g | %d | %d | %lld | %lld | " TEXT(" %s | %d | [%d,%d) | %.9g | %.9g | %lld | %d | %.9g | %.9g | %d | %d | %lld | %lld | "
"%d | [%d,%d) | %.9g | %.9g | %.9g | %.9g | %d | %d | %lld | %lld\n"), "%d | [%d,%d) | %.9g | %.9g | %lld | %d | %.9g | %.9g | %d | %d | %lld | %lld\n"),
*Report.Name, *Report.Name,
Old.ResolvedMarginVoxels, Old.ResolvedMarginVoxels,
Old.SampledMinZ, Old.SampledMinZ,
Old.SampledMaxZ, Old.SampledMaxZ,
Old.AirFraction, Old.AirFraction,
Old.LargestComponentShare, Old.LargestComponentShare,
Old.LargestComponentCells,
Old.NumComponentsAtLeast1Pct,
Old.WalkableFraction, Old.WalkableFraction,
Old.MedianFeatureScale, Old.MedianFeatureScale,
Old.MedianVerticalClearance, Old.MedianVerticalClearance,
@@ -433,6 +476,8 @@ bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters)
Derived.SampledMaxZ, Derived.SampledMaxZ,
Derived.AirFraction, Derived.AirFraction,
Derived.LargestComponentShare, Derived.LargestComponentShare,
Derived.LargestComponentCells,
Derived.NumComponentsAtLeast1Pct,
Derived.WalkableFraction, Derived.WalkableFraction,
Derived.MedianFeatureScale, Derived.MedianFeatureScale,
Derived.MedianVerticalClearance, Derived.MedianVerticalClearance,
@@ -443,6 +488,81 @@ bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters)
int32 NumRoutesChecked = 0; int32 NumRoutesChecked = 0;
int32 NumRoutesRefuted = 0; int32 NumRoutesRefuted = 0;
int32 NumGuardRoutesChecked = 0;
int32 NumGuardRoutesRefuted = 0;
int32 NumSnappedEndpointEvents = 0;
int32 NumArrivalDepartureRoutesChecked = 0;
int32 NumArrivalDepartureSnappedEndpointEvents = 0;
int32 NumAnchorProbes = 0;
int32 NumAnchorConnected = 0;
int32 NumAnchorNotConnected = 0;
int32 NumAnchorStartCellSolid = 0;
int32 NumAnchorGoalCellSolid = 0;
int32 NumAnchorOutOfWindow = 0;
int32 NumAnchorCoarseLied = 0;
int32 NumAnchorSnappedEndpointEvents = 0;
auto ProbeRoute = [&](int32 StrateIndex,
const FVector& Start,
const FVector& Goal,
bool bCountInGuard,
bool bCountAsAnchorProbe,
bool bCountAsArrivalDeparture) -> FConnectivityProbe
{
FConnectivityProbe Probe;
Probe.bChecked = true;
Probe.Result = VF_AreConnected(
*World.Generator,
*World.StrateManager,
StrateIndex,
Start,
Goal,
DerivedWindowSettings,
Probe.bStartSnapped,
Probe.bGoalSnapped);
++NumRoutesChecked;
if (Probe.Result == EVoxelConnectivityResult::CoarseLied)
{
++NumRoutesRefuted;
}
if (bCountInGuard)
{
++NumGuardRoutesChecked;
if (Probe.Result == EVoxelConnectivityResult::CoarseLied)
{
++NumGuardRoutesRefuted;
}
}
const int32 SnappedHere = (Probe.bStartSnapped ? 1 : 0)
+ (Probe.bGoalSnapped ? 1 : 0);
NumSnappedEndpointEvents += SnappedHere;
if (bCountAsArrivalDeparture)
{
++NumArrivalDepartureRoutesChecked;
NumArrivalDepartureSnappedEndpointEvents += SnappedHere;
}
if (bCountAsAnchorProbe)
{
++NumAnchorProbes;
NumAnchorSnappedEndpointEvents += SnappedHere;
switch (Probe.Result)
{
case EVoxelConnectivityResult::Connected: ++NumAnchorConnected; break;
case EVoxelConnectivityResult::NotConnected: ++NumAnchorNotConnected; break;
case EVoxelConnectivityResult::StartCellSolid: ++NumAnchorStartCellSolid; break;
case EVoxelConnectivityResult::GoalCellSolid: ++NumAnchorGoalCellSolid; break;
case EVoxelConnectivityResult::OutOfWindow: ++NumAnchorOutOfWindow; break;
case EVoxelConnectivityResult::CoarseLied: ++NumAnchorCoarseLied; break;
default: break;
}
}
return Probe;
};
for (int32 StrateIndex = 0; StrateIndex < Layout.Num(); ++StrateIndex) for (int32 StrateIndex = 0; StrateIndex < Layout.Num(); ++StrateIndex)
{ {
const FStrateSlot& Slot = Layout[StrateIndex]; const FStrateSlot& Slot = Layout[StrateIndex];
@@ -455,22 +575,15 @@ bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters)
const FVector B(Settings.CenterXY.X + 2.0f, Settings.CenterXY.Y + 2.0f, const FVector B(Settings.CenterXY.X + 2.0f, Settings.CenterXY.Y + 2.0f,
static_cast<float>(Window.SampledMaxZ) static_cast<float>(Window.SampledMaxZ)
- 0.5f * static_cast<float>(DerivedWindowSettings.SampleStep)); - 0.5f * static_cast<float>(DerivedWindowSettings.SampleStep));
bool bCoarseLied = false; const FConnectivityProbe Probe = ProbeRoute(
const bool bConnected = VF_AreConnected( StrateIndex, A, B, /*bCountInGuard=*/true, /*bCountAsAnchorProbe=*/false,
*World.Generator, /*bCountAsArrivalDeparture=*/false);
*World.StrateManager, if (Probe.Result == EVoxelConnectivityResult::Connected
StrateIndex, && FullResolutionDirectSegmentHasSolid(*World.Generator, A, B))
A,
B,
DerivedWindowSettings,
bCoarseLied);
++NumRoutesChecked;
if (bCoarseLied) ++NumRoutesRefuted;
if (bConnected && FullResolutionDirectSegmentHasSolid(*World.Generator, A, B))
{ {
AddError(FString::Printf( AddError(FString::Printf(
TEXT("HARD FAILURE: VF_AreConnected returned true for %s while its full-resolution " TEXT("HARD FAILURE: VF_AreConnected returned CONNECTED for %s while its "
"direct walk contained a solid sample."), "full-resolution direct walk contained a solid sample."),
*ArchetypeName(Slot))); *ArchetypeName(Slot)));
bAllChecksPassed = false; bAllChecksPassed = false;
} }
@@ -498,102 +611,315 @@ bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters)
bAllChecksPassed = false; bAllChecksPassed = false;
} }
bool bWallCoarseLied = false; const FConnectivityProbe WallProbe = ProbeRoute(
const bool bWallConnected = VF_AreConnected( 0, WallA, WallB, /*bCountInGuard=*/true, /*bCountAsAnchorProbe=*/false,
*World.Generator, /*bCountAsArrivalDeparture=*/false);
*World.StrateManager, if (WallProbe.Result != EVoxelConnectivityResult::CoarseLied)
0,
WallA,
WallB,
DerivedWindowSettings,
bWallCoarseLied);
++NumRoutesChecked;
if (bWallCoarseLied) ++NumRoutesRefuted;
if (bWallConnected)
{ {
AddError(TEXT("HARD FAILURE: VF_AreConnected reported true across a full-resolution solid 3-voxel wall.")); AddError(FString::Printf(
bAllChecksPassed = false; TEXT("HARD FAILURE: the controlled coarse-connectivity lie returned %s instead of "
} "COARSE_LIED."),
if (!bWallCoarseLied) ConnectivityResultName(WallProbe.Result)));
{
AddError(TEXT("HARD FAILURE: the controlled coarse-connectivity lie did not set bOutCoarseLied."));
bAllChecksPassed = false; bAllChecksPassed = false;
} }
World.DiffLayer->Clear(); World.DiffLayer->Clear();
TArray<FPassageReport> PassageReports;
const TArray<FVoxelPassage>& Passages = World.StrateManager->GetPassages(); const TArray<FVoxelPassage>& Passages = World.StrateManager->GetPassages();
PassageReports.Reserve(Passages.Num()); TArray<int32> ArrivalPassageByStrate;
TArray<int32> DeparturePassageByStrate;
ArrivalPassageByStrate.Init(INDEX_NONE, Layout.Num());
DeparturePassageByStrate.Init(INDEX_NONE, Layout.Num());
int32 SurfaceEntryPassageIndex = INDEX_NONE;
for (int32 PassageIndex = 0; PassageIndex < Passages.Num(); ++PassageIndex) for (int32 PassageIndex = 0; PassageIndex < Passages.Num(); ++PassageIndex)
{ {
const FVoxelPassage& Passage = Passages[PassageIndex]; const FVoxelPassage& Passage = Passages[PassageIndex];
FPassageReport& Report = PassageReports.AddDefaulted_GetRef(); if (Passage.UpperStrateIndex == Passage.LowerStrateIndex)
Report.Index = PassageIndex;
Report.Name = FString::Printf(TEXT("%03d"), PassageIndex);
Report.UpperStrateIndex = Passage.UpperStrateIndex;
Report.LowerStrateIndex = Passage.LowerStrateIndex;
Report.UpperPoint = Passage.UpperPoint;
Report.LowerPoint = Passage.LowerPoint;
auto CheckEndpoint = [&](int32 EndpointStrateIndex, const FVector& Endpoint) -> FString
{ {
if (!Layout.IsValidIndex(EndpointStrateIndex)) return TEXT("invalid-strate"); if (SurfaceEntryPassageIndex == INDEX_NONE)
const FVoxelStrateMetrics& EndpointMetrics =
DerivedMetricsByIndex[EndpointStrateIndex];
if (!EndpointMetrics.bValid) return TEXT("invalid-window");
if (!IsInsideMeasuredWindow(EndpointMetrics, DerivedWindowSettings, Endpoint))
{ {
return TEXT("out-of-window"); SurfaceEntryPassageIndex = PassageIndex;
}
continue;
} }
const FStrateSlot& Slot = Layout[EndpointStrateIndex]; if (Passage.LowerStrateIndex != Passage.UpperStrateIndex + 1)
const FVector Anchor = LargestComponentAnchor( {
Slot, AddError(FString::Printf(
DerivedWindowSettings.CenterXY, TEXT("HARD FAILURE: passage %d is not between consecutive strates (%d -> %d)."),
DerivedWindowSettings.SampleStep); PassageIndex,
bool bCoarseLied = false; Passage.UpperStrateIndex,
const bool bConnected = VF_AreConnected( Passage.LowerStrateIndex));
*World.Generator, bAllChecksPassed = false;
*World.StrateManager, continue;
EndpointStrateIndex,
Endpoint,
Anchor,
DerivedWindowSettings,
bCoarseLied);
++NumRoutesChecked;
if (bCoarseLied) ++NumRoutesRefuted;
return bConnected ? TEXT("CONNECTED_TO_LARGEST") : TEXT("NOT_CONNECTED");
};
Report.UpperResult = CheckEndpoint(Passage.UpperStrateIndex, Passage.UpperPoint);
Report.LowerResult = CheckEndpoint(Passage.LowerStrateIndex, Passage.LowerPoint);
} }
PassageReports.Sort([](const FPassageReport& A, const FPassageReport& B) if (!Layout.IsValidIndex(Passage.UpperStrateIndex)
|| !Layout.IsValidIndex(Passage.LowerStrateIndex))
{ {
return A.Index < B.Index; AddError(FString::Printf(
}); TEXT("HARD FAILURE: passage %d references a strate outside the layout (%d -> %d)."),
PassageIndex,
Passage.UpperStrateIndex,
Passage.LowerStrateIndex));
bAllChecksPassed = false;
continue;
}
if (ArrivalPassageByStrate[Passage.LowerStrateIndex] != INDEX_NONE)
{
AddError(FString::Printf(
TEXT("HARD FAILURE: strate %d has more than one arrival passage."),
Passage.LowerStrateIndex));
bAllChecksPassed = false;
}
else
{
ArrivalPassageByStrate[Passage.LowerStrateIndex] = PassageIndex;
}
if (DeparturePassageByStrate[Passage.UpperStrateIndex] != INDEX_NONE)
{
AddError(FString::Printf(
TEXT("HARD FAILURE: strate %d has more than one departure passage."),
Passage.UpperStrateIndex));
bAllChecksPassed = false;
}
else
{
DeparturePassageByStrate[Passage.UpperStrateIndex] = PassageIndex;
}
}
for (int32 StrateIndex = 0; StrateIndex < Layout.Num(); ++StrateIndex)
{
if (StrateIndex > 0 && ArrivalPassageByStrate[StrateIndex] == INDEX_NONE)
{
AddError(FString::Printf(
TEXT("HARD FAILURE: non-topmost strate %d has no arrival passage."),
StrateIndex));
bAllChecksPassed = false;
}
if (StrateIndex + 1 < Layout.Num()
&& DeparturePassageByStrate[StrateIndex] == INDEX_NONE)
{
AddError(FString::Printf(
TEXT("HARD FAILURE: non-bottom-most strate %d has no departure passage."),
StrateIndex));
bAllChecksPassed = false;
}
}
TArray<FStrateConnectivityReport> StrateReports;
StrateReports.SetNum(Layout.Num());
for (int32 StrateIndex = 0; StrateIndex < Layout.Num(); ++StrateIndex)
{
FStrateConnectivityReport& Report = StrateReports[StrateIndex];
Report.Index = StrateIndex;
Report.Name = ArchetypeName(Layout[StrateIndex]);
if (ArrivalPassageByStrate[StrateIndex] != INDEX_NONE)
{
Report.bHasArrival = true;
Report.ArrivalPassageIndex = ArrivalPassageByStrate[StrateIndex];
Report.ArrivalPoint = Passages[Report.ArrivalPassageIndex].LowerPoint;
}
if (DeparturePassageByStrate[StrateIndex] != INDEX_NONE)
{
Report.bHasDeparture = true;
Report.DeparturePassageIndex = DeparturePassageByStrate[StrateIndex];
Report.DeparturePoint = Passages[Report.DeparturePassageIndex].UpperPoint;
}
}
// The actual largest-component comparison uses the representative point returned by the
// measurement API. The surface-entry lower mouth remains an auxiliary guard probe so this
// task continues to re-report the established 15 endpoint comparison probes; it is not the
// topmost strate's chain arrival.
for (FStrateConnectivityReport& Report : StrateReports)
{
const FVoxelStrateMetrics& Metrics = DerivedMetricsByIndex[Report.Index];
if (!Metrics.bValid) continue;
if (Report.bHasArrival)
{
Report.ArrivalToLargest = ProbeRoute(
Report.Index,
Report.ArrivalPoint,
Metrics.LargestComponentPoint,
/*bCountInGuard=*/true,
/*bCountAsAnchorProbe=*/true,
/*bCountAsArrivalDeparture=*/false);
}
if (Report.bHasDeparture)
{
Report.DepartureToLargest = ProbeRoute(
Report.Index,
Report.DeparturePoint,
Metrics.LargestComponentPoint,
/*bCountInGuard=*/true,
/*bCountAsAnchorProbe=*/true,
/*bCountAsArrivalDeparture=*/false);
}
}
if (SurfaceEntryPassageIndex != INDEX_NONE && Layout.IsValidIndex(0)
&& DerivedMetricsByIndex[0].bValid)
{
const FVoxelPassage& SurfaceEntry = Passages[SurfaceEntryPassageIndex];
ProbeRoute(
0,
SurfaceEntry.LowerPoint,
DerivedMetricsByIndex[0].LargestComponentPoint,
/*bCountInGuard=*/true,
/*bCountAsAnchorProbe=*/true,
/*bCountAsArrivalDeparture=*/false);
}
else
{
AddError(TEXT("HARD FAILURE: the surface-entry auxiliary endpoint probe is missing."));
bAllChecksPassed = false;
}
int32 NumArrivalDeparturePasses = 0;
TArray<FString> FailedStrates;
for (FStrateConnectivityReport& Report : StrateReports)
{
if (!Report.bHasArrival || !Report.bHasDeparture)
{
continue;
}
Report.ArrivalToDeparture = ProbeRoute(
Report.Index,
Report.ArrivalPoint,
Report.DeparturePoint,
/*bCountInGuard=*/false,
/*bCountAsAnchorProbe=*/false,
/*bCountAsArrivalDeparture=*/true);
if (Report.ArrivalToDeparture.Result == EVoxelConnectivityResult::Connected)
{
++NumArrivalDeparturePasses;
}
else
{
const FString ResultText = ConnectivityProbeText(Report.ArrivalToDeparture);
FailedStrates.Add(FString::Printf(
TEXT("%d:%s (%s)"),
Report.Index,
*Report.Name,
*ResultText));
}
}
FString FailedStrateText = TEXT("none");
if (FailedStrates.Num() > 0)
{
FailedStrateText = FString::Join(FailedStrates, TEXT(", "));
}
Summary += FString::Printf( Summary += FString::Printf(
TEXT("Connectivity guard (derived window): %d routes checked, %d coarse routes refuted at full resolution.\n"), TEXT("Coarse-lie guard (8 interior routes + wall control + 15 endpoint->largest probes): "
"%d routes checked, %d coarse routes refuted at full resolution.\n"),
NumGuardRoutesChecked,
NumGuardRoutesRefuted);
Summary += FString::Printf(
TEXT("All connectivity probes (including arrival->departure law queries): %d routes "
"checked, %d coarse routes refuted at full resolution.\n"),
NumRoutesChecked, NumRoutesChecked,
NumRoutesRefuted); NumRoutesRefuted);
Summary += TEXT("Passages (derived window; each endpoint -> its strate's largest-component spine anchor, sorted):\n");
for (const FPassageReport& Report : PassageReports)
{
Summary += FString::Printf( Summary += FString::Printf(
TEXT(" passage %s: UpperPoint strate=%d (%.3f,%.3f,%.3f) => %s; " TEXT("Endpoint snap repair: %d snapped endpoint events across all probes; %d in "
"LowerPoint strate=%d (%.3f,%.3f,%.3f) => %s\n"), "%d arrival->departure probes.\n"),
NumSnappedEndpointEvents,
NumArrivalDepartureSnappedEndpointEvents,
NumArrivalDepartureRoutesChecked);
Summary += FString::Printf(
TEXT("Correct largest-component endpoint probes (%d, including the surface-entry auxiliary): "
"connected=%d, not-connected=%d, start-cell-solid=%d, goal-cell-solid=%d, "
"out-of-window=%d, coarse-lied=%d, snapped endpoint events=%d.\n"),
NumAnchorProbes,
NumAnchorConnected,
NumAnchorNotConnected,
NumAnchorStartCellSolid,
NumAnchorGoalCellSolid,
NumAnchorOutOfWindow,
NumAnchorCoarseLied,
NumAnchorSnappedEndpointEvents);
Summary += TEXT("Previous \"5 of 15 disconnected\" verdict: ARTIFACT as a claim about sealed "
"pockets; it used a guessed anchor and asked the wrong (largest-component) law.\n");
Summary += FString::Printf(
TEXT("arrival->departure: %d of %d applicable strates pass; failures: %s.\n"),
NumArrivalDeparturePasses,
NumArrivalDepartureRoutesChecked,
*FailedStrateText);
Summary += TEXT("Strates (chain order; derived window; largest point is the deterministic "
"lowest-cell representative):\n");
Summary += TEXT(" Strate | arrival->departure | arrival->largest | departure->largest | components | largest share | largest cells | >=1% | largest point\n");
for (const FStrateConnectivityReport& Report : StrateReports)
{
const FVoxelStrateMetrics& Metrics = DerivedMetricsByIndex[Report.Index];
FString ArrivalToDeparture;
if (Report.bHasArrival && Report.bHasDeparture)
{
ArrivalToDeparture = ConnectivityProbeText(Report.ArrivalToDeparture);
}
else if (!Report.bHasArrival && Report.Index == 0)
{
ArrivalToDeparture = TEXT("N/A (topmost; no arrival)");
}
else if (!Report.bHasDeparture && Report.Index + 1 == StrateReports.Num())
{
ArrivalToDeparture = TEXT("N/A (bottom-most; no departure)");
}
else
{
ArrivalToDeparture = TEXT("MISSING_CHAIN_MOUTH");
}
FString ArrivalToLargest;
if (Report.bHasArrival)
{
ArrivalToLargest = ConnectivityProbeText(Report.ArrivalToLargest);
}
else if (Report.Index == 0)
{
ArrivalToLargest = TEXT("N/A (topmost; no arrival)");
}
else
{
ArrivalToLargest = TEXT("MISSING_ARRIVAL");
}
FString DepartureToLargest;
if (Report.bHasDeparture)
{
DepartureToLargest = ConnectivityProbeText(Report.DepartureToLargest);
}
else if (Report.Index + 1 == StrateReports.Num())
{
DepartureToLargest = TEXT("N/A (bottom-most; no departure)");
}
else
{
DepartureToLargest = TEXT("MISSING_DEPARTURE");
}
Summary += FString::Printf(
TEXT(" %d %s | %s | %s | %s | %d | %.9g | %lld | %d | (%.3f,%.3f,%.3f)\n"),
Report.Index,
*Report.Name, *Report.Name,
Report.UpperStrateIndex, *ArrivalToDeparture,
Report.UpperPoint.X, *ArrivalToLargest,
Report.UpperPoint.Y, *DepartureToLargest,
Report.UpperPoint.Z, Metrics.NumAirComponents,
*Report.UpperResult, Metrics.LargestComponentShare,
Report.LowerStrateIndex, Metrics.LargestComponentCells,
Report.LowerPoint.X, Metrics.NumComponentsAtLeast1Pct,
Report.LowerPoint.Y, Metrics.LargestComponentPoint.X,
Report.LowerPoint.Z, Metrics.LargestComponentPoint.Y,
*Report.LowerResult); Metrics.LargestComponentPoint.Z);
} }
AddInfo(Summary); AddInfo(Summary);
+156 -16
View File
@@ -298,7 +298,10 @@ namespace VoxelStrateMeasurePrivate
const FSampleGrid& Grid, const FSampleGrid& Grid,
TArray<int32>& OutComponents, TArray<int32>& OutComponents,
int32& OutNumComponents, int32& OutNumComponents,
int32& OutLargestComponentCells) int64 NumAir,
int64& OutLargestComponentCells,
int32& OutLargestComponentLowestCell,
int32& OutNumComponentsAtLeast1Pct)
{ {
OutComponents.SetNumUninitialized(Grid.CellCount); OutComponents.SetNumUninitialized(Grid.CellCount);
for (int32 Index = 0; Index < Grid.CellCount; ++Index) for (int32 Index = 0; Index < Grid.CellCount; ++Index)
@@ -310,6 +313,8 @@ namespace VoxelStrateMeasurePrivate
Queue.SetNumUninitialized(Grid.CellCount); Queue.SetNumUninitialized(Grid.CellCount);
OutNumComponents = 0; OutNumComponents = 0;
OutLargestComponentCells = 0; OutLargestComponentCells = 0;
OutLargestComponentLowestCell = INDEX_NONE;
OutNumComponentsAtLeast1Pct = 0;
for (int32 Start = 0; Start < Grid.CellCount; ++Start) for (int32 Start = 0; Start < Grid.CellCount; ++Start)
{ {
@@ -353,7 +358,20 @@ namespace VoxelStrateMeasurePrivate
if (Z > 0) Visit(Grid.Index(X, Y, Z - 1)); if (Z > 0) Visit(Grid.Index(X, Y, Z - 1));
} }
OutLargestComponentCells = FMath::Max(OutLargestComponentCells, ComponentCells); const int64 ComponentCells64 = static_cast<int64>(ComponentCells);
if (NumAir > 0 && ComponentCells64 * 100 >= NumAir)
{
++OutNumComponentsAtLeast1Pct;
}
if (ComponentCells64 > OutLargestComponentCells
|| (ComponentCells64 == OutLargestComponentCells
&& (OutLargestComponentLowestCell == INDEX_NONE
|| Start < OutLargestComponentLowestCell)))
{
OutLargestComponentCells = ComponentCells64;
OutLargestComponentLowestCell = Start;
}
} }
} }
@@ -429,8 +447,6 @@ namespace VoxelStrateMeasurePrivate
{ {
TArray<int32> Components; TArray<int32> Components;
int32 NumComponents = 0; int32 NumComponents = 0;
int32 LargestComponentCells = 0;
FloodFillAir(Grid, Components, NumComponents, LargestComponentCells);
int64 NumAir = 0; int64 NumAir = 0;
for (const uint8 bAir : Grid.Air) for (const uint8 bAir : Grid.Air)
@@ -438,16 +454,38 @@ namespace VoxelStrateMeasurePrivate
NumAir += bAir != 0u ? 1 : 0; NumAir += bAir != 0u ? 1 : 0;
} }
int64 LargestComponentCells = 0;
int32 LargestComponentLowestCell = INDEX_NONE;
int32 NumComponentsAtLeast1Pct = 0;
FloodFillAir(
Grid,
Components,
NumComponents,
NumAir,
LargestComponentCells,
LargestComponentLowestCell,
NumComponentsAtLeast1Pct);
InOutMetrics.NumSampled = Grid.CellCount; InOutMetrics.NumSampled = Grid.CellCount;
InOutMetrics.NumAir = NumAir; InOutMetrics.NumAir = NumAir;
InOutMetrics.NumSolid = static_cast<int64>(Grid.CellCount) - NumAir; InOutMetrics.NumSolid = static_cast<int64>(Grid.CellCount) - NumAir;
InOutMetrics.AirFraction = static_cast<float>( InOutMetrics.AirFraction = static_cast<float>(
static_cast<double>(NumAir) / static_cast<double>(Grid.CellCount)); static_cast<double>(NumAir) / static_cast<double>(Grid.CellCount));
InOutMetrics.NumAirComponents = NumComponents; InOutMetrics.NumAirComponents = NumComponents;
InOutMetrics.LargestComponentCells = LargestComponentCells;
InOutMetrics.NumComponentsAtLeast1Pct = NumComponentsAtLeast1Pct;
InOutMetrics.LargestComponentShare = NumAir > 0 InOutMetrics.LargestComponentShare = NumAir > 0
? static_cast<float>(static_cast<double>(LargestComponentCells) ? static_cast<float>(static_cast<double>(LargestComponentCells)
/ static_cast<double>(NumAir)) / static_cast<double>(NumAir))
: 0.0f; : 0.0f;
if (NumAir > 0 && LargestComponentLowestCell != INDEX_NONE)
{
int32 LargestX = 0;
int32 LargestY = 0;
int32 LargestZ = 0;
DecodeIndex(Grid, LargestComponentLowestCell, LargestX, LargestY, LargestZ);
InOutMetrics.LargestComponentPoint = Grid.CellCenter(LargestX, LargestY, LargestZ);
}
TArray<int32> ClearanceValues; TArray<int32> ClearanceValues;
ClearanceValues.Reserve(static_cast<int32>(NumAir)); ClearanceValues.Reserve(static_cast<int32>(NumAir));
@@ -537,6 +575,93 @@ namespace VoxelStrateMeasurePrivate
return true; return true;
} }
enum class EEndpointCellResult : uint8
{
OutOfWindow,
Air,
Solid
};
EEndpointCellResult ResolveEndpointCell(
const FSampleGrid& Grid,
const FVector& Point,
int32& OutCell,
bool& bOutSnapped)
{
bOutSnapped = false;
int32 OriginalCell = -1;
if (!FindCellForPoint(Grid, Point, OriginalCell))
{
return EEndpointCellResult::OutOfWindow;
}
if (Grid.Air[OriginalCell] != 0u)
{
OutCell = OriginalCell;
return EEndpointCellResult::Air;
}
int32 OriginalX = 0;
int32 OriginalY = 0;
int32 OriginalZ = 0;
DecodeIndex(Grid, OriginalCell, OriginalX, OriginalY, OriginalZ);
int32 BestCell = INDEX_NONE;
float BestDistanceSquared = FLT_MAX;
const int32 MaxX = Grid.NumX - 1;
const int32 MaxY = Grid.NumY - 1;
const int32 MaxZ = Grid.NumZ - 1;
const int32 MinX = OriginalX > 0 ? OriginalX - 1 : 0;
const int32 MinY = OriginalY > 0 ? OriginalY - 1 : 0;
const int32 MinZ = OriginalZ > 0 ? OriginalZ - 1 : 0;
const int32 CandidateMaxX = OriginalX < MaxX ? OriginalX + 1 : MaxX;
const int32 CandidateMaxY = OriginalY < MaxY ? OriginalY + 1 : MaxY;
const int32 CandidateMaxZ = OriginalZ < MaxZ ? OriginalZ + 1 : MaxZ;
for (int32 Z = MinZ;
Z <= CandidateMaxZ;
++Z)
{
for (int32 Y = MinY;
Y <= CandidateMaxY;
++Y)
{
for (int32 X = MinX;
X <= CandidateMaxX;
++X)
{
if (X == OriginalX && Y == OriginalY && Z == OriginalZ)
{
continue;
}
const int32 Candidate = Grid.Index(X, Y, Z);
if (Grid.Air[Candidate] == 0u)
{
continue;
}
const float DistanceSquared = FVector::DistSquared(
Point, Grid.CellCenter(X, Y, Z));
if (DistanceSquared < BestDistanceSquared
|| (DistanceSquared == BestDistanceSquared
&& (BestCell == INDEX_NONE || Candidate < BestCell)))
{
BestDistanceSquared = DistanceSquared;
BestCell = Candidate;
}
}
}
}
if (BestCell == INDEX_NONE)
{
return EEndpointCellResult::Solid;
}
OutCell = BestCell;
bOutSnapped = true;
return EEndpointCellResult::Air;
}
bool FullResolutionAirOnSegment( bool FullResolutionAirOnSegment(
const UVoxelGenerator& Generator, const UVoxelGenerator& Generator,
const FVector& Start, const FVector& Start,
@@ -706,20 +831,22 @@ FVoxelStrateMetrics VF_MeasureStrate(
return Result; return Result;
} }
bool VF_AreConnected( EVoxelConnectivityResult VF_AreConnected(
const UVoxelGenerator& Generator, const UVoxelGenerator& Generator,
const UVoxelStrateManager& Manager, const UVoxelStrateManager& Manager,
int32 StrateIndex, int32 StrateIndex,
const FVector& AVoxel, const FVector& AVoxel,
const FVector& BVoxel, const FVector& BVoxel,
const FVoxelStrateMeasureSettings& Settings, const FVoxelStrateMeasureSettings& Settings,
bool& bOutCoarseLied) bool& bOutStartSnapped,
bool& bOutGoalSnapped)
{ {
bOutCoarseLied = false; bOutStartSnapped = false;
bOutGoalSnapped = false;
if (!FMath::IsFinite(AVoxel.X) || !FMath::IsFinite(AVoxel.Y) || !FMath::IsFinite(AVoxel.Z) if (!FMath::IsFinite(AVoxel.X) || !FMath::IsFinite(AVoxel.Y) || !FMath::IsFinite(AVoxel.Z)
|| !FMath::IsFinite(BVoxel.X) || !FMath::IsFinite(BVoxel.Y) || !FMath::IsFinite(BVoxel.Z)) || !FMath::IsFinite(BVoxel.X) || !FMath::IsFinite(BVoxel.Y) || !FMath::IsFinite(BVoxel.Z))
{ {
return false; return EVoxelConnectivityResult::OutOfWindow;
} }
VoxelStrateMeasurePrivate::FSampleGrid Grid; VoxelStrateMeasurePrivate::FSampleGrid Grid;
@@ -727,21 +854,35 @@ bool VF_AreConnected(
if (!VoxelStrateMeasurePrivate::BuildSampleGrid( if (!VoxelStrateMeasurePrivate::BuildSampleGrid(
Generator, Manager, StrateIndex, Settings, Grid, RefusalReason)) Generator, Manager, StrateIndex, Settings, Grid, RefusalReason))
{ {
return false; return EVoxelConnectivityResult::OutOfWindow;
} }
int32 Start = -1; int32 Start = -1;
int32 Goal = -1; int32 Goal = -1;
if (!VoxelStrateMeasurePrivate::FindCellForPoint(Grid, AVoxel, Start) const VoxelStrateMeasurePrivate::EEndpointCellResult StartCell =
|| !VoxelStrateMeasurePrivate::FindCellForPoint(Grid, BVoxel, Goal)) VoxelStrateMeasurePrivate::ResolveEndpointCell(
Grid, AVoxel, Start, bOutStartSnapped);
const VoxelStrateMeasurePrivate::EEndpointCellResult GoalCell =
VoxelStrateMeasurePrivate::ResolveEndpointCell(
Grid, BVoxel, Goal, bOutGoalSnapped);
if (StartCell == VoxelStrateMeasurePrivate::EEndpointCellResult::OutOfWindow
|| GoalCell == VoxelStrateMeasurePrivate::EEndpointCellResult::OutOfWindow)
{ {
return false; return EVoxelConnectivityResult::OutOfWindow;
}
if (StartCell == VoxelStrateMeasurePrivate::EEndpointCellResult::Solid)
{
return EVoxelConnectivityResult::StartCellSolid;
}
if (GoalCell == VoxelStrateMeasurePrivate::EEndpointCellResult::Solid)
{
return EVoxelConnectivityResult::GoalCellSolid;
} }
TArray<int32> Path; TArray<int32> Path;
if (!VoxelStrateMeasurePrivate::FindCoarsePath(Grid, Start, Goal, Path)) if (!VoxelStrateMeasurePrivate::FindCoarsePath(Grid, Start, Goal, Path))
{ {
return false; return EVoxelConnectivityResult::NotConnected;
} }
// The coarse route is evidence, not a pass. Re-check this one BFS route at one-voxel spacing // The coarse route is evidence, not a pass. Re-check this one BFS route at one-voxel spacing
@@ -749,8 +890,7 @@ bool VF_AreConnected(
if (!VoxelStrateMeasurePrivate::FullResolutionPathIsAir( if (!VoxelStrateMeasurePrivate::FullResolutionPathIsAir(
Generator, Grid, AVoxel, BVoxel, Path)) Generator, Grid, AVoxel, BVoxel, Path))
{ {
bOutCoarseLied = true; return EVoxelConnectivityResult::CoarseLied;
return false;
} }
return true; return EVoxelConnectivityResult::Connected;
} }
+21 -4
View File
@@ -7,6 +7,17 @@
class UVoxelGenerator; class UVoxelGenerator;
class UVoxelStrateManager; class UVoxelStrateManager;
/** Explicit outcome of a coarse connectivity query plus its full-resolution route re-check. */
enum class EVoxelConnectivityResult : uint8
{
Connected,
NotConnected,
StartCellSolid,
GoalCellSolid,
OutOfWindow,
CoarseLied
};
/** Settings for the deterministic coarse grid used by the strate measurement pass. */ /** Settings for the deterministic coarse grid used by the strate measurement pass. */
struct VOXELFORGE_API FVoxelStrateMeasureSettings struct VOXELFORGE_API FVoxelStrateMeasureSettings
{ {
@@ -31,6 +42,9 @@ struct VOXELFORGE_API FVoxelStrateMetrics
float AirFraction = 0.f; float AirFraction = 0.f;
int32 NumAirComponents = 0; int32 NumAirComponents = 0;
float LargestComponentShare = 0.f; float LargestComponentShare = 0.f;
FVector LargestComponentPoint = FVector::ZeroVector; // Actor-space voxel coords; valid iff NumAir > 0.
int64 LargestComponentCells = 0;
int32 NumComponentsAtLeast1Pct = 0;
float WalkableFraction = 0.f; float WalkableFraction = 0.f;
float MedianFeatureScale = 0.f; float MedianFeatureScale = 0.f;
int32 MedianVerticalClearance = 0; int32 MedianVerticalClearance = 0;
@@ -57,14 +71,17 @@ VOXELFORGE_API FVoxelStrateMetrics VF_MeasureStrate(
/** /**
* Test coarse connectivity and then re-check the one recovered coarse route at full resolution. * Test coarse connectivity and then re-check the one recovered coarse route at full resolution.
* *
* bOutCoarseLied is true only when the coarse route existed but a full-resolution sample on that * If an endpoint's own coarse cell is solid, the query searches the 26 neighbouring cells and
* route was solid (or non-finite), in which case this function always returns false. * uses the nearest air cell when one exists. The snap flags report those repairs. Solid-cell
* results are returned only when no adjacent air cell exists. A coarse route that fails its
* full-resolution re-check returns CoarseLied.
*/ */
VOXELFORGE_API bool VF_AreConnected( VOXELFORGE_API EVoxelConnectivityResult VF_AreConnected(
const UVoxelGenerator& Generator, const UVoxelGenerator& Generator,
const UVoxelStrateManager& Manager, const UVoxelStrateManager& Manager,
int32 StrateIndex, int32 StrateIndex,
const FVector& AVoxel, const FVector& AVoxel,
const FVector& BVoxel, const FVector& BVoxel,
const FVoxelStrateMeasureSettings& Settings, const FVoxelStrateMeasureSettings& Settings,
bool& bOutCoarseLied); bool& bOutStartSnapped,
bool& bOutGoalSnapped);