From efcc8fcb65ed210759b099d14d0088df054d0064 Mon Sep 17 00:00:00 2001 From: Fr0zka Date: Sun, 30 Aug 2026 14:06:43 +0200 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01Bp7FxoQcSoiuhxjuLr6aBM --- CODEMAP.md | 13 +- .../VoxelForgeStrateConnectivityTest.cpp | 572 ++++++++++++++---- .../VoxelForge/Private/VoxelStrateMeasure.cpp | 172 +++++- Source/VoxelForge/Public/VoxelStrateMeasure.h | 25 +- 4 files changed, 634 insertions(+), 148 deletions(-) diff --git a/CODEMAP.md b/CODEMAP.md index fafc2db..f9a94d3 100644 --- a/CODEMAP.md +++ b/CODEMAP.md @@ -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, 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. +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 -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. +only that route at full voxel resolution before returning `Connected`. It returns an explicit +`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 | |--------|------| -| `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_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` Header is rich with inline docs. Two namespaces + a per-chunk cache system. diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeStrateConnectivityTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeStrateConnectivityTest.cpp index dc1269b..a79d4a6 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeStrateConnectivityTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgeStrateConnectivityTest.cpp @@ -25,16 +25,27 @@ namespace 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; FString Name; - int32 UpperStrateIndex = INDEX_NONE; - int32 LowerStrateIndex = INDEX_NONE; - FVector UpperPoint = FVector::ZeroVector; - FVector LowerPoint = FVector::ZeroVector; - FString UpperResult; - FString LowerResult; + int32 ArrivalPassageIndex = INDEX_NONE; + int32 DeparturePassageIndex = INDEX_NONE; + FVector ArrivalPoint = FVector::ZeroVector; + FVector DeparturePoint = FVector::ZeroVector; + bool bHasArrival = false; + bool bHasDeparture = false; + FConnectivityProbe ArrivalToDeparture; + FConnectivityProbe ArrivalToLargest; + FConnectivityProbe DepartureToLargest; }; FString ArchetypeName(const FStrateSlot& Slot) @@ -55,6 +66,13 @@ namespace 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) { return A.bValid == B.bValid @@ -65,6 +83,9 @@ namespace && SameFloatBits(A.AirFraction, B.AirFraction) && A.NumAirComponents == B.NumAirComponents && 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.MedianFeatureScale, B.MedianFeatureScale) && A.MedianVerticalClearance == B.MedianVerticalClearance @@ -73,34 +94,45 @@ namespace && A.SampledMaxZ == B.SampledMaxZ; } - bool IsInsideMeasuredWindow( - const FVoxelStrateMetrics& Metrics, - const FVoxelStrateMeasureSettings& Settings, - const FVector& Point) + const TCHAR* ConnectivityResultName(EVoxelConnectivityResult Result) { - 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); + switch (Result) + { + case EVoxelConnectivityResult::Connected: return TEXT("CONNECTED"); + case EVoxelConnectivityResult::NotConnected: return TEXT("NOT_CONNECTED"); + case EVoxelConnectivityResult::StartCellSolid: return TEXT("START_CELL_SOLID"); + case EVoxelConnectivityResult::GoalCellSolid: return TEXT("GOAL_CELL_SOLID"); + case EVoxelConnectivityResult::OutOfWindow: return TEXT("OUT_OF_WINDOW"); + case EVoxelConnectivityResult::CoarseLied: return TEXT("COARSE_LIED"); + default: return TEXT("UNKNOWN"); + } } - FVector LargestComponentAnchor( - const FStrateSlot& Slot, - const FVector2D& CenterXY, - int32 SampleStep) + FString ConnectivityProbeText(const FConnectivityProbe& Probe) { - // 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. - // 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); + if (!Probe.bChecked) + { + return TEXT("NOT_CHECKED"); + } + + FString Text = ConnectivityResultName(Probe.Result); + if (Probe.bStartSnapped || Probe.bGoalSnapped) + { + 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( @@ -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. * 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. + * 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 return COARSE_LIED. */ int32 GapTopChunkZ = 0; @@ -349,6 +381,15 @@ 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.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.ResolvedMarginVoxels < 1 || Metrics.SampledMinZ >= Metrics.SampledMaxZ) @@ -408,20 +449,22 @@ bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters) GapAirSamples, GapSamples); 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) { const FVoxelStrateMetrics& Old = Report.OldWindow; const FVoxelStrateMetrics& Derived = Report.DerivedWindow; Summary += FString::Printf( - 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"), + TEXT(" %s | %d | [%d,%d) | %.9g | %.9g | %lld | %d | %.9g | %.9g | %d | %d | %lld | %lld | " + "%d | [%d,%d) | %.9g | %.9g | %lld | %d | %.9g | %.9g | %d | %d | %lld | %lld\n"), *Report.Name, Old.ResolvedMarginVoxels, Old.SampledMinZ, Old.SampledMaxZ, Old.AirFraction, Old.LargestComponentShare, + Old.LargestComponentCells, + Old.NumComponentsAtLeast1Pct, Old.WalkableFraction, Old.MedianFeatureScale, Old.MedianVerticalClearance, @@ -433,6 +476,8 @@ bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters) Derived.SampledMaxZ, Derived.AirFraction, Derived.LargestComponentShare, + Derived.LargestComponentCells, + Derived.NumComponentsAtLeast1Pct, Derived.WalkableFraction, Derived.MedianFeatureScale, Derived.MedianVerticalClearance, @@ -443,6 +488,81 @@ bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters) int32 NumRoutesChecked = 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) { 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, 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, - DerivedWindowSettings, - bCoarseLied); - ++NumRoutesChecked; - if (bCoarseLied) ++NumRoutesRefuted; - if (bConnected && FullResolutionDirectSegmentHasSolid(*World.Generator, A, B)) + const FConnectivityProbe Probe = ProbeRoute( + StrateIndex, A, B, /*bCountInGuard=*/true, /*bCountAsAnchorProbe=*/false, + /*bCountAsArrivalDeparture=*/false); + if (Probe.Result == EVoxelConnectivityResult::Connected + && 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."), + TEXT("HARD FAILURE: VF_AreConnected returned CONNECTED for %s while its " + "full-resolution direct walk contained a solid sample."), *ArchetypeName(Slot))); bAllChecksPassed = false; } @@ -498,102 +611,315 @@ bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters) bAllChecksPassed = false; } - bool bWallCoarseLied = false; - const bool bWallConnected = VF_AreConnected( - *World.Generator, - *World.StrateManager, - 0, - WallA, - WallB, - DerivedWindowSettings, - bWallCoarseLied); - ++NumRoutesChecked; - if (bWallCoarseLied) ++NumRoutesRefuted; - if (bWallConnected) + const FConnectivityProbe WallProbe = ProbeRoute( + 0, WallA, WallB, /*bCountInGuard=*/true, /*bCountAsAnchorProbe=*/false, + /*bCountAsArrivalDeparture=*/false); + if (WallProbe.Result != EVoxelConnectivityResult::CoarseLied) { - 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.")); + AddError(FString::Printf( + TEXT("HARD FAILURE: the controlled coarse-connectivity lie returned %s instead of " + "COARSE_LIED."), + ConnectivityResultName(WallProbe.Result))); bAllChecksPassed = false; } World.DiffLayer->Clear(); - TArray PassageReports; const TArray& Passages = World.StrateManager->GetPassages(); - PassageReports.Reserve(Passages.Num()); + TArray ArrivalPassageByStrate; + TArray 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) { const FVoxelPassage& Passage = Passages[PassageIndex]; - 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 (Passage.UpperStrateIndex == Passage.LowerStrateIndex) { - if (!Layout.IsValidIndex(EndpointStrateIndex)) return TEXT("invalid-strate"); - const FVoxelStrateMetrics& EndpointMetrics = - DerivedMetricsByIndex[EndpointStrateIndex]; - if (!EndpointMetrics.bValid) return TEXT("invalid-window"); - if (!IsInsideMeasuredWindow(EndpointMetrics, DerivedWindowSettings, Endpoint)) + if (SurfaceEntryPassageIndex == INDEX_NONE) { - return TEXT("out-of-window"); + SurfaceEntryPassageIndex = PassageIndex; } + continue; + } - const FStrateSlot& Slot = Layout[EndpointStrateIndex]; - const FVector Anchor = LargestComponentAnchor( - Slot, - DerivedWindowSettings.CenterXY, - DerivedWindowSettings.SampleStep); - bool bCoarseLied = false; - const bool bConnected = VF_AreConnected( - *World.Generator, - *World.StrateManager, - EndpointStrateIndex, - Endpoint, - Anchor, - DerivedWindowSettings, - bCoarseLied); - ++NumRoutesChecked; - if (bCoarseLied) ++NumRoutesRefuted; - return bConnected ? TEXT("CONNECTED_TO_LARGEST") : TEXT("NOT_CONNECTED"); - }; + if (Passage.LowerStrateIndex != Passage.UpperStrateIndex + 1) + { + AddError(FString::Printf( + TEXT("HARD FAILURE: passage %d is not between consecutive strates (%d -> %d)."), + PassageIndex, + Passage.UpperStrateIndex, + Passage.LowerStrateIndex)); + bAllChecksPassed = false; + continue; + } - Report.UpperResult = CheckEndpoint(Passage.UpperStrateIndex, Passage.UpperPoint); - Report.LowerResult = CheckEndpoint(Passage.LowerStrateIndex, Passage.LowerPoint); + if (!Layout.IsValidIndex(Passage.UpperStrateIndex) + || !Layout.IsValidIndex(Passage.LowerStrateIndex)) + { + 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; + } } - PassageReports.Sort([](const FPassageReport& A, const FPassageReport& B) + for (int32 StrateIndex = 0; StrateIndex < Layout.Num(); ++StrateIndex) { - return A.Index < B.Index; - }); + 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 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 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( - 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, 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( + TEXT("Endpoint snap repair: %d snapped endpoint events across all probes; %d in " + "%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(" passage %s: UpperPoint strate=%d (%.3f,%.3f,%.3f) => %s; " - "LowerPoint strate=%d (%.3f,%.3f,%.3f) => %s\n"), + TEXT(" %d %s | %s | %s | %s | %d | %.9g | %lld | %d | (%.3f,%.3f,%.3f)\n"), + Report.Index, *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); + *ArrivalToDeparture, + *ArrivalToLargest, + *DepartureToLargest, + Metrics.NumAirComponents, + Metrics.LargestComponentShare, + Metrics.LargestComponentCells, + Metrics.NumComponentsAtLeast1Pct, + Metrics.LargestComponentPoint.X, + Metrics.LargestComponentPoint.Y, + Metrics.LargestComponentPoint.Z); } AddInfo(Summary); diff --git a/Source/VoxelForge/Private/VoxelStrateMeasure.cpp b/Source/VoxelForge/Private/VoxelStrateMeasure.cpp index 25035cd..97b97d5 100644 --- a/Source/VoxelForge/Private/VoxelStrateMeasure.cpp +++ b/Source/VoxelForge/Private/VoxelStrateMeasure.cpp @@ -298,7 +298,10 @@ namespace VoxelStrateMeasurePrivate const FSampleGrid& Grid, TArray& OutComponents, int32& OutNumComponents, - int32& OutLargestComponentCells) + int64 NumAir, + int64& OutLargestComponentCells, + int32& OutLargestComponentLowestCell, + int32& OutNumComponentsAtLeast1Pct) { OutComponents.SetNumUninitialized(Grid.CellCount); for (int32 Index = 0; Index < Grid.CellCount; ++Index) @@ -310,6 +313,8 @@ namespace VoxelStrateMeasurePrivate Queue.SetNumUninitialized(Grid.CellCount); OutNumComponents = 0; OutLargestComponentCells = 0; + OutLargestComponentLowestCell = INDEX_NONE; + OutNumComponentsAtLeast1Pct = 0; for (int32 Start = 0; Start < Grid.CellCount; ++Start) { @@ -353,7 +358,20 @@ namespace VoxelStrateMeasurePrivate if (Z > 0) Visit(Grid.Index(X, Y, Z - 1)); } - OutLargestComponentCells = FMath::Max(OutLargestComponentCells, ComponentCells); + const int64 ComponentCells64 = static_cast(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 Components; int32 NumComponents = 0; - int32 LargestComponentCells = 0; - FloodFillAir(Grid, Components, NumComponents, LargestComponentCells); int64 NumAir = 0; for (const uint8 bAir : Grid.Air) @@ -438,16 +454,38 @@ namespace VoxelStrateMeasurePrivate 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.NumAir = NumAir; InOutMetrics.NumSolid = static_cast(Grid.CellCount) - NumAir; InOutMetrics.AirFraction = static_cast( static_cast(NumAir) / static_cast(Grid.CellCount)); InOutMetrics.NumAirComponents = NumComponents; + InOutMetrics.LargestComponentCells = LargestComponentCells; + InOutMetrics.NumComponentsAtLeast1Pct = NumComponentsAtLeast1Pct; InOutMetrics.LargestComponentShare = NumAir > 0 ? static_cast(static_cast(LargestComponentCells) / static_cast(NumAir)) : 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 ClearanceValues; ClearanceValues.Reserve(static_cast(NumAir)); @@ -537,6 +575,93 @@ namespace VoxelStrateMeasurePrivate 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( const UVoxelGenerator& Generator, const FVector& Start, @@ -706,20 +831,22 @@ FVoxelStrateMetrics VF_MeasureStrate( return Result; } -bool VF_AreConnected( +EVoxelConnectivityResult VF_AreConnected( const UVoxelGenerator& Generator, const UVoxelStrateManager& Manager, int32 StrateIndex, const FVector& AVoxel, const FVector& BVoxel, 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) || !FMath::IsFinite(BVoxel.X) || !FMath::IsFinite(BVoxel.Y) || !FMath::IsFinite(BVoxel.Z)) { - return false; + return EVoxelConnectivityResult::OutOfWindow; } VoxelStrateMeasurePrivate::FSampleGrid Grid; @@ -727,21 +854,35 @@ bool VF_AreConnected( if (!VoxelStrateMeasurePrivate::BuildSampleGrid( Generator, Manager, StrateIndex, Settings, Grid, RefusalReason)) { - return false; + return EVoxelConnectivityResult::OutOfWindow; } int32 Start = -1; int32 Goal = -1; - if (!VoxelStrateMeasurePrivate::FindCellForPoint(Grid, AVoxel, Start) - || !VoxelStrateMeasurePrivate::FindCellForPoint(Grid, BVoxel, Goal)) + const VoxelStrateMeasurePrivate::EEndpointCellResult StartCell = + 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 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 @@ -749,8 +890,7 @@ bool VF_AreConnected( if (!VoxelStrateMeasurePrivate::FullResolutionPathIsAir( Generator, Grid, AVoxel, BVoxel, Path)) { - bOutCoarseLied = true; - return false; + return EVoxelConnectivityResult::CoarseLied; } - return true; + return EVoxelConnectivityResult::Connected; } diff --git a/Source/VoxelForge/Public/VoxelStrateMeasure.h b/Source/VoxelForge/Public/VoxelStrateMeasure.h index 837d7c3..506a915 100644 --- a/Source/VoxelForge/Public/VoxelStrateMeasure.h +++ b/Source/VoxelForge/Public/VoxelStrateMeasure.h @@ -7,6 +7,17 @@ class UVoxelGenerator; 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. */ struct VOXELFORGE_API FVoxelStrateMeasureSettings { @@ -31,6 +42,9 @@ struct VOXELFORGE_API FVoxelStrateMetrics float AirFraction = 0.f; int32 NumAirComponents = 0; 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 MedianFeatureScale = 0.f; 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. * - * 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. + * If an endpoint's own coarse cell is solid, the query searches the 26 neighbouring cells and + * 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 UVoxelStrateManager& Manager, int32 StrateIndex, const FVector& AVoxel, const FVector& BVoxel, const FVoxelStrateMeasureSettings& Settings, - bool& bOutCoarseLied); + bool& bOutStartSnapped, + bool& bOutGoalSnapped);