From 5519b323c8ed0f424c2740102e6ca4cbbd17085e Mon Sep 17 00:00:00 2001 From: Fr0zka Date: Sun, 30 Aug 2026 15:10:00 +0200 Subject: [PATCH] test(measure): the spine is the backbone in 7 of 8 strates -- and not in VerticalShafts Diagnosis only. No generation or placement changed. The hypothesis was that the (0,0) spine (OriginSpineRadius 14) is the universal connector, so "can this mouth reach the spine" would be the real primordial-law test. Measured at margin 0, step 2, radius 192: strate spine share largest? arr->spine dep->spine TunnelNetwork 0.979 YES CONNECTED not connected FlatPlain 1.000 YES CONNECTED CONNECTED CrystalChamber 1.000 YES CONNECTED CONNECTED Maze 0.997 YES coarse-lied CONNECTED SurfaceWorld 1.000 YES CONNECTED CONNECTED VerticalShafts 0.096 NO not conn. not connected FloatingIslands 1.000 YES CONNECTED CONNECTED Underwater 0.999 YES CONNECTED n/a The spine forms one uninterrupted open interior run in 8/8 strates, and holds essentially ALL the air in 7 of them. VerticalShafts is the lone anomaly: the spine punches through as an isolated column holding 9.6% of the air and is NOT the largest component. So the archetype has three mutually unreachable things -- the spine, the arrival network, the departure network. It has no lateral connectivity at all. Over 16 seeds: spine open 16/16, spine largest only 8/16, arrival->spine 1 connected, departure->spine 0 connected. Neither extreme was right. The spine is not a universal connector, but it is far from irrelevant: it is the de-facto backbone everywhere except here, which reframes the bug as an ANOMALY to bring in line rather than a missing feature to invent. Ordering hazard confirmed on the record: making passage i depend on passage i-1 would break LayoutOrderIndependence. Both candidate fixes avoid it, because the spine is a fixed deterministic anchor that two mouths can target independently. Build clean, 19 succeeded / 0 warnings / 0 failed. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Bp7FxoQcSoiuhxjuLr6aBM --- CODEMAP.md | 4 +- .../VoxelForgeStrateConnectivityTest.cpp | 742 +++++++++++++++++- .../VoxelForge/Private/VoxelStrateMeasure.cpp | 280 ++++--- Source/VoxelForge/Public/VoxelStrateMeasure.h | 23 +- 4 files changed, 903 insertions(+), 146 deletions(-) diff --git a/CODEMAP.md b/CODEMAP.md index 2ee39da..27ff29d 100644 --- a/CODEMAP.md +++ b/CODEMAP.md @@ -320,7 +320,7 @@ or PIE is required. |--------|------| | `FVoxelStrateMeasureSettings` / `FVoxelStrateMetrics` | Plain settings/result structs for bounded strate sampling and derived measurements; callers may override the interior margin (including zero to sample the seal), 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, with explicit endpoint-solid, out-of-window, `CoarseLied`, `NotConnectedAtThisResolution`, and connected outcomes. | +| `VF_AreConnected` | Coarse 6-connected BFS plus one full-resolution recheck of its recovered route, with explicit endpoint-solid, out-of-window, `CoarseLied`, `NotConnectedAtThisResolution`, and connected outcomes; the diagnostics overload returns endpoint component facts from that same grid. | | `VF_DiagnoseConnectivity` | The same query plus each mouth's air-component size/share and exact geometric distance to the nearest cell of the other component; proximity is measured in coarse-cell coordinates and does not claim a route through solid. | ### 3.7 Cave morphology (SDF rooms/tunnels) — `Public/VoxelCaveMorphology.h` + `.cpp` @@ -486,7 +486,7 @@ The plugin's first tests (`OPSTACK-PLAN.md` Phase 0.5). Run them from the editor | `VoxelForgeStrateParamCoverageTest.cpp` | `VoxelForge.Determinism.StrateParamBlendCoverage` | **The X-macro guard** (added 2026-08-17). `FStrateGenerationParams::Lerp` blends the hand-written `VF_STRATE_PARAM_FIELDS` list, **not** the struct — so a field added to one and not the other compiles, tests green, and silently takes its **default** inside every Gradient/Interleaved transition band. This expands the X-macro a **third** way (after LERP and SNAP), into a name list, and diffs it against the struct's UObject reflection. Pure shape test: no fixture, no world, instant. `GExemptFieldNames` is **empty** — every reflected field is covered today, and any exemption must be written down as a decision. Stakes rise with the world composer, which intends to invent parameter sets through this same `Lerp` (`COMPOSER-NOTES.md`). | | `VoxelForgeLayoutOrderIndependenceTest.cpp` | `VoxelForge.Determinism.LayoutOrderIndependence` | Builds a known transient soft-pointer pool, then rebuilds it in original, reversed, and swapped orders. Requires a non-empty layout and passage set, and compares every slot's definition/Z/height plus passage endpoints, radius, type, control geometry, and bounds bit-for-bit. | | `VoxelForgePassageOpenSpaceTest.cpp` | `VoxelForge.Determinism.PassageLandsInOpenSpace` | Uses the real fixture density path to check every generated inter-strate passage whose destination query answers: a 16-point ring outside the mouth's carve/blend band has at least half its samples in destination air, and the endpoint matches the pure open-point result within the mouth's float envelope. This is a connectivity proxy, not a flood-fill proof. Reports checked passages and false/unanswerable archetypes; fails if it inspects zero passages. | -| `VoxelForgeStrateConnectivityTest.cpp` | `VoxelForge.Generation.StrateConnectivity` / `VoxelForge.Generation.StrateConnectivityRefinement` | Bounded strate metrics with density-polarity and solid-gap controls, deterministic route rechecks, and a VerticalShafts refinement sweep. The refinement test explicitly checks margins 0/2/4/8 at step 2/radius 192, then conditionally reports mouth components/proximity and a 16-seed arrival→departure result. | +| `VoxelForgeStrateConnectivityTest.cpp` | `VoxelForge.Generation.StrateConnectivity` / `VoxelForge.Generation.StrateConnectivityRefinement` | Bounded strate metrics with density-polarity and solid-gap controls, deterministic route rechecks, and refinement sweeps. The refinement test explicitly checks margins 0/2/4/8 at step 2/radius 192, reports the `(0,0)` spine's full-window/interior vertical run and component for all eight fixture strates plus arrival/departure→spine and arrival→departure enums, then repeats those spine/mouth measurements across 16 VerticalShafts seeds. | ## 4. The density pipeline (most-edited hot path) diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeStrateConnectivityTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeStrateConnectivityTest.cpp index f9bd2a6..eebe674 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeStrateConnectivityTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgeStrateConnectivityTest.cpp @@ -276,6 +276,341 @@ namespace } return ArrivalCount == 1 && DepartureCount == 1; } + + struct FSpineColumnReport + { + bool bValid = false; + int32 SampledMinZ = 0; + int32 SampledMaxZ = 0; + int64 NumSamples = 0; + int64 NumAir = 0; + float AirFraction = 0.0f; + bool bFullWindowRun = false; + bool bAirSamplesFormOneRun = false; + int32 FirstOpenZ = INDEX_NONE; + int32 LastOpenZ = INDEX_NONE; + int32 LongestOpenRun = 0; + int32 RepresentativeOpenZ = INDEX_NONE; + TArray AirByZ; + }; + + bool SampleOriginSpineColumn( + const UVoxelGenerator& Generator, + int32 SampledMinZ, + int32 SampledMaxZ, + FSpineColumnReport& OutReport) + { + OutReport = FSpineColumnReport(); + if (SampledMaxZ <= SampledMinZ) + { + return false; + } + + const int64 NumSamples64 = static_cast(SampledMaxZ) + - static_cast(SampledMinZ); + if (NumSamples64 <= 0 || NumSamples64 > INT32_MAX) + { + return false; + } + + OutReport.SampledMinZ = SampledMinZ; + OutReport.SampledMaxZ = SampledMaxZ; + OutReport.NumSamples = NumSamples64; + OutReport.AirByZ.SetNumUninitialized(static_cast(NumSamples64)); + + bool bSawNonFiniteDensity = false; + int32 CurrentRun = 0; + for (int32 SampleIndex = 0; SampleIndex < OutReport.AirByZ.Num(); ++SampleIndex) + { + const int32 Z = SampledMinZ + SampleIndex; + const float Density = Generator.GetDensityAt(0.0f, 0.0f, static_cast(Z)); + const bool bAir = FMath::IsFinite(Density) && Density > 0.0f; + OutReport.AirByZ[SampleIndex] = bAir ? 1u : 0u; + if (!FMath::IsFinite(Density)) + { + bSawNonFiniteDensity = true; + } + + if (bAir) + { + ++OutReport.NumAir; + ++CurrentRun; + OutReport.FirstOpenZ = OutReport.FirstOpenZ == INDEX_NONE + ? Z : OutReport.FirstOpenZ; + OutReport.LastOpenZ = Z; + OutReport.LongestOpenRun = FMath::Max(OutReport.LongestOpenRun, CurrentRun); + } + else + { + CurrentRun = 0; + } + } + + OutReport.AirFraction = static_cast( + static_cast(OutReport.NumAir) + / static_cast(OutReport.NumSamples)); + OutReport.bFullWindowRun = OutReport.NumAir == OutReport.NumSamples; + OutReport.bAirSamplesFormOneRun = OutReport.NumAir > 0 + && OutReport.LongestOpenRun == OutReport.NumAir; + OutReport.bValid = !bSawNonFiniteDensity; + + if (OutReport.NumAir > 0) + { + const int32 CentreZ = SampledMinZ + + static_cast(NumSamples64 / 2); + for (int32 Offset = 0; Offset < OutReport.AirByZ.Num(); ++Offset) + { + const int32 LowerZ = CentreZ - Offset; + if (LowerZ >= SampledMinZ && LowerZ < SampledMaxZ + && OutReport.AirByZ[LowerZ - SampledMinZ] != 0u) + { + OutReport.RepresentativeOpenZ = LowerZ; + break; + } + + const int32 UpperZ = CentreZ + Offset; + if (UpperZ >= SampledMinZ && UpperZ < SampledMaxZ + && OutReport.AirByZ[UpperZ - SampledMinZ] != 0u) + { + OutReport.RepresentativeOpenZ = UpperZ; + break; + } + } + } + + return OutReport.bValid; + } + + bool FindNearestOpenSpinePoint( + const UVoxelGenerator& Generator, + const FSpineColumnReport& Spine, + float TargetZ, + FVector& OutPoint) + { + if (!Spine.bValid || Spine.NumAir <= 0 || !FMath::IsFinite(TargetZ)) + { + return false; + } + + if (TargetZ >= static_cast(Spine.SampledMinZ) + && TargetZ < static_cast(Spine.SampledMaxZ)) + { + const float Density = Generator.GetDensityAt(0.0f, 0.0f, TargetZ); + if (FMath::IsFinite(Density) && Density > 0.0f) + { + OutPoint = FVector(0.0f, 0.0f, TargetZ); + return true; + } + } + + const int32 NearestZ = FMath::Clamp( + FMath::RoundToInt(TargetZ), Spine.SampledMinZ, Spine.SampledMaxZ - 1); + for (int32 Offset = 0; Offset < Spine.AirByZ.Num(); ++Offset) + { + const int32 LowerZ = NearestZ - Offset; + if (LowerZ >= Spine.SampledMinZ && LowerZ < Spine.SampledMaxZ + && Spine.AirByZ[LowerZ - Spine.SampledMinZ] != 0u) + { + OutPoint = FVector(0.0f, 0.0f, static_cast(LowerZ)); + return true; + } + + const int32 UpperZ = NearestZ + Offset; + if (UpperZ >= Spine.SampledMinZ && UpperZ < Spine.SampledMaxZ + && Spine.AirByZ[UpperZ - Spine.SampledMinZ] != 0u) + { + OutPoint = FVector(0.0f, 0.0f, static_cast(UpperZ)); + return true; + } + } + return false; + } + + struct FSpineDiagnosisReport + { + int32 Seed = INDEX_NONE; + int32 Index = INDEX_NONE; + FString Name; + FSpineColumnReport Spine; + bool bHasArrival = false; + bool bArrivalIsSurfaceEntry = false; + bool bHasChainArrival = false; + bool bHasDeparture = false; + FVector ArrivalPoint = FVector::ZeroVector; + FVector DeparturePoint = FVector::ZeroVector; + FVector ArrivalSpinePoint = FVector::ZeroVector; + FVector DepartureSpinePoint = FVector::ZeroVector; + FVector RepresentativeSpinePoint = FVector::ZeroVector; + FConnectivityProbe SpineComponentProbe; + FConnectivityProbe ArrivalToSpine; + FConnectivityProbe DepartureToSpine; + FConnectivityProbe ArrivalToDeparture; + FVoxelConnectivityDiagnostics SpineComponent; + FVoxelConnectivityDiagnostics ArrivalToSpineFacts; + FVoxelConnectivityDiagnostics DepartureToSpineFacts; + }; + + bool FindDiagnosticMouths( + const TArray& Passages, + int32 StrateIndex, + FSpineDiagnosisReport& OutReport) + { + int32 ChainArrivalCount = 0; + int32 SurfaceArrivalCount = 0; + int32 DepartureCount = 0; + for (const FVoxelPassage& Passage : Passages) + { + if (Passage.LowerStrateIndex == StrateIndex + && Passage.UpperStrateIndex + 1 == StrateIndex) + { + OutReport.ArrivalPoint = Passage.LowerPoint; + ++ChainArrivalCount; + } + if (StrateIndex == 0 + && Passage.UpperStrateIndex == 0 + && Passage.LowerStrateIndex == 0) + { + OutReport.ArrivalPoint = Passage.LowerPoint; + ++SurfaceArrivalCount; + } + if (Passage.UpperStrateIndex == StrateIndex + && Passage.LowerStrateIndex == StrateIndex + 1) + { + OutReport.DeparturePoint = Passage.UpperPoint; + ++DepartureCount; + } + } + + if (ChainArrivalCount > 1 || SurfaceArrivalCount > 1 || DepartureCount > 1) + { + return false; + } + if (StrateIndex == 0) + { + OutReport.bHasArrival = SurfaceArrivalCount == 1; + OutReport.bArrivalIsSurfaceEntry = OutReport.bHasArrival; + OutReport.bHasChainArrival = ChainArrivalCount == 1; + } + else + { + OutReport.bHasArrival = ChainArrivalCount == 1; + OutReport.bHasChainArrival = OutReport.bHasArrival; + } + OutReport.bHasDeparture = DepartureCount == 1; + return true; + } + + bool BuildSpineDiagnosisReport( + const UVoxelGenerator& Generator, + const UVoxelStrateManager& Manager, + int32 StrateIndex, + const FVoxelStrateMeasureSettings& Settings, + FSpineDiagnosisReport& OutReport) + { + OutReport = FSpineDiagnosisReport(); + OutReport.Index = StrateIndex; + const TArray& Layout = Manager.GetLayout(); + if (!Layout.IsValidIndex(StrateIndex) || Layout[StrateIndex].Definition == nullptr) + { + return false; + } + OutReport.Name = ArchetypeName(Layout[StrateIndex]); + + if (Settings.InteriorMarginVoxels != 0) + { + return false; + } + const int64 MinZ64 = static_cast(Layout[StrateIndex].BottomChunkZ) * CHUNK_SIZE; + const int64 MaxZ64 = (static_cast(Layout[StrateIndex].TopChunkZ) + 1) * CHUNK_SIZE; + if (MinZ64 < INT32_MIN || MinZ64 > INT32_MAX + || MaxZ64 < INT32_MIN || MaxZ64 > INT32_MAX) + { + return false; + } + if (!SampleOriginSpineColumn( + Generator, + static_cast(MinZ64), + static_cast(MaxZ64), + OutReport.Spine)) + { + return false; + } + + if (OutReport.Spine.RepresentativeOpenZ != INDEX_NONE) + { + OutReport.RepresentativeSpinePoint = FVector( + 0.0f, 0.0f, static_cast(OutReport.Spine.RepresentativeOpenZ)); + OutReport.SpineComponentProbe.bChecked = true; + OutReport.SpineComponentProbe.Result = VF_AreConnected( + Generator, + Manager, + StrateIndex, + OutReport.RepresentativeSpinePoint, + OutReport.RepresentativeSpinePoint, + Settings, + OutReport.SpineComponentProbe.bStartSnapped, + OutReport.SpineComponentProbe.bGoalSnapped, + OutReport.SpineComponent); + } + + if (!FindDiagnosticMouths(Manager.GetPassages(), StrateIndex, OutReport)) + { + return false; + } + + auto ProbeMouthToSpine = [&](const FVector& Mouth, + FVector& OutSpinePoint, + FConnectivityProbe& OutProbe, + FVoxelConnectivityDiagnostics& OutFacts) + { + if (!FindNearestOpenSpinePoint(Generator, OutReport.Spine, Mouth.Z, OutSpinePoint)) + { + OutSpinePoint = FVector(0.0f, 0.0f, Mouth.Z); + } + OutProbe.bChecked = true; + OutProbe.Result = VF_AreConnected( + Generator, + Manager, + StrateIndex, + Mouth, + OutSpinePoint, + Settings, + OutProbe.bStartSnapped, + OutProbe.bGoalSnapped, + OutFacts); + }; + + if (OutReport.bHasArrival) + { + ProbeMouthToSpine( + OutReport.ArrivalPoint, + OutReport.ArrivalSpinePoint, + OutReport.ArrivalToSpine, + OutReport.ArrivalToSpineFacts); + } + if (OutReport.bHasDeparture) + { + ProbeMouthToSpine( + OutReport.DeparturePoint, + OutReport.DepartureSpinePoint, + OutReport.DepartureToSpine, + OutReport.DepartureToSpineFacts); + } + if (OutReport.bHasChainArrival && OutReport.bHasDeparture) + { + OutReport.ArrivalToDeparture.bChecked = true; + OutReport.ArrivalToDeparture.Result = VF_AreConnected( + Generator, + Manager, + StrateIndex, + OutReport.ArrivalPoint, + OutReport.DeparturePoint, + Settings, + OutReport.ArrivalToDeparture.bStartSnapped, + OutReport.ArrivalToDeparture.bGoalSnapped); + } + return true; + } } bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters) @@ -1229,6 +1564,121 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet && !Row.Probe.bGoalSnapped; } + FVoxelStrateMeasureSettings DiagnosticSettings = BaseSettings; + DiagnosticSettings.SampleStep = 2; + DiagnosticSettings.RadiusInVoxels = 192; + DiagnosticSettings.InteriorMarginVoxels = 0; + + TArray SpineReports; + SpineReports.SetNum(Layout.Num()); + bool bSpineDiagnosisUsable = true; + const double SpineDiagnosisStartSeconds = FPlatformTime::Seconds(); + for (int32 StrateIndex = 0; StrateIndex < Layout.Num(); ++StrateIndex) + { + FSpineDiagnosisReport& Report = SpineReports[StrateIndex]; + if (!BuildSpineDiagnosisReport( + *World.Generator, + *World.StrateManager, + StrateIndex, + DiagnosticSettings, + Report)) + { + AddError(FString::Printf( + TEXT("HARD FAILURE: strate %d could not produce a bounded origin-spine " + "diagnostic at margin 0."), + StrateIndex)); + bAllChecksPassed = false; + bSpineDiagnosisUsable = false; + continue; + } + + if (Report.Spine.NumSamples <= 0 + || !FMath::IsFinite(Report.Spine.AirFraction) + || Report.Spine.AirFraction < 0.0f + || Report.Spine.AirFraction > 1.0f + || Report.Spine.SampledMinZ >= Report.Spine.SampledMaxZ) + { + AddError(FString::Printf( + TEXT("HARD FAILURE: %s origin-spine column report is out of range."), + *Report.Name)); + bAllChecksPassed = false; + bSpineDiagnosisUsable = false; + } + + if (!Report.bHasArrival) + { + AddError(FString::Printf( + TEXT("HARD FAILURE: %s has no arrival mouth for the spine diagnosis."), + *Report.Name)); + bAllChecksPassed = false; + bSpineDiagnosisUsable = false; + } + if (StrateIndex + 1 < Layout.Num() && !Report.bHasDeparture) + { + AddError(FString::Printf( + TEXT("HARD FAILURE: %s has no departure mouth for the spine diagnosis."), + *Report.Name)); + bAllChecksPassed = false; + bSpineDiagnosisUsable = false; + } + + if (Report.Spine.RepresentativeOpenZ != INDEX_NONE) + { + if (!Report.SpineComponent.bValid + || Report.SpineComponentProbe.Result != EVoxelConnectivityResult::Connected + || Report.SpineComponentProbe.bStartSnapped + || Report.SpineComponentProbe.bGoalSnapped + || Report.SpineComponent.GoalComponentCells <= 0) + { + AddError(FString::Printf( + TEXT("HARD FAILURE: %s origin-spine component probe was not a stable " + "self-connected air query: %s."), + *Report.Name, + *ConnectivityProbeText(Report.SpineComponentProbe))); + bAllChecksPassed = false; + bSpineDiagnosisUsable = false; + } + } + } + const double SpineDiagnosisSeconds = FPlatformTime::Seconds() - SpineDiagnosisStartSeconds; + + int32 NumSpineFullWindowRuns = 0; + int32 NumSpineOneOpenRuns = 0; + int32 NumSpineLargestComponents = 0; + int32 NumMouthToSpineQueries = 0; + int32 NumMouthToSpineConnected = 0; + int32 NumMouthToSpineCoarseLied = 0; + for (const FSpineDiagnosisReport& Report : SpineReports) + { + if (Report.Spine.bFullWindowRun) + { + ++NumSpineFullWindowRuns; + } + if (Report.Spine.bAirSamplesFormOneRun) + { + ++NumSpineOneOpenRuns; + } + if (Report.SpineComponent.GoalComponentCells > 0 + && Report.SpineComponent.bGoalComponentIsLargest) + { + ++NumSpineLargestComponents; + } + const FConnectivityProbe* MouthProbes[] = { + Report.bHasArrival ? &Report.ArrivalToSpine : nullptr, + Report.bHasDeparture ? &Report.DepartureToSpine : nullptr, + }; + for (const FConnectivityProbe* Probe : MouthProbes) + { + if (Probe == nullptr) + { + continue; + } + ++NumMouthToSpineQueries; + NumMouthToSpineConnected += Probe->Result == EVoxelConnectivityResult::Connected; + NumMouthToSpineCoarseLied += Probe->Result == EVoxelConnectivityResult::CoarseLied; + } + } + FString Summary = TEXT( "VerticalShafts arrival->departure refinement (seed 1337; source mouth pair; " "derived interior window):\n"); @@ -1293,13 +1743,104 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet : TEXT("FINDING SURVIVES — no margin-0/2/4 query is CONNECTED; the full window is exonerated"), MarginSweepSeconds); + Summary += FString::Printf( + TEXT("Origin-spine diagnosis (seed 1337; OriginSpineRadius=%.3f; margin 0; " + "SampleStep=2; RadiusInVoxels=192; column sampled at every integer Z):\n"), + World.Generator->OriginSpineRadius); + Summary += TEXT( + " strate | measured window | spine open? (air fraction; full-window run; one open run; " + "open Z span) | spine component share | spine is largest? | arrival->spine | " + "departure->spine | arrival->departure\n"); + for (const FSpineDiagnosisReport& Report : SpineReports) + { + const FSpineColumnReport& Spine = Report.Spine; + const FVoxelConnectivityDiagnostics& SpineComponent = Report.SpineComponent; + const FString OpenSpan = Spine.FirstOpenZ == INDEX_NONE + ? TEXT("none") + : FString::Printf(TEXT("[%d,%d]"), Spine.FirstOpenZ, Spine.LastOpenZ); + const FString SpineOpenText = FString::Printf( + TEXT("air=%.9g; full=%s; one-run=%s; open=%s"), + Spine.AirFraction, + Spine.bFullWindowRun ? TEXT("YES") : TEXT("NO"), + Spine.bAirSamplesFormOneRun ? TEXT("YES") : TEXT("NO"), + *OpenSpan); + const FString SpineShareText = SpineComponent.GoalComponentCells > 0 + ? FString::Printf( + TEXT("%.9g (%lld/%lld)"), + SpineComponent.GoalComponentShare, + SpineComponent.GoalComponentCells, + SpineComponent.NumAirCells) + : TEXT("N/A"); + const FString SpineLargestText = SpineComponent.GoalComponentCells > 0 + ? (SpineComponent.bGoalComponentIsLargest ? TEXT("YES") : TEXT("NO")) + : TEXT("N/A"); + const FString ArrivalText = Report.bHasArrival + ? ConnectivityProbeText(Report.ArrivalToSpine) + : TEXT("MISSING_ARRIVAL"); + const FString DepartureText = Report.bHasDeparture + ? ConnectivityProbeText(Report.DepartureToSpine) + : TEXT("N/A (bottom-most; no departure)"); + FString ArrivalDepartureText; + if (Report.bHasChainArrival && Report.bHasDeparture) + { + ArrivalDepartureText = ConnectivityProbeText(Report.ArrivalToDeparture); + } + else if (Report.bArrivalIsSurfaceEntry && !Report.bHasChainArrival) + { + ArrivalDepartureText = TEXT("N/A (surface entry; no chain arrival)"); + } + else if (!Report.bHasDeparture && Report.Index + 1 == SpineReports.Num()) + { + ArrivalDepartureText = TEXT("N/A (bottom-most; no departure)"); + } + else + { + ArrivalDepartureText = TEXT("MISSING_CHAIN_MOUTH"); + } + + Summary += FString::Printf( + TEXT(" %d %s | [%d,%d) | %s | %s | %s | %s | %s | %s\n"), + Report.Index, + *Report.Name, + Spine.SampledMinZ, + Spine.SampledMaxZ, + *SpineOpenText, + *SpineShareText, + *SpineLargestText, + *ArrivalText, + *DepartureText, + *ArrivalDepartureText); + } + Summary += FString::Printf( + TEXT("Origin-spine diagnosis wall-clock: %.3f seconds; usable=%s.\n"), + SpineDiagnosisSeconds, + bSpineDiagnosisUsable ? TEXT("yes") : TEXT("no")); + Summary += FString::Printf( + TEXT("Interpretation counts: spine full-window run=%d/%d, one uninterrupted open run=%d/%d, " + "spine component largest=%d/%d; mouth->spine CONNECTED=%d/%d, COARSE_LIED=%d.\n"), + NumSpineFullWindowRuns, + SpineReports.Num(), + NumSpineOneOpenRuns, + SpineReports.Num(), + NumSpineLargestComponents, + SpineReports.Num(), + NumMouthToSpineConnected, + NumMouthToSpineQueries, + NumMouthToSpineCoarseLied); + Summary += TEXT( + "Interpretation: the spine is relevant and usually a backbone, but it is not a universal " + "connector. The VerticalShafts result supports a placement-first choice for this defect " + "because most fixture mouths reach the spine; it does not support treating the spine as " + "a guarantee for every archetype.\n"); + Summary += TEXT( + "Ordering-dependency hazard: making passage i's departure mouth depend on passage i-1's " + "arrival mouth would make layout generation a sequence. That conflicts with " + "VoxelForge.Determinism.LayoutOrderIndependence, which asserts the layout is a set; a " + "coupled implementation would be expected to break that test. No such dependency was " + "implemented here.\n"); + if (!bMarginArtifact && bMarginSweepUsable) { - FVoxelStrateMeasureSettings DiagnosticSettings = BaseSettings; - DiagnosticSettings.SampleStep = 2; - DiagnosticSettings.RadiusInVoxels = 192; - DiagnosticSettings.InteriorMarginVoxels = 0; - const FVoxelConnectivityDiagnostics Diagnostics = VF_DiagnoseConnectivity( *World.Generator, *World.StrateManager, @@ -1369,8 +1910,20 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet }; int32 SeedPasses = 0; int32 SeedFailures = 0; - int32 SeedSnapEvents = 0; - int32 SeedResultCounts[6] = {}; + int32 SeedArrivalToSpineSnapEvents = 0; + int32 SeedDepartureToSpineSnapEvents = 0; + int32 SeedArrivalToDepartureSnapEvents = 0; + int32 SeedArrivalToSpineResultCounts[6] = {}; + int32 SeedDepartureToSpineResultCounts[6] = {}; + int32 SeedArrivalToDepartureResultCounts[6] = {}; + int32 SeedFullWindowRuns = 0; + int32 SeedOneOpenRuns = 0; + int32 SeedSpineLargest = 0; + int32 SeedSpineNoOpenCell = 0; + float SeedSpineShareMin = FLT_MAX; + float SeedSpineShareMax = -FLT_MAX; + TArray SeedReports; + SeedReports.Reserve(UE_ARRAY_COUNT(SeedCases)); const double SeedSweepStartSeconds = FPlatformTime::Seconds(); for (const int32 Seed : SeedCases) { @@ -1381,43 +1934,91 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet AddError(FString::Printf( TEXT("HARD FAILURE: seed %d could not build the VerticalShafts fixture."), Seed)); + bAllChecksPassed = false; ++SeedFailures; continue; } - FVector SeedArrivalPoint = FVector::ZeroVector; - FVector SeedDeparturePoint = FVector::ZeroVector; - if (!FindChainMouths( - SeedWorld.StrateManager->GetPassages(), + FSpineDiagnosisReport& SeedReport = SeedReports.AddDefaulted_GetRef(); + if (!BuildSpineDiagnosisReport( + *SeedWorld.Generator, + *SeedWorld.StrateManager, VerticalShaftsIndex, - SeedArrivalPoint, - SeedDeparturePoint)) + DiagnosticSettings, + SeedReport)) + { + SeedReports.Pop(); + AddError(FString::Printf( + TEXT("HARD FAILURE: seed %d could not produce the VerticalShafts spine " + "diagnosis."), + Seed)); + bAllChecksPassed = false; + ++SeedFailures; + continue; + } + SeedReport.Seed = Seed; + + if (!SeedReport.bHasChainArrival || !SeedReport.bHasDeparture + || !SeedReport.ArrivalToDeparture.bChecked) { AddError(FString::Printf( TEXT("HARD FAILURE: seed %d did not produce exactly one VerticalShafts " "arrival and departure mouth."), Seed)); + bAllChecksPassed = false; ++SeedFailures; continue; } - bool bStartSnapped = false; - bool bGoalSnapped = false; - const EVoxelConnectivityResult SeedResult = VF_AreConnected( - *SeedWorld.Generator, - *SeedWorld.StrateManager, - VerticalShaftsIndex, - SeedArrivalPoint, - SeedDeparturePoint, - DiagnosticSettings, - bStartSnapped, - bGoalSnapped); - ++SeedResultCounts[static_cast(SeedResult)]; - const int32 SnapEvents = (bStartSnapped ? 1 : 0) + (bGoalSnapped ? 1 : 0); - SeedSnapEvents += SnapEvents; + const auto CountSeedResult = [](const FConnectivityProbe& Probe, int32 (&Counts)[6]) + { + if (Probe.bChecked) + { + ++Counts[static_cast(Probe.Result)]; + } + }; + CountSeedResult(SeedReport.ArrivalToSpine, SeedArrivalToSpineResultCounts); + CountSeedResult(SeedReport.DepartureToSpine, SeedDepartureToSpineResultCounts); + CountSeedResult(SeedReport.ArrivalToDeparture, SeedArrivalToDepartureResultCounts); + + SeedArrivalToSpineSnapEvents += + (SeedReport.ArrivalToSpine.bStartSnapped ? 1 : 0) + + (SeedReport.ArrivalToSpine.bGoalSnapped ? 1 : 0); + SeedDepartureToSpineSnapEvents += + (SeedReport.DepartureToSpine.bStartSnapped ? 1 : 0) + + (SeedReport.DepartureToSpine.bGoalSnapped ? 1 : 0); + SeedArrivalToDepartureSnapEvents += + (SeedReport.ArrivalToDeparture.bStartSnapped ? 1 : 0) + + (SeedReport.ArrivalToDeparture.bGoalSnapped ? 1 : 0); + + if (SeedReport.Spine.bFullWindowRun) + { + ++SeedFullWindowRuns; + } + if (SeedReport.Spine.bAirSamplesFormOneRun) + { + ++SeedOneOpenRuns; + } + if (SeedReport.SpineComponent.GoalComponentCells > 0) + { + if (SeedReport.SpineComponent.bGoalComponentIsLargest) + { + ++SeedSpineLargest; + } + SeedSpineShareMin = FMath::Min( + SeedSpineShareMin, SeedReport.SpineComponent.GoalComponentShare); + SeedSpineShareMax = FMath::Max( + SeedSpineShareMax, SeedReport.SpineComponent.GoalComponentShare); + } + else + { + ++SeedSpineNoOpenCell; + } + + const EVoxelConnectivityResult SeedResult = SeedReport.ArrivalToDeparture.Result; if (SeedResult == EVoxelConnectivityResult::Connected - && !bStartSnapped - && !bGoalSnapped) + && !SeedReport.ArrivalToDeparture.bStartSnapped + && !SeedReport.ArrivalToDeparture.bGoalSnapped) { ++SeedPasses; } @@ -1429,22 +2030,83 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet const double SeedSweepSeconds = FPlatformTime::Seconds() - SeedSweepStartSeconds; Summary += FString::Printf( - TEXT(" multi-seed arrival->departure sweep (%d seeds; margin 0, step 2, radius 192): " - "%d pass, %d fail, %d snapped endpoint events; wall-clock %.3f seconds.\n"), + TEXT(" multi-seed VerticalShafts diagnosis (%d seeds; margin 0, step 2, radius 192): " + "arrival->departure %d pass, %d fail, %d snapped endpoint events; " + "wall-clock %.3f seconds.\n"), UE_ARRAY_COUNT(SeedCases), SeedPasses, SeedFailures, - SeedSnapEvents, + SeedArrivalToDepartureSnapEvents, SeedSweepSeconds); Summary += FString::Printf( - TEXT(" seed result breakdown: CONNECTED=%d, NOT_CONNECTED_AT_THIS_RESOLUTION=%d, " - "START_CELL_SOLID=%d, GOAL_CELL_SOLID=%d, OUT_OF_WINDOW=%d, COARSE_LIED=%d.\n"), - SeedResultCounts[static_cast(EVoxelConnectivityResult::Connected)], - SeedResultCounts[static_cast(EVoxelConnectivityResult::NotConnectedAtThisResolution)], - SeedResultCounts[static_cast(EVoxelConnectivityResult::StartCellSolid)], - SeedResultCounts[static_cast(EVoxelConnectivityResult::GoalCellSolid)], - SeedResultCounts[static_cast(EVoxelConnectivityResult::OutOfWindow)], - SeedResultCounts[static_cast(EVoxelConnectivityResult::CoarseLied)]); + TEXT(" spine column sweep: full-window run=%d/%d, one-open-run=%d/%d, " + "component-largest=%d/%d, no open component=%d; component share range " + "[%.9g, %.9g].\n"), + SeedFullWindowRuns, + UE_ARRAY_COUNT(SeedCases), + SeedOneOpenRuns, + UE_ARRAY_COUNT(SeedCases), + SeedSpineLargest, + UE_ARRAY_COUNT(SeedCases), + SeedSpineNoOpenCell, + SeedSpineShareMin == FLT_MAX ? 0.0f : SeedSpineShareMin, + SeedSpineShareMax == -FLT_MAX ? 0.0f : SeedSpineShareMax); + Summary += FString::Printf( + TEXT(" seed result breakdown — arrival->spine: CONNECTED=%d, " + "NOT_CONNECTED_AT_THIS_RESOLUTION=%d, START_CELL_SOLID=%d, GOAL_CELL_SOLID=%d, " + "OUT_OF_WINDOW=%d, COARSE_LIED=%d; departure->spine: CONNECTED=%d, " + "NOT_CONNECTED_AT_THIS_RESOLUTION=%d, START_CELL_SOLID=%d, GOAL_CELL_SOLID=%d, " + "OUT_OF_WINDOW=%d, COARSE_LIED=%d; arrival->departure: CONNECTED=%d, " + "NOT_CONNECTED_AT_THIS_RESOLUTION=%d, START_CELL_SOLID=%d, GOAL_CELL_SOLID=%d, " + "OUT_OF_WINDOW=%d, COARSE_LIED=%d.\n"), + SeedArrivalToSpineResultCounts[static_cast(EVoxelConnectivityResult::Connected)], + SeedArrivalToSpineResultCounts[static_cast(EVoxelConnectivityResult::NotConnectedAtThisResolution)], + SeedArrivalToSpineResultCounts[static_cast(EVoxelConnectivityResult::StartCellSolid)], + SeedArrivalToSpineResultCounts[static_cast(EVoxelConnectivityResult::GoalCellSolid)], + SeedArrivalToSpineResultCounts[static_cast(EVoxelConnectivityResult::OutOfWindow)], + SeedArrivalToSpineResultCounts[static_cast(EVoxelConnectivityResult::CoarseLied)], + SeedDepartureToSpineResultCounts[static_cast(EVoxelConnectivityResult::Connected)], + SeedDepartureToSpineResultCounts[static_cast(EVoxelConnectivityResult::NotConnectedAtThisResolution)], + SeedDepartureToSpineResultCounts[static_cast(EVoxelConnectivityResult::StartCellSolid)], + SeedDepartureToSpineResultCounts[static_cast(EVoxelConnectivityResult::GoalCellSolid)], + SeedDepartureToSpineResultCounts[static_cast(EVoxelConnectivityResult::OutOfWindow)], + SeedDepartureToSpineResultCounts[static_cast(EVoxelConnectivityResult::CoarseLied)], + SeedArrivalToDepartureResultCounts[static_cast(EVoxelConnectivityResult::Connected)], + SeedArrivalToDepartureResultCounts[static_cast(EVoxelConnectivityResult::NotConnectedAtThisResolution)], + SeedArrivalToDepartureResultCounts[static_cast(EVoxelConnectivityResult::StartCellSolid)], + SeedArrivalToDepartureResultCounts[static_cast(EVoxelConnectivityResult::GoalCellSolid)], + SeedArrivalToDepartureResultCounts[static_cast(EVoxelConnectivityResult::OutOfWindow)], + SeedArrivalToDepartureResultCounts[static_cast(EVoxelConnectivityResult::CoarseLied)]); + Summary += FString::Printf( + TEXT(" seed endpoint snaps — arrival->spine=%d, departure->spine=%d, " + "arrival->departure=%d.\n"), + SeedArrivalToSpineSnapEvents, + SeedDepartureToSpineSnapEvents, + SeedArrivalToDepartureSnapEvents); + Summary += TEXT(" seed | spine air | full run | one open run | spine share | spine largest | " + "arrival->spine | departure->spine | arrival->departure\n"); + for (int32 SeedIndex = 0; SeedIndex < SeedReports.Num(); ++SeedIndex) + { + const FSpineDiagnosisReport& SeedReport = SeedReports[SeedIndex]; + const FString SpineShare = SeedReport.SpineComponent.GoalComponentCells > 0 + ? FString::Printf( + TEXT("%.9g"), SeedReport.SpineComponent.GoalComponentShare) + : TEXT("N/A"); + const FString SpineLargest = SeedReport.SpineComponent.GoalComponentCells > 0 + ? (SeedReport.SpineComponent.bGoalComponentIsLargest ? TEXT("YES") : TEXT("NO")) + : TEXT("N/A"); + Summary += FString::Printf( + TEXT(" %d | %.9g | %s | %s | %s | %s | %s | %s | %s\n"), + SeedReport.Seed, + SeedReport.Spine.AirFraction, + SeedReport.Spine.bFullWindowRun ? TEXT("YES") : TEXT("NO"), + SeedReport.Spine.bAirSamplesFormOneRun ? TEXT("YES") : TEXT("NO"), + *SpineShare, + *SpineLargest, + *ConnectivityProbeText(SeedReport.ArrivalToSpine), + *ConnectivityProbeText(SeedReport.DepartureToSpine), + *ConnectivityProbeText(SeedReport.ArrivalToDeparture)); + } } else { diff --git a/Source/VoxelForge/Private/VoxelStrateMeasure.cpp b/Source/VoxelForge/Private/VoxelStrateMeasure.cpp index 8435317..6beaa4c 100644 --- a/Source/VoxelForge/Private/VoxelStrateMeasure.cpp +++ b/Source/VoxelForge/Private/VoxelStrateMeasure.cpp @@ -891,6 +891,148 @@ namespace VoxelStrateMeasurePrivate ? -1.0f : FMath::Sqrt(static_cast(BestDistanceSquared)); } + + void PopulateConnectivityDiagnostics( + const FSampleGrid& Grid, + int32 Start, + int32 Goal, + EVoxelConnectivityResult ConnectivityResult, + bool bStartSnapped, + bool bGoalSnapped, + FVoxelConnectivityDiagnostics& OutDiagnostics) + { + OutDiagnostics = FVoxelConnectivityDiagnostics(); + OutDiagnostics.Result = ConnectivityResult; + OutDiagnostics.bStartSnapped = bStartSnapped; + OutDiagnostics.bGoalSnapped = bGoalSnapped; + OutDiagnostics.bValid = ConnectivityResult != EVoxelConnectivityResult::OutOfWindow + || (Start >= 0 && Goal >= 0); + if (Start < 0 || Goal < 0) + { + return; + } + + TArray Components; + int32 NumComponents = 0; + int64 LargestComponentCells = 0; + int32 LargestComponentLowestCell = INDEX_NONE; + int32 NumComponentsAtLeast1Pct = 0; + int64 NumAir = 0; + for (const uint8 bAir : Grid.Air) + { + NumAir += bAir != 0u ? 1 : 0; + } + FloodFillAir( + Grid, + Components, + NumComponents, + NumAir, + LargestComponentCells, + LargestComponentLowestCell, + NumComponentsAtLeast1Pct); + + const int32 StartComponent = Components[Start]; + const int32 GoalComponent = Components[Goal]; + if (StartComponent < 0 || GoalComponent < 0) + { + return; + } + + TArray ComponentCells; + ComponentCells.Init(0, NumComponents); + for (const int32 Component : Components) + { + if (Component >= 0) + { + ++ComponentCells[Component]; + } + } + + OutDiagnostics.NumAirCells = NumAir; + OutDiagnostics.StartComponentCells = ComponentCells[StartComponent]; + OutDiagnostics.GoalComponentCells = ComponentCells[GoalComponent]; + OutDiagnostics.LargestComponentCells = LargestComponentCells; + if (NumAir > 0) + { + OutDiagnostics.StartComponentShare = static_cast( + static_cast(OutDiagnostics.StartComponentCells) + / static_cast(NumAir)); + OutDiagnostics.GoalComponentShare = static_cast( + static_cast(OutDiagnostics.GoalComponentCells) + / static_cast(NumAir)); + OutDiagnostics.LargestComponentShare = static_cast( + static_cast(LargestComponentCells) + / static_cast(NumAir)); + } + OutDiagnostics.bStartComponentIsLargest = + OutDiagnostics.StartComponentCells == LargestComponentCells; + OutDiagnostics.bGoalComponentIsLargest = + OutDiagnostics.GoalComponentCells == LargestComponentCells; + OutDiagnostics.StartToGoalComponentDistanceCells = DistanceToComponent( + Grid, Start, GoalComponent, Components); + OutDiagnostics.GoalToStartComponentDistanceCells = DistanceToComponent( + Grid, Goal, StartComponent, Components); + } + + EVoxelConnectivityResult QueryConnectivity( + const UVoxelGenerator& Generator, + const UVoxelStrateManager& Manager, + int32 StrateIndex, + const FVector& AVoxel, + const FVector& BVoxel, + const FVoxelStrateMeasureSettings& Settings, + bool& bOutStartSnapped, + bool& bOutGoalSnapped, + FVoxelConnectivityDiagnostics* OutDiagnostics) + { + 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)) + { + if (OutDiagnostics != nullptr) + { + *OutDiagnostics = FVoxelConnectivityDiagnostics(); + } + return EVoxelConnectivityResult::OutOfWindow; + } + + FSampleGrid Grid; + FString RefusalReason; + if (!BuildSampleGrid(Generator, Manager, StrateIndex, Settings, Grid, RefusalReason)) + { + if (OutDiagnostics != nullptr) + { + *OutDiagnostics = FVoxelConnectivityDiagnostics(); + } + return EVoxelConnectivityResult::OutOfWindow; + } + + int32 Start = -1; + int32 Goal = -1; + const EVoxelConnectivityResult Result = EvaluateConnectivityOnGrid( + Generator, + Grid, + AVoxel, + BVoxel, + Start, + Goal, + bOutStartSnapped, + bOutGoalSnapped); + if (OutDiagnostics != nullptr) + { + PopulateConnectivityDiagnostics( + Grid, + Start, + Goal, + Result, + bOutStartSnapped, + bOutGoalSnapped, + *OutDiagnostics); + } + return Result; + } } FVoxelStrateMetrics VF_MeasureStrate( @@ -929,33 +1071,39 @@ EVoxelConnectivityResult VF_AreConnected( bool& bOutStartSnapped, bool& bOutGoalSnapped) { - 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 EVoxelConnectivityResult::OutOfWindow; - } - - VoxelStrateMeasurePrivate::FSampleGrid Grid; - FString RefusalReason; - if (!VoxelStrateMeasurePrivate::BuildSampleGrid( - Generator, Manager, StrateIndex, Settings, Grid, RefusalReason)) - { - return EVoxelConnectivityResult::OutOfWindow; - } - - int32 Start = -1; - int32 Goal = -1; - return VoxelStrateMeasurePrivate::EvaluateConnectivityOnGrid( + return VoxelStrateMeasurePrivate::QueryConnectivity( Generator, - Grid, + Manager, + StrateIndex, AVoxel, BVoxel, - Start, - Goal, + Settings, bOutStartSnapped, - bOutGoalSnapped); + bOutGoalSnapped, + nullptr); +} + +EVoxelConnectivityResult VF_AreConnected( + const UVoxelGenerator& Generator, + const UVoxelStrateManager& Manager, + int32 StrateIndex, + const FVector& AVoxel, + const FVector& BVoxel, + const FVoxelStrateMeasureSettings& Settings, + bool& bOutStartSnapped, + bool& bOutGoalSnapped, + FVoxelConnectivityDiagnostics& OutDiagnostics) +{ + return VoxelStrateMeasurePrivate::QueryConnectivity( + Generator, + Manager, + StrateIndex, + AVoxel, + BVoxel, + Settings, + bOutStartSnapped, + bOutGoalSnapped, + &OutDiagnostics); } FVoxelConnectivityDiagnostics VF_DiagnoseConnectivity( @@ -967,89 +1115,15 @@ FVoxelConnectivityDiagnostics VF_DiagnoseConnectivity( const FVoxelStrateMeasureSettings& Settings) { FVoxelConnectivityDiagnostics Result; - 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 Result; - } - - VoxelStrateMeasurePrivate::FSampleGrid Grid; - FString RefusalReason; - if (!VoxelStrateMeasurePrivate::BuildSampleGrid( - Generator, Manager, StrateIndex, Settings, Grid, RefusalReason)) - { - return Result; - } - - int32 Start = -1; - int32 Goal = -1; - Result.Result = VoxelStrateMeasurePrivate::EvaluateConnectivityOnGrid( + VoxelStrateMeasurePrivate::QueryConnectivity( Generator, - Grid, + Manager, + StrateIndex, AVoxel, BVoxel, - Start, - Goal, + Settings, Result.bStartSnapped, - Result.bGoalSnapped); - Result.bValid = Result.Result != EVoxelConnectivityResult::OutOfWindow - || (Start >= 0 && Goal >= 0); - if (Start < 0 || Goal < 0) - { - return Result; - } - - TArray Components; - int32 NumComponents = 0; - int64 LargestComponentCells = 0; - int32 LargestComponentLowestCell = INDEX_NONE; - int32 NumComponentsAtLeast1Pct = 0; - int64 NumAir = 0; - for (const uint8 bAir : Grid.Air) - { - NumAir += bAir != 0u ? 1 : 0; - } - FloodFillAir( - Grid, - Components, - NumComponents, - NumAir, - LargestComponentCells, - LargestComponentLowestCell, - NumComponentsAtLeast1Pct); - - const int32 StartComponent = Components[Start]; - const int32 GoalComponent = Components[Goal]; - if (StartComponent < 0 || GoalComponent < 0) - { - return Result; - } - - TArray ComponentCells; - ComponentCells.Init(0, NumComponents); - for (const int32 Component : Components) - { - if (Component >= 0) - { - ++ComponentCells[Component]; - } - } - - Result.NumAirCells = NumAir; - Result.StartComponentCells = ComponentCells[StartComponent]; - Result.GoalComponentCells = ComponentCells[GoalComponent]; - if (NumAir > 0) - { - Result.StartComponentShare = static_cast( - static_cast(Result.StartComponentCells) / static_cast(NumAir)); - Result.GoalComponentShare = static_cast( - static_cast(Result.GoalComponentCells) / static_cast(NumAir)); - } - Result.StartToGoalComponentDistanceCells = - VoxelStrateMeasurePrivate::DistanceToComponent( - Grid, Start, GoalComponent, Components); - Result.GoalToStartComponentDistanceCells = - VoxelStrateMeasurePrivate::DistanceToComponent( - Grid, Goal, StartComponent, Components); + Result.bGoalSnapped, + &Result); return Result; } diff --git a/Source/VoxelForge/Public/VoxelStrateMeasure.h b/Source/VoxelForge/Public/VoxelStrateMeasure.h index e4f98c3..b0b1bb2 100644 --- a/Source/VoxelForge/Public/VoxelStrateMeasure.h +++ b/Source/VoxelForge/Public/VoxelStrateMeasure.h @@ -75,8 +75,12 @@ struct VOXELFORGE_API FVoxelConnectivityDiagnostics int64 NumAirCells = 0; int64 StartComponentCells = 0; int64 GoalComponentCells = 0; + int64 LargestComponentCells = 0; float StartComponentShare = 0.0f; float GoalComponentShare = 0.0f; + float LargestComponentShare = 0.0f; + bool bStartComponentIsLargest = false; + bool bGoalComponentIsLargest = false; float StartToGoalComponentDistanceCells = -1.0f; float GoalToStartComponentDistanceCells = -1.0f; @@ -115,11 +119,28 @@ VOXELFORGE_API EVoxelConnectivityResult VF_AreConnected( bool& bOutStartSnapped, bool& bOutGoalSnapped); +/** + * The same connectivity query with component facts returned from the same sampled grid. This + * overload exists for diagnostics that need both the required route verdict and the component + * containing one endpoint without sampling the strate a second time. + */ +VOXELFORGE_API EVoxelConnectivityResult VF_AreConnected( + const UVoxelGenerator& Generator, + const UVoxelStrateManager& Manager, + int32 StrateIndex, + const FVector& AVoxel, + const FVector& BVoxel, + const FVoxelStrateMeasureSettings& Settings, + bool& bOutStartSnapped, + bool& bOutGoalSnapped, + FVoxelConnectivityDiagnostics& OutDiagnostics); + /** * Return the component sizes and spatial separation facts for the same query performed by * VF_AreConnected. The returned Result uses the same full-resolution route re-check. bValid means * the sample grid was built and both endpoint coordinates were in its bounds; component fields - * remain zero/negative when an endpoint has no air cell to analyze. + * remain zero/negative when an endpoint has no air cell to analyze. Largest-component membership + * treats equal-sized components as tied for largest. */ VOXELFORGE_API FVoxelConnectivityDiagnostics VF_DiagnoseConnectivity( const UVoxelGenerator& Generator,