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

@@ -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;
}