diff --git a/CODEMAP.md b/CODEMAP.md index 42da966..fafc2db 100644 --- a/CODEMAP.md +++ b/CODEMAP.md @@ -303,15 +303,17 @@ This is **where terrain shape lives.** > 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. +Headless, read-only Tier 2 measurement pass. `VF_MeasureStrate` samples one strate between a +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, +walkability, feature scale, and clearance from that grid. Metrics report the resolved margin and +the inclusive/exclusive voxel Z window used. `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. | +| `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. | | `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. | diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeStrateConnectivityTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeStrateConnectivityTest.cpp index 7890f7f..dc1269b 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeStrateConnectivityTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgeStrateConnectivityTest.cpp @@ -19,14 +19,20 @@ namespace { FString Name; int32 Index = INDEX_NONE; - FVoxelStrateMetrics First; - FVoxelStrateMetrics Second; + FVoxelStrateMetrics OldWindow; + FVoxelStrateMetrics OldWindowRepeat; + FVoxelStrateMetrics DerivedWindow; + FVoxelStrateMetrics DerivedWindowRepeat; }; struct FPassageReport { int32 Index = INDEX_NONE; FString Name; + int32 UpperStrateIndex = INDEX_NONE; + int32 LowerStrateIndex = INDEX_NONE; + FVector UpperPoint = FVector::ZeroVector; + FVector LowerPoint = FVector::ZeroVector; FString UpperResult; FString LowerResult; }; @@ -61,22 +67,39 @@ namespace && SameFloatBits(A.LargestComponentShare, B.LargestComponentShare) && SameFloatBits(A.WalkableFraction, B.WalkableFraction) && SameFloatBits(A.MedianFeatureScale, B.MedianFeatureScale) - && A.MedianVerticalClearance == B.MedianVerticalClearance; + && A.MedianVerticalClearance == B.MedianVerticalClearance + && A.ResolvedMarginVoxels == B.ResolvedMarginVoxels + && A.SampledMinZ == B.SampledMinZ + && A.SampledMaxZ == B.SampledMaxZ; } - bool IsStrictInteriorPoint(const FStrateSlot& Slot, const FVector& Point) + bool IsInsideMeasuredWindow( + const FVoxelStrateMetrics& Metrics, + const FVoxelStrateMeasureSettings& Settings, + const FVector& Point) { - const float MinZ = (static_cast(Slot.BottomChunkZ) + 1.0f) * CHUNK_SIZE; - const float MaxZ = static_cast(Slot.TopChunkZ) * CHUNK_SIZE; - return Point.Z >= MinZ && Point.Z < MaxZ; + const float MinX = Settings.CenterXY.X - static_cast(Settings.RadiusInVoxels); + const float MaxX = Settings.CenterXY.X + static_cast(Settings.RadiusInVoxels); + const float MinY = Settings.CenterXY.Y - static_cast(Settings.RadiusInVoxels); + const float MaxY = Settings.CenterXY.Y + static_cast(Settings.RadiusInVoxels); + return Point.X >= MinX && Point.X < MaxX + && Point.Y >= MinY && Point.Y < MaxY + && Point.Z >= static_cast(Metrics.SampledMinZ) + && Point.Z < static_cast(Metrics.SampledMaxZ); } - FVector LargestComponentAnchor(const FStrateSlot& Slot, const FVector2D& CenterXY) + FVector LargestComponentAnchor( + const FStrateSlot& Slot, + const FVector2D& CenterXY, + int32 SampleStep) { // 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(Slot.BottomChunkZ) + 1.0f) * CHUNK_SIZE + 2.0f; + // 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(Slot.BottomChunkZ) + 1.0f) * CHUNK_SIZE + + 0.5f * static_cast(SampleStep); return FVector(CenterXY.X + 2.0f, CenterXY.Y + 2.0f, AnchorZ); } @@ -200,6 +223,11 @@ bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters) Settings.MaxCells = 8000000; Settings.HeadroomCells = 2; + FVoxelStrateMeasureSettings OldWindowSettings = Settings; + OldWindowSettings.InteriorMarginVoxels = CHUNK_SIZE; + FVoxelStrateMeasureSettings DerivedWindowSettings = Settings; + DerivedWindowSettings.InteriorMarginVoxels = -1; + bool bAllChecksPassed = true; FString Summary = TEXT("VoxelForge strate measurement summary (seed 1337, step 4, radius 256):\n"); @@ -258,8 +286,8 @@ bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters) // 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; + const int32 SolidControlMinZ = SolidControlSlot.BottomChunkZ * CHUNK_SIZE; + const int32 SolidControlMaxZ = (SolidControlSlot.TopChunkZ + 1) * CHUNK_SIZE; FVoxelModification SolidControl; SolidControl.Shape = EVoxelBrushShape::Box; SolidControl.Center = FVector( @@ -275,7 +303,7 @@ bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters) SolidControl.Strength = 100.0f; const TArray SolidControlChunks = World.DiffLayer->ApplyModification(SolidControl); const FVoxelStrateMetrics SolidControlMetrics = VF_MeasureStrate( - *World.Generator, *World.StrateManager, 0, Settings); + *World.Generator, *World.StrateManager, 0, DerivedWindowSettings); if (SolidControlChunks.Num() == 0 || !SolidControlMetrics.bValid || SolidControlMetrics.AirFraction > 0.05f) { @@ -291,22 +319,16 @@ bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters) TArray ArchetypeReports; const TArray& 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); + TArray DerivedMetricsByIndex; + DerivedMetricsByIndex.SetNum(Layout.Num()); - const FVoxelStrateMetrics& Metrics = Report.First; + auto ValidateMeasurement = [&](const FString& Label, const FVoxelStrateMetrics& Metrics) + { if (!Metrics.bValid) { AddError(FString::Printf( TEXT("HARD FAILURE: %s measurement refused: %s."), - *Report.Name, + *Label, *Metrics.RefusalReason)); bAllChecksPassed = false; } @@ -314,7 +336,7 @@ bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters) { AddError(FString::Printf( TEXT("HARD FAILURE: vacuous %s measurement — sampled=%lld, air=%lld, solid=%lld."), - *Report.Name, + *Label, Metrics.NumSampled, Metrics.NumAir, Metrics.NumSolid)); @@ -327,19 +349,49 @@ bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters) || !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)) + || (Metrics.LargestComponentShare == 0.0f && Metrics.AirFraction > 0.05f) + || Metrics.ResolvedMarginVoxels < 1 + || Metrics.SampledMinZ >= Metrics.SampledMaxZ) { AddError(FString::Printf( - TEXT("HARD FAILURE: range/component sanity failed for %s."), *Report.Name)); + TEXT("HARD FAILURE: range/component/window sanity failed for %s."), *Label)); bAllChecksPassed = false; } - if (!MetricsAreBitIdentical(Report.First, Report.Second)) + }; + + for (int32 StrateIndex = 0; StrateIndex < Layout.Num(); ++StrateIndex) + { + FArchetypeReport& Report = ArchetypeReports.AddDefaulted_GetRef(); + Report.Index = StrateIndex; + Report.Name = ArchetypeName(Layout[StrateIndex]); + Report.OldWindow = VF_MeasureStrate( + *World.Generator, *World.StrateManager, StrateIndex, OldWindowSettings); + Report.OldWindowRepeat = VF_MeasureStrate( + *World.Generator, *World.StrateManager, StrateIndex, OldWindowSettings); + Report.DerivedWindow = VF_MeasureStrate( + *World.Generator, *World.StrateManager, StrateIndex, DerivedWindowSettings); + Report.DerivedWindowRepeat = VF_MeasureStrate( + *World.Generator, *World.StrateManager, StrateIndex, DerivedWindowSettings); + + ValidateMeasurement( + FString::Printf(TEXT("%s old-window"), *Report.Name), Report.OldWindow); + ValidateMeasurement( + FString::Printf(TEXT("%s derived-window"), *Report.Name), Report.DerivedWindow); + if (!MetricsAreBitIdentical(Report.OldWindow, Report.OldWindowRepeat)) { AddError(FString::Printf( - TEXT("HARD FAILURE: %s measurement was not bit-identical on the second call."), + TEXT("HARD FAILURE: %s old-window measurement was not bit-identical on the second call."), *Report.Name)); bAllChecksPassed = false; } + if (!MetricsAreBitIdentical(Report.DerivedWindow, Report.DerivedWindowRepeat)) + { + AddError(FString::Printf( + TEXT("HARD FAILURE: %s derived-window measurement was not bit-identical on the second call."), + *Report.Name)); + bAllChecksPassed = false; + } + DerivedMetricsByIndex[StrateIndex] = Report.DerivedWindow; } ArchetypeReports.Sort([](const FArchetypeReport& A, const FArchetypeReport& B) @@ -355,22 +407,38 @@ bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters) GapAirFraction, GapAirSamples, GapSamples); - Summary += TEXT("Archetypes (sorted):\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"); for (const FArchetypeReport& Report : ArchetypeReports) { - const FVoxelStrateMetrics& Metrics = Report.First; + const FVoxelStrateMetrics& Old = Report.OldWindow; + const FVoxelStrateMetrics& Derived = Report.DerivedWindow; Summary += FString::Printf( - TEXT(" %s: Air=%.9g, Largest=%.9g, Walkable=%.9g, FeatureScale=%.9g vox, " - "Clearance=%d vox, Components=%d, Samples=%lld, Solid=%lld\n"), + TEXT(" %s | %d | [%d,%d) | %.9g | %.9g | %.9g | %.9g | %d | %d | %lld | %lld | " + "%d | [%d,%d) | %.9g | %.9g | %.9g | %.9g | %d | %d | %lld | %lld\n"), *Report.Name, - Metrics.AirFraction, - Metrics.LargestComponentShare, - Metrics.WalkableFraction, - Metrics.MedianFeatureScale, - Metrics.MedianVerticalClearance, - Metrics.NumAirComponents, - Metrics.NumSampled, - Metrics.NumSolid); + Old.ResolvedMarginVoxels, + Old.SampledMinZ, + Old.SampledMaxZ, + Old.AirFraction, + Old.LargestComponentShare, + Old.WalkableFraction, + Old.MedianFeatureScale, + Old.MedianVerticalClearance, + Old.NumAirComponents, + Old.NumSampled, + Old.NumSolid, + Derived.ResolvedMarginVoxels, + Derived.SampledMinZ, + Derived.SampledMaxZ, + Derived.AirFraction, + Derived.LargestComponentShare, + Derived.WalkableFraction, + Derived.MedianFeatureScale, + Derived.MedianVerticalClearance, + Derived.NumAirComponents, + Derived.NumSampled, + Derived.NumSolid); } int32 NumRoutesChecked = 0; @@ -378,17 +446,24 @@ bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters) 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 FVoxelStrateMetrics& Window = DerivedMetricsByIndex[StrateIndex]; + if (!Window.bValid || Window.SampledMinZ >= Window.SampledMaxZ) continue; const FVector A(Settings.CenterXY.X + 2.0f, Settings.CenterXY.Y + 2.0f, - static_cast(InteriorMinZ + 2)); + static_cast(Window.SampledMinZ) + + 0.5f * static_cast(DerivedWindowSettings.SampleStep)); const FVector B(Settings.CenterXY.X + 2.0f, Settings.CenterXY.Y + 2.0f, - static_cast(InteriorMaxZ - 2)); + static_cast(Window.SampledMaxZ) + - 0.5f * static_cast(DerivedWindowSettings.SampleStep)); bool bCoarseLied = false; const bool bConnected = VF_AreConnected( - *World.Generator, *World.StrateManager, StrateIndex, A, B, Settings, bCoarseLied); + *World.Generator, + *World.StrateManager, + StrateIndex, + A, + B, + DerivedWindowSettings, + bCoarseLied); ++NumRoutesChecked; if (bCoarseLied) ++NumRoutesRefuted; if (bConnected && FullResolutionDirectSegmentHasSolid(*World.Generator, A, B)) @@ -403,9 +478,9 @@ bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters) // 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(WallInteriorMinZ + 2); + const FVoxelStrateMetrics& WallWindow = DerivedMetricsByIndex[0]; + const float WallZ = static_cast(WallWindow.SampledMinZ) + + 0.5f * static_cast(DerivedWindowSettings.SampleStep); 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); @@ -430,7 +505,7 @@ bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters) 0, WallA, WallB, - Settings, + DerivedWindowSettings, bWallCoarseLied); ++NumRoutesChecked; if (bWallCoarseLied) ++NumRoutesRefuted; @@ -455,14 +530,27 @@ bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters) FPassageReport& Report = PassageReports.AddDefaulted_GetRef(); 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"); - const FStrateSlot& Slot = Layout[EndpointStrateIndex]; - if (!IsStrictInteriorPoint(Slot, Endpoint)) return TEXT("out-of-band"); + const FVoxelStrateMetrics& EndpointMetrics = + DerivedMetricsByIndex[EndpointStrateIndex]; + if (!EndpointMetrics.bValid) return TEXT("invalid-window"); + if (!IsInsideMeasuredWindow(EndpointMetrics, DerivedWindowSettings, Endpoint)) + { + return TEXT("out-of-window"); + } - const FVector Anchor = LargestComponentAnchor(Slot, Settings.CenterXY); + const FStrateSlot& Slot = Layout[EndpointStrateIndex]; + const FVector Anchor = LargestComponentAnchor( + Slot, + DerivedWindowSettings.CenterXY, + DerivedWindowSettings.SampleStep); bool bCoarseLied = false; const bool bConnected = VF_AreConnected( *World.Generator, @@ -470,11 +558,11 @@ bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters) EndpointStrateIndex, Endpoint, Anchor, - Settings, + DerivedWindowSettings, bCoarseLied); ++NumRoutesChecked; if (bCoarseLied) ++NumRoutesRefuted; - return bConnected ? TEXT("connected") : TEXT("not-connected"); + return bConnected ? TEXT("CONNECTED_TO_LARGEST") : TEXT("NOT_CONNECTED"); }; Report.UpperResult = CheckEndpoint(Passage.UpperStrateIndex, Passage.UpperPoint); @@ -486,16 +574,25 @@ bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters) return A.Index < B.Index; }); Summary += FString::Printf( - TEXT("Connectivity guard: %d routes checked, %d coarse routes refuted at full resolution.\n"), + TEXT("Connectivity guard (derived window): %d routes checked, %d coarse routes refuted at full resolution.\n"), NumRoutesChecked, NumRoutesRefuted); - Summary += TEXT("Passages (endpoint -> largest-component spine anchor, sorted):\n"); + Summary += TEXT("Passages (derived window; each endpoint -> its strate's largest-component spine anchor, sorted):\n"); for (const FPassageReport& Report : PassageReports) { Summary += FString::Printf( - TEXT(" passage %s: upper=%s, lower=%s\n"), + TEXT(" passage %s: UpperPoint strate=%d (%.3f,%.3f,%.3f) => %s; " + "LowerPoint strate=%d (%.3f,%.3f,%.3f) => %s\n"), *Report.Name, + Report.UpperStrateIndex, + Report.UpperPoint.X, + Report.UpperPoint.Y, + Report.UpperPoint.Z, *Report.UpperResult, + Report.LowerStrateIndex, + Report.LowerPoint.X, + Report.LowerPoint.Y, + Report.LowerPoint.Z, *Report.LowerResult); } AddInfo(Summary); diff --git a/Source/VoxelForge/Private/VoxelStrateMeasure.cpp b/Source/VoxelForge/Private/VoxelStrateMeasure.cpp index b273725..25035cd 100644 --- a/Source/VoxelForge/Private/VoxelStrateMeasure.cpp +++ b/Source/VoxelForge/Private/VoxelStrateMeasure.cpp @@ -26,6 +26,10 @@ namespace VoxelStrateMeasurePrivate float ExtentY = 0.0f; float ExtentZ = 0.0f; + int32 ResolvedMarginVoxels = 0; + int32 SampledMinZ = 0; + int32 SampledMaxZ = 0; + // 1 = air, 0 = solid. The polarity is deliberately explicit at the sampling site below. TArray Air; @@ -114,14 +118,74 @@ namespace VoxelStrateMeasurePrivate 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(Slot.BottomChunkZ) + 1) * CHUNK_SIZE; - const int64 InteriorMaxZ = static_cast(Slot.TopChunkZ) * CHUNK_SIZE; + const int64 StrateBottomZ = static_cast(Slot.BottomChunkZ) * CHUNK_SIZE; + const int64 StrateTopZ = (static_cast(Slot.TopChunkZ) + 1) * CHUNK_SIZE; + const int64 StrateHeight = StrateTopZ - StrateBottomZ; + if (StrateHeight <= 0) + { + return Refuse(OutReason, TEXT("The requested strate has no positive voxel height.")); + } + + // Resolve the margin from the same blended parameter query used by generation. The + // representative chunk is deliberately in the middle of this slot and uses the sample + // centre's XY chunk, so a future non-Hard transition resolves through the manager exactly + // as generation does at that representative location. + const int32 MidChunkZ = Slot.BottomChunkZ + + (Slot.TopChunkZ - Slot.BottomChunkZ) / 2; + const FIntVector RepresentativeChunk( + FMath::FloorToInt(Settings.CenterXY.X / static_cast(CHUNK_SIZE)), + FMath::FloorToInt(Settings.CenterXY.Y / static_cast(CHUNK_SIZE)), + MidChunkZ); + const FStrateGenerationParams RepresentativeParams = + Manager.GetGenerationParams(RepresentativeChunk); + + const int64 MaxMargin64 = FMath::Max(1, StrateHeight / 4); + const int32 MaxMargin = static_cast( + FMath::Min(MaxMargin64, static_cast(INT32_MAX))); + int32 RequestedMargin = Settings.InteriorMarginVoxels; + if (RequestedMargin < 0) + { + const float DoubleSealThickness = 2.0f * RepresentativeParams.BoundarySealThickness; + if (FMath::IsNaN(RepresentativeParams.BoundarySealThickness)) + { + return Refuse(OutReason, TEXT("BoundarySealThickness is NaN; the interior margin cannot be derived.")); + } + + // Avoid converting an infinite value to int. A positive overflow is still an + // intentionally absurd seal request and therefore resolves to the safe maximum; + // negative infinity resolves to the minimum. + if (!FMath::IsFinite(DoubleSealThickness)) + { + RequestedMargin = DoubleSealThickness > 0.0f ? MaxMargin : 1; + } + else if (DoubleSealThickness >= static_cast(INT32_MAX)) + { + RequestedMargin = MaxMargin; + } + else if (DoubleSealThickness <= static_cast(INT32_MIN)) + { + RequestedMargin = 1; + } + else + { + RequestedMargin = FMath::CeilToInt(DoubleSealThickness); + } + } + const int32 ResolvedMargin = FMath::Clamp(RequestedMargin, 1, MaxMargin); + + const int64 InteriorMinZ = StrateBottomZ + static_cast(ResolvedMargin); + const int64 InteriorMaxZ = StrateTopZ - static_cast(ResolvedMargin); const int64 InteriorHeight = InteriorMaxZ - InteriorMinZ; if (InteriorHeight <= 0) { - return Refuse(OutReason, TEXT("The strate is too short after excluding its top and bottom chunks.")); + return Refuse(OutReason, TEXT("The strate has no measurable height after applying its interior margin.")); + } + if (InteriorMinZ < static_cast(INT32_MIN) + || InteriorMinZ > static_cast(INT32_MAX) + || InteriorMaxZ < static_cast(INT32_MIN) + || InteriorMaxZ > static_cast(INT32_MAX)) + { + return Refuse(OutReason, TEXT("The sampled Z window is outside the metrics struct's int32 range.")); } const int64 XYExtent = static_cast(Settings.RadiusInVoxels) * 2; @@ -188,6 +252,9 @@ namespace VoxelStrateMeasurePrivate OutGrid.ExtentX = MaxX - MinX; OutGrid.ExtentY = MaxY - MinY; OutGrid.ExtentZ = MaxZ - MinZ; + OutGrid.ResolvedMarginVoxels = ResolvedMargin; + OutGrid.SampledMinZ = static_cast(InteriorMinZ); + OutGrid.SampledMaxZ = static_cast(InteriorMaxZ); OutGrid.Air.SetNumUninitialized(OutGrid.CellCount); bool bSawNonFiniteDensity = false; @@ -627,6 +694,9 @@ FVoxelStrateMetrics VF_MeasureStrate( return Result; } + Result.ResolvedMarginVoxels = Grid.ResolvedMarginVoxels; + Result.SampledMinZ = Grid.SampledMinZ; + Result.SampledMaxZ = Grid.SampledMaxZ; VoxelStrateMeasurePrivate::BuildMetricsFromGrid(Grid, Settings, Result); Result.bValid = Result.NumSampled > 0; if (!Result.bValid) diff --git a/Source/VoxelForge/Public/VoxelStrateMeasure.h b/Source/VoxelForge/Public/VoxelStrateMeasure.h index 55e0de9..837d7c3 100644 --- a/Source/VoxelForge/Public/VoxelStrateMeasure.h +++ b/Source/VoxelForge/Public/VoxelStrateMeasure.h @@ -15,6 +15,7 @@ struct VOXELFORGE_API FVoxelStrateMeasureSettings 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. + int32 InteriorMarginVoxels = -1; // <0 = derive from BoundarySealThickness (2x, clamped). }; /** Metrics derived from one coarse grid and its single air flood fill. */ @@ -33,13 +34,19 @@ struct VOXELFORGE_API FVoxelStrateMetrics float WalkableFraction = 0.f; float MedianFeatureScale = 0.f; int32 MedianVerticalClearance = 0; + + // The exact vertical window used by the measurement: [SampledMinZ, SampledMaxZ). + int32 ResolvedMarginVoxels = 0; + int32 SampledMinZ = 0; + int32 SampledMaxZ = 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. + * The pass samples the requested strate between a resolved margin inside its top and bottom + * boundaries, 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,