feat(measure): one grid, one flood fill -- the strate measurement pass

Tier 2 slice 1 from COMPOSER-NOTES.md 3.4. A read-only, deterministic,
headless measurement of a built strate: sample a coarse grid once,
flood-fill the air once, derive every metric from that same grid.

What it answers today: air fraction, air component count and largest
component share (the swiss-cheese signal), walkable fraction, median
feature scale, median vertical clearance -- and whether two points are
actually connected.

The connectivity verdict is the point. Coarse sampling LIES about it in
the dangerous direction: a wall thinner than the sample step vanishes and
two sealed caverns read as joined. So VF_AreConnected recovers the actual
BFS route and re-walks that one corridor at one-voxel spacing; a true
verdict is never returned on coarse evidence alone. It fired: 2 of 21
routes were coarse false positives.

The test carries a negative control, because a green suite that cannot
fail is what hid the last defect. Two of them, in fact: a direct probe of
the known-solid inter-strate gap (AirFraction 0.00042), and -- Codex's
improvement on the spec -- a diff-layer box filled solid and measured
through VF_MeasureStrate itself, so an inverted sign is caught end to end
rather than around the function under test.

Build clean, 18 succeeded / 0 warnings / 0 failed.

KNOWN, fix in flight: the interior window excludes a whole chunk (32
voxels) at each end to avoid a 4-voxel seal band -- 8x too much. On the
fixture's 4-chunk strates that discards the floor, so the reported
FlatPlain/CrystalChamber air fractions of 0.999 describe the open band
above the floor, not the strate. The engine is right; the window is not.

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 13:20:42 +02:00
1 parent 8cd5face9b
commit 6988e5ace1
4 files changed
+1268

No files matched your search

+13
View File
@@ -302,6 +302,19 @@ This is **where terrain shape lives.**
> tile by the mesher drops tail octaves on coarse tiles (opt-in `LODOctaveDrop`, default 0 = off); > tile by the mesher drops tail octaves on coarse tiles (opt-in `LODOctaveDrop`, default 0 = off);
> XY-field noise (heightfield/ceiling/relief/moisture) deliberately stays un-biased. §8.10. > XY-field noise (heightfield/ceiling/relief/moisture) deliberately stays un-biased. §8.10.
### 3.6b Strate measurement — `Public/VoxelStrateMeasure.h` + `Private/VoxelStrateMeasure.cpp`
Headless, read-only Tier 2 measurement pass. `VF_MeasureStrate` samples one strate's strict
interior into a bounded coarse grid, performs one deterministic 6-connected air flood fill, and
derives fractions, components, walkability, feature scale, and clearance from that grid.
`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
`bOutCoarseLied`. No UObject state, cache, actor, world, or PIE is required.
| Symbol | Role |
|--------|------|
| `FVoxelStrateMeasureSettings` / `FVoxelStrateMetrics` | Plain settings/result structs for bounded strate sampling and derived measurements. |
| `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. |
### 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.
@@ -0,0 +1,506 @@
// VoxelForgeStrateConnectivityTest.cpp
// Tier 2: the measurement pass and its coarse-connectivity false-positive guard.
#if WITH_DEV_AUTOMATION_TESTS
#include "Misc/AutomationTest.h"
#include "VoxelForgeTestFixture.h"
#include "VoxelStrateMeasure.h"
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FVoxelForgeStrateConnectivityTest,
"VoxelForge.Generation.StrateConnectivity",
EAutomationTestFlags_ApplicationContextMask | EAutomationTestFlags::EngineFilter)
namespace
{
struct FArchetypeReport
{
FString Name;
int32 Index = INDEX_NONE;
FVoxelStrateMetrics First;
FVoxelStrateMetrics Second;
};
struct FPassageReport
{
int32 Index = INDEX_NONE;
FString Name;
FString UpperResult;
FString LowerResult;
};
FString ArchetypeName(const FStrateSlot& Slot)
{
if (Slot.Definition != nullptr)
{
if (const UEnum* ArchetypeEnum = StaticEnum<ECaveGeneratorType>())
{
return ArchetypeEnum->GetNameStringByValue(
static_cast<int64>(Slot.Definition->GeneratorType));
}
}
return FString::Printf(TEXT("Strate_%d"), Slot.StrateIndex);
}
bool SameFloatBits(float A, float B)
{
return FMemory::Memcmp(&A, &B, sizeof(float)) == 0;
}
bool MetricsAreBitIdentical(const FVoxelStrateMetrics& A, const FVoxelStrateMetrics& B)
{
return A.bValid == B.bValid
&& A.RefusalReason == B.RefusalReason
&& A.NumSampled == B.NumSampled
&& A.NumAir == B.NumAir
&& A.NumSolid == B.NumSolid
&& SameFloatBits(A.AirFraction, B.AirFraction)
&& A.NumAirComponents == B.NumAirComponents
&& SameFloatBits(A.LargestComponentShare, B.LargestComponentShare)
&& SameFloatBits(A.WalkableFraction, B.WalkableFraction)
&& SameFloatBits(A.MedianFeatureScale, B.MedianFeatureScale)
&& A.MedianVerticalClearance == B.MedianVerticalClearance;
}
bool IsStrictInteriorPoint(const FStrateSlot& Slot, const FVector& Point)
{
const float MinZ = (static_cast<float>(Slot.BottomChunkZ) + 1.0f) * CHUNK_SIZE;
const float MaxZ = static_cast<float>(Slot.TopChunkZ) * CHUNK_SIZE;
return Point.Z >= MinZ && Point.Z < MaxZ;
}
FVector LargestComponentAnchor(const FStrateSlot& Slot, const FVector2D& CenterXY)
{
// The fixture's origin spine is the deterministic hub/void anchor. The measurement API
// 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.
const float AnchorZ = (static_cast<float>(Slot.BottomChunkZ) + 1.0f) * CHUNK_SIZE + 2.0f;
return FVector(CenterXY.X + 2.0f, CenterXY.Y + 2.0f, AnchorZ);
}
bool FullResolutionDirectSegmentHasSolid(
const UVoxelGenerator& Generator,
const FVector& A,
const FVector& B)
{
const FVector Delta = B - A;
const float MaxDelta = FMath::Max3(
FMath::Abs(Delta.X), FMath::Abs(Delta.Y), FMath::Abs(Delta.Z));
const int32 NumSteps = FMath::Max(1, FMath::CeilToInt(MaxDelta));
for (int32 Step = 0; Step <= NumSteps; ++Step)
{
const float Alpha = static_cast<float>(Step) / static_cast<float>(NumSteps);
const FVector Sample = A + Delta * Alpha;
const float Density = Generator.GetDensityAt(Sample.X, Sample.Y, Sample.Z);
if (!FMath::IsFinite(Density) || !(Density > 0.0f))
{
return true;
}
}
return false;
}
bool FindFirstGap(
const TArray<FStrateSlot>& Layout,
int32& OutGapTopChunkZ,
int32& OutGapBottomChunkZ)
{
for (int32 Index = 0; Index + 1 < Layout.Num(); ++Index)
{
const int32 GapTop = Layout[Index].BottomChunkZ - 1;
const int32 GapBottom = Layout[Index + 1].TopChunkZ + 1;
if (GapTop >= GapBottom)
{
OutGapTopChunkZ = GapTop;
OutGapBottomChunkZ = GapBottom;
return true;
}
}
return false;
}
float MeasureGapAirFraction(
const UVoxelGenerator& Generator,
const UVoxelStrateManager& Manager,
int32 GapTopChunkZ,
int32 GapBottomChunkZ,
const FVoxelStrateMeasureSettings& Settings,
int64& OutSampleCount,
int64& OutAirCount)
{
OutSampleCount = 0;
OutAirCount = 0;
const float MinX = Settings.CenterXY.X - static_cast<float>(Settings.RadiusInVoxels);
const float MinY = Settings.CenterXY.Y - static_cast<float>(Settings.RadiusInVoxels);
const float Step = static_cast<float>(Settings.SampleStep);
const int32 NumXY = FMath::CeilToInt(
(2.0f * static_cast<float>(Settings.RadiusInVoxels)) / Step);
const int32 MinGapChunkZ = GapBottomChunkZ;
const int32 MaxGapChunkZ = GapTopChunkZ;
for (int32 Z = MinGapChunkZ * CHUNK_SIZE + Settings.SampleStep / 2;
Z < (MaxGapChunkZ + 1) * CHUNK_SIZE;
Z += Settings.SampleStep)
{
for (int32 YIndex = 0; YIndex < NumXY; ++YIndex)
{
const float Y = FMath::Min(
MinY + (static_cast<float>(YIndex) + 0.5f) * Step,
Settings.CenterXY.Y + static_cast<float>(Settings.RadiusInVoxels) - 0.5f);
for (int32 XIndex = 0; XIndex < NumXY; ++XIndex)
{
const float X = FMath::Min(
MinX + (static_cast<float>(XIndex) + 0.5f) * Step,
Settings.CenterXY.X + static_cast<float>(Settings.RadiusInVoxels) - 0.5f);
const int32 ChunkZ = FMath::FloorToInt(static_cast<float>(Z) / CHUNK_SIZE);
if (!Manager.IsGapChunk(FIntVector(
FMath::FloorToInt(X / CHUNK_SIZE),
FMath::FloorToInt(Y / CHUNK_SIZE),
ChunkZ)))
{
continue;
}
const float Density = Generator.GetDensityAt(X, Y, static_cast<float>(Z));
++OutSampleCount;
if (FMath::IsFinite(Density) && Density > 0.0f)
{
++OutAirCount;
}
}
}
}
return OutSampleCount > 0
? static_cast<float>(static_cast<double>(OutAirCount)
/ static_cast<double>(OutSampleCount))
: 0.0f;
}
}
bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters)
{
using namespace VoxelForgeTest;
FTestWorld World;
World.Build(/*InSeed=*/1337, /*InGapChunks=*/2);
if (!World.IsValid())
{
AddError(World.WhyInvalid());
return false;
}
FVoxelStrateMeasureSettings Settings;
Settings.SampleStep = 4;
Settings.RadiusInVoxels = 256;
Settings.CenterXY = FVector2D::ZeroVector;
Settings.MaxCells = 8000000;
Settings.HeadroomCells = 2;
bool bAllChecksPassed = true;
FString Summary = TEXT("VoxelForge strate measurement summary (seed 1337, step 4, radius 256):\n");
/*
* HARD FAILURES — these are the reasons this test exists:
*
* 1. Negative control: the inter-strate gap is bedrock. If it reports mostly air, the density
* sign is inverted and every other number in this file is meaningless.
* 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.
* 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
* solid sample; the controlled 3-voxel wall below must make bOutCoarseLied fire.
*/
int32 GapTopChunkZ = 0;
int32 GapBottomChunkZ = 0;
int64 GapSamples = 0;
int64 GapAirSamples = 0;
const bool bHaveGap = FindFirstGap(
World.StrateManager->GetLayout(), GapTopChunkZ, GapBottomChunkZ);
float GapAirFraction = 0.0f;
if (!bHaveGap)
{
AddError(TEXT("HARD FAILURE: the fixture has no inter-strate gap for the density-sign negative control."));
bAllChecksPassed = false;
}
else
{
GapAirFraction = MeasureGapAirFraction(
*World.Generator,
*World.StrateManager,
GapTopChunkZ,
GapBottomChunkZ,
Settings,
GapSamples,
GapAirSamples);
if (GapSamples == 0)
{
AddError(TEXT("HARD FAILURE: the inter-strate gap negative control sampled zero cells."));
bAllChecksPassed = false;
}
if (GapAirFraction > 0.05f)
{
AddError(FString::Printf(
TEXT("HARD FAILURE: density sign is inverted — the known-solid inter-strate gap "
"reported AirFraction %.9g (mostly air), and every other number in this file "
"is meaningless."),
GapAirFraction));
bAllChecksPassed = false;
}
}
// This second control runs through VF_MeasureStrate itself. It fills the complete sampled
// box of the first strate, then requires that the measurement reports almost no air. It is
// deliberately separate from the gap probe above: this catches a sign inversion in the new
// measurement pass even though the generator-level gap probe uses the correct MC predicate.
const FStrateSlot& SolidControlSlot = World.StrateManager->GetLayout()[0];
const int32 SolidControlMinZ = (SolidControlSlot.BottomChunkZ + 1) * CHUNK_SIZE;
const int32 SolidControlMaxZ = SolidControlSlot.TopChunkZ * CHUNK_SIZE;
FVoxelModification SolidControl;
SolidControl.Shape = EVoxelBrushShape::Box;
SolidControl.Center = FVector(
Settings.CenterXY.X,
Settings.CenterXY.Y,
static_cast<float>(SolidControlMinZ + SolidControlMaxZ) * 0.5f);
SolidControl.BoxExtent = FVector(
static_cast<float>(Settings.RadiusInVoxels),
static_cast<float>(Settings.RadiusInVoxels),
static_cast<float>(Settings.RadiusInVoxels));
SolidControl.Radius = 1.0f;
SolidControl.Falloff = 0.1f;
SolidControl.Strength = 100.0f;
const TArray<FIntVector> SolidControlChunks = World.DiffLayer->ApplyModification(SolidControl);
const FVoxelStrateMetrics SolidControlMetrics = VF_MeasureStrate(
*World.Generator, *World.StrateManager, 0, Settings);
if (SolidControlChunks.Num() == 0 || !SolidControlMetrics.bValid
|| SolidControlMetrics.AirFraction > 0.05f)
{
AddError(FString::Printf(
TEXT("HARD FAILURE: VF_MeasureStrate classified the known-solid control as air "
"(AirFraction %.9g); the density sign is inverted and every other number in "
"this file is meaningless."),
SolidControlMetrics.AirFraction));
bAllChecksPassed = false;
}
World.DiffLayer->Clear();
TArray<FArchetypeReport> ArchetypeReports;
const TArray<FStrateSlot>& Layout = World.StrateManager->GetLayout();
ArchetypeReports.Reserve(Layout.Num());
for (int32 StrateIndex = 0; StrateIndex < Layout.Num(); ++StrateIndex)
{
FArchetypeReport& Report = ArchetypeReports.AddDefaulted_GetRef();
Report.Index = StrateIndex;
Report.Name = ArchetypeName(Layout[StrateIndex]);
Report.First = VF_MeasureStrate(
*World.Generator, *World.StrateManager, StrateIndex, Settings);
Report.Second = VF_MeasureStrate(
*World.Generator, *World.StrateManager, StrateIndex, Settings);
const FVoxelStrateMetrics& Metrics = Report.First;
if (!Metrics.bValid)
{
AddError(FString::Printf(
TEXT("HARD FAILURE: %s measurement refused: %s."),
*Report.Name,
*Metrics.RefusalReason));
bAllChecksPassed = false;
}
if (Metrics.NumSampled == 0 || Metrics.NumAir == 0 || Metrics.NumSolid == 0)
{
AddError(FString::Printf(
TEXT("HARD FAILURE: vacuous %s measurement — sampled=%lld, air=%lld, solid=%lld."),
*Report.Name,
Metrics.NumSampled,
Metrics.NumAir,
Metrics.NumSolid));
bAllChecksPassed = false;
}
if (!FMath::IsFinite(Metrics.AirFraction)
|| Metrics.AirFraction < 0.0f || Metrics.AirFraction > 1.0f
|| !FMath::IsFinite(Metrics.LargestComponentShare)
|| Metrics.LargestComponentShare < 0.0f || Metrics.LargestComponentShare > 1.0f
|| !FMath::IsFinite(Metrics.WalkableFraction)
|| Metrics.WalkableFraction < 0.0f || Metrics.WalkableFraction > 1.0f
|| (Metrics.NumAir > 0 && Metrics.NumAirComponents == 0)
|| (Metrics.LargestComponentShare == 0.0f && Metrics.AirFraction > 0.05f))
{
AddError(FString::Printf(
TEXT("HARD FAILURE: range/component sanity failed for %s."), *Report.Name));
bAllChecksPassed = false;
}
if (!MetricsAreBitIdentical(Report.First, Report.Second))
{
AddError(FString::Printf(
TEXT("HARD FAILURE: %s measurement was not bit-identical on the second call."),
*Report.Name));
bAllChecksPassed = false;
}
}
ArchetypeReports.Sort([](const FArchetypeReport& A, const FArchetypeReport& B)
{
if (A.Name == B.Name) return A.Index < B.Index;
return A.Name < B.Name;
});
Summary += FString::Printf(
TEXT("Negative control: gap chunks [%d..%d], AirFraction=%.9g (%lld/%lld samples air).\n"),
GapTopChunkZ,
GapBottomChunkZ,
GapAirFraction,
GapAirSamples,
GapSamples);
Summary += TEXT("Archetypes (sorted):\n");
for (const FArchetypeReport& Report : ArchetypeReports)
{
const FVoxelStrateMetrics& Metrics = Report.First;
Summary += FString::Printf(
TEXT(" %s: Air=%.9g, Largest=%.9g, Walkable=%.9g, FeatureScale=%.9g vox, "
"Clearance=%d vox, Components=%d, Samples=%lld, Solid=%lld\n"),
*Report.Name,
Metrics.AirFraction,
Metrics.LargestComponentShare,
Metrics.WalkableFraction,
Metrics.MedianFeatureScale,
Metrics.MedianVerticalClearance,
Metrics.NumAirComponents,
Metrics.NumSampled,
Metrics.NumSolid);
}
int32 NumRoutesChecked = 0;
int32 NumRoutesRefuted = 0;
for (int32 StrateIndex = 0; StrateIndex < Layout.Num(); ++StrateIndex)
{
const FStrateSlot& Slot = Layout[StrateIndex];
const int32 InteriorMinZ = (Slot.BottomChunkZ + 1) * CHUNK_SIZE;
const int32 InteriorMaxZ = Slot.TopChunkZ * CHUNK_SIZE;
if (InteriorMinZ >= InteriorMaxZ) continue;
const FVector A(Settings.CenterXY.X + 2.0f, Settings.CenterXY.Y + 2.0f,
static_cast<float>(InteriorMinZ + 2));
const FVector B(Settings.CenterXY.X + 2.0f, Settings.CenterXY.Y + 2.0f,
static_cast<float>(InteriorMaxZ - 2));
bool bCoarseLied = false;
const bool bConnected = VF_AreConnected(
*World.Generator, *World.StrateManager, StrateIndex, A, B, Settings, bCoarseLied);
++NumRoutesChecked;
if (bCoarseLied) ++NumRoutesRefuted;
if (bConnected && FullResolutionDirectSegmentHasSolid(*World.Generator, A, B))
{
AddError(FString::Printf(
TEXT("HARD FAILURE: VF_AreConnected returned true for %s while its full-resolution "
"direct walk contained a solid sample."),
*ArchetypeName(Slot)));
bAllChecksPassed = false;
}
}
// Controlled negative route: at SampleStep 4, the two coarse centres at X=-2 and X=+2
// remain air while the three full-resolution voxels at X=-1,0,+1 are filled solid.
const FStrateSlot& WallSlot = Layout[0];
const int32 WallInteriorMinZ = (WallSlot.BottomChunkZ + 1) * CHUNK_SIZE;
const float WallZ = static_cast<float>(WallInteriorMinZ + 2);
const FVector WallA(Settings.CenterXY.X - 2.0f, Settings.CenterXY.Y + 2.0f, WallZ);
const FVector WallB(Settings.CenterXY.X + 2.0f, Settings.CenterXY.Y + 2.0f, WallZ);
FVoxelModification Wall;
Wall.Shape = EVoxelBrushShape::Box;
Wall.Center = FVector(Settings.CenterXY.X, Settings.CenterXY.Y + 2.0f, WallZ);
Wall.BoxExtent = FVector(1.5f, 256.0f, 256.0f);
Wall.Radius = 1.0f;
Wall.Falloff = 0.1f;
Wall.Strength = 100.0f;
const TArray<FIntVector> WallChunks = World.DiffLayer->ApplyModification(Wall);
if (WallChunks.Num() == 0)
{
AddError(TEXT("HARD FAILURE: the controlled 3-voxel wall could not be installed."));
bAllChecksPassed = false;
}
bool bWallCoarseLied = false;
const bool bWallConnected = VF_AreConnected(
*World.Generator,
*World.StrateManager,
0,
WallA,
WallB,
Settings,
bWallCoarseLied);
++NumRoutesChecked;
if (bWallCoarseLied) ++NumRoutesRefuted;
if (bWallConnected)
{
AddError(TEXT("HARD FAILURE: VF_AreConnected reported true across a full-resolution solid 3-voxel wall."));
bAllChecksPassed = false;
}
if (!bWallCoarseLied)
{
AddError(TEXT("HARD FAILURE: the controlled coarse-connectivity lie did not set bOutCoarseLied."));
bAllChecksPassed = false;
}
World.DiffLayer->Clear();
TArray<FPassageReport> PassageReports;
const TArray<FVoxelPassage>& Passages = World.StrateManager->GetPassages();
PassageReports.Reserve(Passages.Num());
for (int32 PassageIndex = 0; PassageIndex < Passages.Num(); ++PassageIndex)
{
const FVoxelPassage& Passage = Passages[PassageIndex];
FPassageReport& Report = PassageReports.AddDefaulted_GetRef();
Report.Index = PassageIndex;
Report.Name = FString::Printf(TEXT("%03d"), PassageIndex);
auto CheckEndpoint = [&](int32 EndpointStrateIndex, const FVector& Endpoint) -> FString
{
if (!Layout.IsValidIndex(EndpointStrateIndex)) return TEXT("invalid-strate");
const FStrateSlot& Slot = Layout[EndpointStrateIndex];
if (!IsStrictInteriorPoint(Slot, Endpoint)) return TEXT("out-of-band");
const FVector Anchor = LargestComponentAnchor(Slot, Settings.CenterXY);
bool bCoarseLied = false;
const bool bConnected = VF_AreConnected(
*World.Generator,
*World.StrateManager,
EndpointStrateIndex,
Endpoint,
Anchor,
Settings,
bCoarseLied);
++NumRoutesChecked;
if (bCoarseLied) ++NumRoutesRefuted;
return bConnected ? TEXT("connected") : 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)
{
return A.Index < B.Index;
});
Summary += FString::Printf(
TEXT("Connectivity guard: %d routes checked, %d coarse routes refuted at full resolution.\n"),
NumRoutesChecked,
NumRoutesRefuted);
Summary += TEXT("Passages (endpoint -> largest-component spine anchor, sorted):\n");
for (const FPassageReport& Report : PassageReports)
{
Summary += FString::Printf(
TEXT(" passage %s: upper=%s, lower=%s\n"),
*Report.Name,
*Report.UpperResult,
*Report.LowerResult);
}
AddInfo(Summary);
return bAllChecksPassed;
}
#endif // WITH_DEV_AUTOMATION_TESTS
@@ -0,0 +1,686 @@
// Read-only, deterministic strate measurement and connectivity checks.
#include "VoxelStrateMeasure.h"
#include "VoxelGenerator.h"
#include "VoxelStrateManager.h"
#include "VoxelTypes.h"
namespace VoxelStrateMeasurePrivate
{
struct FSampleGrid
{
int32 NumX = 0;
int32 NumY = 0;
int32 NumZ = 0;
int32 SampleStep = 1;
int32 CellCount = 0;
float MinX = 0.0f;
float MinY = 0.0f;
float MinZ = 0.0f;
float MaxX = 0.0f;
float MaxY = 0.0f;
float MaxZ = 0.0f;
float ExtentX = 0.0f;
float ExtentY = 0.0f;
float ExtentZ = 0.0f;
// 1 = air, 0 = solid. The polarity is deliberately explicit at the sampling site below.
TArray<uint8> Air;
FORCEINLINE int32 Index(int32 X, int32 Y, int32 Z) const
{
return static_cast<int32>(
static_cast<int64>(X)
+ static_cast<int64>(NumX) * (static_cast<int64>(Y)
+ static_cast<int64>(NumY) * static_cast<int64>(Z)));
}
FVector CellCenter(int32 X, int32 Y, int32 Z) const
{
const float Step = static_cast<float>(SampleStep);
const float XOffset = FMath::Min(
static_cast<float>(X) * Step + 0.5f * Step, ExtentX - 0.5f);
const float YOffset = FMath::Min(
static_cast<float>(Y) * Step + 0.5f * Step, ExtentY - 0.5f);
const float ZOffset = FMath::Min(
static_cast<float>(Z) * Step + 0.5f * Step, ExtentZ - 0.5f);
return FVector(MinX + XOffset, MinY + YOffset, MinZ + ZOffset);
}
bool ContainsPoint(const FVector& Point) const
{
// The upper bounds are exclusive: a point on the box edge belongs to the next box,
// not to the last coarse cell in this measurement.
return Point.X >= MinX && Point.X < MaxX
&& Point.Y >= MinY && Point.Y < MaxY
&& Point.Z >= MinZ && Point.Z < MaxZ;
}
};
int64 CeilDivPositive(int64 Numerator, int64 Denominator)
{
return (Numerator + Denominator - 1) / Denominator;
}
bool Refuse(FString& OutReason, const TCHAR* Reason)
{
OutReason = Reason;
return false;
}
bool BuildSampleGrid(
const UVoxelGenerator& Generator,
const UVoxelStrateManager& Manager,
int32 StrateIndex,
const FVoxelStrateMeasureSettings& Settings,
FSampleGrid& OutGrid,
FString& OutReason)
{
const TArray<FStrateSlot>& Layout = Manager.GetLayout();
if (!Layout.IsValidIndex(StrateIndex))
{
return Refuse(OutReason, TEXT("StrateIndex is outside the manager layout."));
}
const FStrateSlot& Slot = Layout[StrateIndex];
if (Slot.Definition == nullptr)
{
return Refuse(OutReason, TEXT("The requested strate has no definition."));
}
if (Settings.SampleStep <= 0)
{
return Refuse(OutReason, TEXT("SampleStep must be greater than zero."));
}
if (Settings.RadiusInVoxels <= 0)
{
return Refuse(OutReason, TEXT("RadiusInVoxels must be greater than zero."));
}
if (Settings.MaxCells <= 0)
{
return Refuse(OutReason, TEXT("MaxCells must be greater than zero."));
}
if (Settings.HeadroomCells < 0)
{
return Refuse(OutReason, TEXT("HeadroomCells cannot be negative."));
}
if (!FMath::IsFinite(Settings.CenterXY.X) || !FMath::IsFinite(Settings.CenterXY.Y))
{
return Refuse(OutReason, TEXT("CenterXY must contain finite voxel coordinates."));
}
if (Slot.TopChunkZ <= Slot.BottomChunkZ)
{
return Refuse(OutReason, TEXT("The requested strate has no positive vertical extent."));
}
// Exclude the complete top and bottom chunks. Those outer chunks contain the seal bands;
// the measurement is about the strate interior, not the boundary bedrock.
const int64 InteriorMinZ = (static_cast<int64>(Slot.BottomChunkZ) + 1) * CHUNK_SIZE;
const int64 InteriorMaxZ = static_cast<int64>(Slot.TopChunkZ) * CHUNK_SIZE;
const int64 InteriorHeight = InteriorMaxZ - InteriorMinZ;
if (InteriorHeight <= 0)
{
return Refuse(OutReason, TEXT("The strate is too short after excluding its top and bottom chunks."));
}
const int64 XYExtent = static_cast<int64>(Settings.RadiusInVoxels) * 2;
const int64 Step = static_cast<int64>(Settings.SampleStep);
const int64 NumX64 = CeilDivPositive(XYExtent, Step);
const int64 NumY64 = NumX64;
const int64 NumZ64 = CeilDivPositive(InteriorHeight, Step);
const int64 MaxCells = static_cast<int64>(Settings.MaxCells);
if (NumX64 > INT32_MAX || NumY64 > INT32_MAX || NumZ64 > INT32_MAX)
{
return Refuse(OutReason, TEXT("A sample-grid dimension exceeds the supported array size."));
}
if (NumX64 > MaxCells || NumY64 > MaxCells || NumZ64 > MaxCells
|| NumX64 > MaxCells / NumY64)
{
return Refuse(OutReason, TEXT("The requested sample grid exceeds MaxCells; measurement refused."));
}
const int64 XYCells = NumX64 * NumY64;
if (XYCells > MaxCells / NumZ64)
{
return Refuse(OutReason, TEXT("The requested sample grid exceeds MaxCells; measurement refused."));
}
const int64 CellCount64 = XYCells * NumZ64;
if (CellCount64 <= 0 || CellCount64 > MaxCells || CellCount64 > INT32_MAX)
{
return Refuse(OutReason, TEXT("The requested sample grid exceeds the bounded cell count."));
}
const double MinX64 = static_cast<double>(Settings.CenterXY.X)
- static_cast<double>(Settings.RadiusInVoxels);
const double MaxX64 = static_cast<double>(Settings.CenterXY.X)
+ static_cast<double>(Settings.RadiusInVoxels);
const double MinY64 = static_cast<double>(Settings.CenterXY.Y)
- static_cast<double>(Settings.RadiusInVoxels);
const double MaxY64 = static_cast<double>(Settings.CenterXY.Y)
+ static_cast<double>(Settings.RadiusInVoxels);
const float MinX = static_cast<float>(MinX64);
const float MaxX = static_cast<float>(MaxX64);
const float MinY = static_cast<float>(MinY64);
const float MaxY = static_cast<float>(MaxY64);
const float MinZ = static_cast<float>(InteriorMinZ);
const float MaxZ = static_cast<float>(InteriorMaxZ);
if (!FMath::IsFinite(MinX) || !FMath::IsFinite(MaxX)
|| !FMath::IsFinite(MinY) || !FMath::IsFinite(MaxY)
|| !FMath::IsFinite(MinZ) || !FMath::IsFinite(MaxZ)
|| MaxX <= MinX || MaxY <= MinY || MaxZ <= MinZ)
{
return Refuse(OutReason, TEXT("The requested measurement bounds are not representable as finite voxel coordinates."));
}
OutGrid.NumX = static_cast<int32>(NumX64);
OutGrid.NumY = static_cast<int32>(NumY64);
OutGrid.NumZ = static_cast<int32>(NumZ64);
OutGrid.SampleStep = Settings.SampleStep;
OutGrid.CellCount = static_cast<int32>(CellCount64);
OutGrid.MinX = MinX;
OutGrid.MinY = MinY;
OutGrid.MinZ = MinZ;
OutGrid.MaxX = MaxX;
OutGrid.MaxY = MaxY;
OutGrid.MaxZ = MaxZ;
OutGrid.ExtentX = MaxX - MinX;
OutGrid.ExtentY = MaxY - MinY;
OutGrid.ExtentZ = MaxZ - MinZ;
OutGrid.Air.SetNumUninitialized(OutGrid.CellCount);
bool bSawNonFiniteDensity = false;
int32 CellIndex = 0;
for (int32 Z = 0; Z < OutGrid.NumZ; ++Z)
{
for (int32 Y = 0; Y < OutGrid.NumY; ++Y)
{
for (int32 X = 0; X < OutGrid.NumX; ++X)
{
const FVector Sample = OutGrid.CellCenter(X, Y, Z);
const float Density = Generator.GetDensityAt(Sample.X, Sample.Y, Sample.Z);
if (!FMath::IsFinite(Density))
{
bSawNonFiniteDensity = true;
}
// MC convention in this codebase: negative is solid, positive is air.
OutGrid.Air[CellIndex++] = Density > 0.0f ? 1u : 0u;
}
}
}
if (bSawNonFiniteDensity)
{
return Refuse(OutReason, TEXT("At least one density sample was non-finite."));
}
return true;
}
void DecodeIndex(const FSampleGrid& Grid, int32 Index, int32& OutX, int32& OutY, int32& OutZ)
{
const int32 Plane = Grid.NumX * Grid.NumY;
OutZ = Index / Plane;
const int32 InPlane = Index - OutZ * Plane;
OutY = InPlane / Grid.NumX;
OutX = InPlane - OutY * Grid.NumX;
}
void FloodFillAir(
const FSampleGrid& Grid,
TArray<int32>& OutComponents,
int32& OutNumComponents,
int32& OutLargestComponentCells)
{
OutComponents.SetNumUninitialized(Grid.CellCount);
for (int32 Index = 0; Index < Grid.CellCount; ++Index)
{
OutComponents[Index] = -1;
}
TArray<int32> Queue;
Queue.SetNumUninitialized(Grid.CellCount);
OutNumComponents = 0;
OutLargestComponentCells = 0;
for (int32 Start = 0; Start < Grid.CellCount; ++Start)
{
if (Grid.Air[Start] == 0u || OutComponents[Start] >= 0)
{
continue;
}
const int32 ComponentId = OutNumComponents++;
int32 Head = 0;
int32 Tail = 0;
int32 ComponentCells = 0;
OutComponents[Start] = ComponentId;
Queue[Tail++] = Start;
while (Head < Tail)
{
const int32 Current = Queue[Head++];
++ComponentCells;
int32 X = 0;
int32 Y = 0;
int32 Z = 0;
DecodeIndex(Grid, Current, X, Y, Z);
auto Visit = [&](int32 Neighbor)
{
if (Grid.Air[Neighbor] != 0u && OutComponents[Neighbor] < 0)
{
OutComponents[Neighbor] = ComponentId;
Queue[Tail++] = Neighbor;
}
};
// Fixed order keeps every derived value independent of container iteration order.
if (X + 1 < Grid.NumX) Visit(Grid.Index(X + 1, Y, Z));
if (X > 0) Visit(Grid.Index(X - 1, Y, Z));
if (Y + 1 < Grid.NumY) Visit(Grid.Index(X, Y + 1, Z));
if (Y > 0) Visit(Grid.Index(X, Y - 1, Z));
if (Z + 1 < Grid.NumZ) Visit(Grid.Index(X, Y, Z + 1));
if (Z > 0) Visit(Grid.Index(X, Y, Z - 1));
}
OutLargestComponentCells = FMath::Max(OutLargestComponentCells, ComponentCells);
}
}
void BuildManhattanDistanceToSolid(const FSampleGrid& Grid, TArray<int32>& OutDistances)
{
// Two raster passes with 6-neighbour Manhattan relaxation. The result is an approximation
// of distance-to-solid in the original field: each coarse grid unit is SampleStep VOXELS.
constexpr int32 Infinity = INT32_MAX;
OutDistances.SetNumUninitialized(Grid.CellCount);
for (int32 Index = 0; Index < Grid.CellCount; ++Index)
{
OutDistances[Index] = Grid.Air[Index] == 0u ? 0 : Infinity;
}
auto Relax = [&](int32 Current, int32 Neighbor)
{
if (OutDistances[Neighbor] == Infinity)
{
return;
}
const int32 Candidate = OutDistances[Neighbor] == INT32_MAX - 1
? Infinity : OutDistances[Neighbor] + 1;
OutDistances[Current] = FMath::Min(OutDistances[Current], Candidate);
};
for (int32 Z = 0; Z < Grid.NumZ; ++Z)
{
for (int32 Y = 0; Y < Grid.NumY; ++Y)
{
for (int32 X = 0; X < Grid.NumX; ++X)
{
const int32 Current = Grid.Index(X, Y, Z);
if (Grid.Air[Current] == 0u) continue;
if (X > 0) Relax(Current, Grid.Index(X - 1, Y, Z));
if (Y > 0) Relax(Current, Grid.Index(X, Y - 1, Z));
if (Z > 0) Relax(Current, Grid.Index(X, Y, Z - 1));
}
}
}
for (int32 Z = Grid.NumZ - 1; Z >= 0; --Z)
{
for (int32 Y = Grid.NumY - 1; Y >= 0; --Y)
{
for (int32 X = Grid.NumX - 1; X >= 0; --X)
{
const int32 Current = Grid.Index(X, Y, Z);
if (Grid.Air[Current] == 0u) continue;
if (X + 1 < Grid.NumX) Relax(Current, Grid.Index(X + 1, Y, Z));
if (Y + 1 < Grid.NumY) Relax(Current, Grid.Index(X, Y + 1, Z));
if (Z + 1 < Grid.NumZ) Relax(Current, Grid.Index(X, Y, Z + 1));
}
}
}
}
float MedianFloat(TArray<float>& Values)
{
if (Values.Num() == 0) return 0.0f;
Values.Sort();
const int32 Middle = Values.Num() / 2;
if ((Values.Num() & 1) != 0)
{
return Values[Middle];
}
return (Values[Middle - 1] + Values[Middle]) * 0.5f;
}
void BuildMetricsFromGrid(
const FSampleGrid& Grid,
const FVoxelStrateMeasureSettings& Settings,
FVoxelStrateMetrics& InOutMetrics)
{
TArray<int32> Components;
int32 NumComponents = 0;
int32 LargestComponentCells = 0;
FloodFillAir(Grid, Components, NumComponents, LargestComponentCells);
int64 NumAir = 0;
for (const uint8 bAir : Grid.Air)
{
NumAir += bAir != 0u ? 1 : 0;
}
InOutMetrics.NumSampled = Grid.CellCount;
InOutMetrics.NumAir = NumAir;
InOutMetrics.NumSolid = static_cast<int64>(Grid.CellCount) - NumAir;
InOutMetrics.AirFraction = static_cast<float>(
static_cast<double>(NumAir) / static_cast<double>(Grid.CellCount));
InOutMetrics.NumAirComponents = NumComponents;
InOutMetrics.LargestComponentShare = NumAir > 0
? static_cast<float>(static_cast<double>(LargestComponentCells)
/ static_cast<double>(NumAir))
: 0.0f;
TArray<int32> ClearanceValues;
ClearanceValues.Reserve(static_cast<int32>(NumAir));
int64 NumWalkable = 0;
for (int32 Z = 0; Z < Grid.NumZ; ++Z)
{
for (int32 Y = 0; Y < Grid.NumY; ++Y)
{
for (int32 X = 0; X < Grid.NumX; ++X)
{
const int32 Current = Grid.Index(X, Y, Z);
if (Grid.Air[Current] == 0u || Z == 0
|| Grid.Air[Grid.Index(X, Y, Z - 1)] != 0u)
{
continue;
}
bool bHasHeadroom = true;
for (int32 H = 0; H < Settings.HeadroomCells; ++H)
{
const int64 HeadroomZ = static_cast<int64>(Z) + H;
if (HeadroomZ >= Grid.NumZ
|| Grid.Air[Grid.Index(X, Y, static_cast<int32>(HeadroomZ))] == 0u)
{
bHasHeadroom = false;
break;
}
}
if (!bHasHeadroom) continue;
++NumWalkable;
int32 ClearanceCells = 0;
while (static_cast<int64>(Z) + ClearanceCells < Grid.NumZ
&& Grid.Air[Grid.Index(X, Y, Z + ClearanceCells)] != 0u)
{
++ClearanceCells;
}
const int64 ClearanceVoxels = static_cast<int64>(ClearanceCells)
* Settings.SampleStep;
ClearanceValues.Add(ClearanceVoxels > INT32_MAX
? INT32_MAX : static_cast<int32>(ClearanceVoxels));
}
}
}
InOutMetrics.WalkableFraction = NumAir > 0
? static_cast<float>(static_cast<double>(NumWalkable)
/ static_cast<double>(NumAir))
: 0.0f;
if (ClearanceValues.Num() > 0)
{
ClearanceValues.Sort();
// The public metric is integral, so use the lower median for an even sample count.
InOutMetrics.MedianVerticalClearance = ClearanceValues[(ClearanceValues.Num() - 1) / 2];
}
if (InOutMetrics.NumSolid > 0 && NumAir > 0)
{
TArray<int32> Distances;
BuildManhattanDistanceToSolid(Grid, Distances);
TArray<float> FeatureScales;
FeatureScales.Reserve(static_cast<int32>(NumAir));
for (int32 Index = 0; Index < Grid.CellCount; ++Index)
{
if (Grid.Air[Index] != 0u)
{
FeatureScales.Add(static_cast<float>(Distances[Index])
* static_cast<float>(Settings.SampleStep));
}
}
InOutMetrics.MedianFeatureScale = MedianFloat(FeatureScales);
}
}
bool FindCellForPoint(const FSampleGrid& Grid, const FVector& Point, int32& OutCell)
{
if (!Grid.ContainsPoint(Point)) return false;
const float Step = static_cast<float>(Grid.SampleStep);
const int32 X = FMath::Clamp(FMath::FloorToInt((Point.X - Grid.MinX) / Step), 0, Grid.NumX - 1);
const int32 Y = FMath::Clamp(FMath::FloorToInt((Point.Y - Grid.MinY) / Step), 0, Grid.NumY - 1);
const int32 Z = FMath::Clamp(FMath::FloorToInt((Point.Z - Grid.MinZ) / Step), 0, Grid.NumZ - 1);
OutCell = Grid.Index(X, Y, Z);
return true;
}
bool FullResolutionAirOnSegment(
const UVoxelGenerator& Generator,
const FVector& Start,
const FVector& End)
{
const FVector Delta = End - Start;
const float MaxDelta = FMath::Max3(
FMath::Abs(Delta.X), FMath::Abs(Delta.Y), FMath::Abs(Delta.Z));
const int32 NumSteps = FMath::Max(1, FMath::CeilToInt(MaxDelta));
for (int32 Step = 0; Step <= NumSteps; ++Step)
{
const float Alpha = static_cast<float>(Step) / static_cast<float>(NumSteps);
const FVector Sample = Start + Delta * Alpha;
const float Density = Generator.GetDensityAt(Sample.X, Sample.Y, Sample.Z);
if (!FMath::IsFinite(Density) || !(Density > 0.0f))
{
return false;
}
}
return true;
}
bool FullResolutionPathIsAir(
const UVoxelGenerator& Generator,
const FSampleGrid& Grid,
const FVector& A,
const FVector& B,
const TArray<int32>& Path)
{
int32 StartX = 0;
int32 StartY = 0;
int32 StartZ = 0;
DecodeIndex(Grid, Path[0], StartX, StartY, StartZ);
int32 EndX = 0;
int32 EndY = 0;
int32 EndZ = 0;
DecodeIndex(Grid, Path.Last(), EndX, EndY, EndZ);
if (!FullResolutionAirOnSegment(Generator, A, Grid.CellCenter(StartX, StartY, StartZ)))
{
return false;
}
for (int32 PathIndex = 1; PathIndex < Path.Num(); ++PathIndex)
{
int32 PreviousX = 0;
int32 PreviousY = 0;
int32 PreviousZ = 0;
DecodeIndex(Grid, Path[PathIndex - 1], PreviousX, PreviousY, PreviousZ);
int32 CurrentX = 0;
int32 CurrentY = 0;
int32 CurrentZ = 0;
DecodeIndex(Grid, Path[PathIndex], CurrentX, CurrentY, CurrentZ);
if (!FullResolutionAirOnSegment(
Generator,
Grid.CellCenter(PreviousX, PreviousY, PreviousZ),
Grid.CellCenter(CurrentX, CurrentY, CurrentZ)))
{
return false;
}
}
return FullResolutionAirOnSegment(Generator, Grid.CellCenter(EndX, EndY, EndZ), B);
}
bool FindCoarsePath(
const FSampleGrid& Grid,
int32 Start,
int32 Goal,
TArray<int32>& OutPath)
{
if (Grid.Air[Start] == 0u || Grid.Air[Goal] == 0u)
{
return false;
}
TArray<int32> Parent;
Parent.SetNumUninitialized(Grid.CellCount);
for (int32 Index = 0; Index < Grid.CellCount; ++Index)
{
Parent[Index] = -2; // unvisited; -1 is the root's parent
}
TArray<int32> Queue;
Queue.SetNumUninitialized(Grid.CellCount);
int32 Head = 0;
int32 Tail = 0;
Parent[Start] = -1;
Queue[Tail++] = Start;
while (Head < Tail && Parent[Goal] == -2)
{
const int32 Current = Queue[Head++];
int32 X = 0;
int32 Y = 0;
int32 Z = 0;
DecodeIndex(Grid, Current, X, Y, Z);
auto Visit = [&](int32 Neighbor)
{
if (Grid.Air[Neighbor] != 0u && Parent[Neighbor] == -2)
{
Parent[Neighbor] = Current;
Queue[Tail++] = Neighbor;
}
};
// The order is fixed so the recovered route is deterministic. This is a real BFS
// parent chain, not just a component-membership answer.
if (X + 1 < Grid.NumX) Visit(Grid.Index(X + 1, Y, Z));
if (X > 0) Visit(Grid.Index(X - 1, Y, Z));
if (Y + 1 < Grid.NumY) Visit(Grid.Index(X, Y + 1, Z));
if (Y > 0) Visit(Grid.Index(X, Y - 1, Z));
if (Z + 1 < Grid.NumZ) Visit(Grid.Index(X, Y, Z + 1));
if (Z > 0) Visit(Grid.Index(X, Y, Z - 1));
}
if (Parent[Goal] == -2)
{
return false;
}
OutPath.Reset();
OutPath.Reserve(Tail);
for (int32 Current = Goal; Current >= 0; Current = Parent[Current])
{
OutPath.Add(Current);
if (Current == Start) break;
}
if (OutPath.Last() != Start)
{
OutPath.Reset();
return false;
}
for (int32 Left = 0, Right = OutPath.Num() - 1; Left < Right; ++Left, --Right)
{
Swap(OutPath[Left], OutPath[Right]);
}
return true;
}
}
FVoxelStrateMetrics VF_MeasureStrate(
const UVoxelGenerator& Generator,
const UVoxelStrateManager& Manager,
int32 StrateIndex,
const FVoxelStrateMeasureSettings& Settings)
{
FVoxelStrateMetrics Result;
VoxelStrateMeasurePrivate::FSampleGrid Grid;
if (!VoxelStrateMeasurePrivate::BuildSampleGrid(
Generator, Manager, StrateIndex, Settings, Grid, Result.RefusalReason))
{
return Result;
}
VoxelStrateMeasurePrivate::BuildMetricsFromGrid(Grid, Settings, Result);
Result.bValid = Result.NumSampled > 0;
if (!Result.bValid)
{
Result.RefusalReason = TEXT("The measurement grid was empty.");
}
return Result;
}
bool VF_AreConnected(
const UVoxelGenerator& Generator,
const UVoxelStrateManager& Manager,
int32 StrateIndex,
const FVector& AVoxel,
const FVector& BVoxel,
const FVoxelStrateMeasureSettings& Settings,
bool& bOutCoarseLied)
{
bOutCoarseLied = false;
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))
{
return false;
}
VoxelStrateMeasurePrivate::FSampleGrid Grid;
FString RefusalReason;
if (!VoxelStrateMeasurePrivate::BuildSampleGrid(
Generator, Manager, StrateIndex, Settings, Grid, RefusalReason))
{
return false;
}
int32 Start = -1;
int32 Goal = -1;
if (!VoxelStrateMeasurePrivate::FindCellForPoint(Grid, AVoxel, Start)
|| !VoxelStrateMeasurePrivate::FindCellForPoint(Grid, BVoxel, Goal))
{
return false;
}
TArray<int32> Path;
if (!VoxelStrateMeasurePrivate::FindCoarsePath(Grid, Start, Goal, Path))
{
return false;
}
// The coarse route is evidence, not a pass. Re-check this one BFS route at one-voxel spacing
// in the original voxel field. A single solid sample is the dangerous coarse false positive.
if (!VoxelStrateMeasurePrivate::FullResolutionPathIsAir(
Generator, Grid, AVoxel, BVoxel, Path))
{
bOutCoarseLied = true;
return false;
}
return true;
}
@@ -0,0 +1,63 @@
// Read-only, headless measurements over a coarse sample of one strate.
#pragma once
#include "CoreMinimal.h"
class UVoxelGenerator;
class UVoxelStrateManager;
/** Settings for the deterministic coarse grid used by the strate measurement pass. */
struct VOXELFORGE_API FVoxelStrateMeasureSettings
{
int32 SampleStep = 4; // Voxels between samples.
int32 RadiusInVoxels = 256; // Half-extent in X and Y around CenterXY.
FVector2D CenterXY = FVector2D::ZeroVector; // Actor-space voxel coordinates.
int32 MaxCells = 8000000; // Refuse a grid larger than this many cells.
int32 HeadroomCells = 2; // Air cells above a floor cell required for walkable.
};
/** Metrics derived from one coarse grid and its single air flood fill. */
struct VOXELFORGE_API FVoxelStrateMetrics
{
bool bValid = false;
FString RefusalReason;
int64 NumSampled = 0;
int64 NumAir = 0;
int64 NumSolid = 0;
float AirFraction = 0.f;
int32 NumAirComponents = 0;
float LargestComponentShare = 0.f;
float WalkableFraction = 0.f;
float MedianFeatureScale = 0.f;
int32 MedianVerticalClearance = 0;
};
/**
* Measure one strate in actor-space voxel coordinates.
*
* The pass samples only the interior chunks of the requested strate, flood-fills its air once,
* and derives all returned metrics from that same grid. It never mutates either input object.
*/
VOXELFORGE_API FVoxelStrateMetrics VF_MeasureStrate(
const UVoxelGenerator& Generator,
const UVoxelStrateManager& Manager,
int32 StrateIndex,
const FVoxelStrateMeasureSettings& Settings);
/**
* 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
* route was solid (or non-finite), in which case this function always returns false.
*/
VOXELFORGE_API bool VF_AreConnected(
const UVoxelGenerator& Generator,
const UVoxelStrateManager& Manager,
int32 StrateIndex,
const FVector& AVoxel,
const FVector& BVoxel,
const FVoxelStrateMeasureSettings& Settings,
bool& bOutCoarseLied);