diff --git a/CODEMAP.md b/CODEMAP.md index e92195f..8a94420 100644 --- a/CODEMAP.md +++ b/CODEMAP.md @@ -330,7 +330,9 @@ When both `CoverPointA` and `CoverPointB` are set, the XY window is their AABB e `CoverMarginVoxels`; otherwise it is the legacy square centered on `CenterXY` with `RadiusInVoxels`. It 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 +walkability, feature scale, and clearance from that grid. It then performs one bounded 4-neighbour +flood fill over the projected XY columns that contain a walkable surface; this is a floor-continuity +indicator, not another source-density sample or a 3D route proof. Metrics report the resolved margin and the inclusive/exclusive voxel Z window used, the exact sampled dimensions/bounds, and the largest component's deterministic lowest-cell representative point, cell count, and count of components holding at least 1% of the air. The flood fill also retains component cell counts in discovery order, @@ -354,7 +356,7 @@ has a full-resolution air-verified route, and the query reports the alternate ro 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 (including zero to sample the seal) or fit the XY window to two points with a margin, and results identify the resolved window/dimensions/Z range and deterministic component representatives/sizes. | +| `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) or fit the XY window to two points with a margin, and results identify the resolved window/dimensions/Z range, deterministic air-component facts, walkable floor-area columns/fraction, projected surface components/share, and median clearance. | | `FVoxelStrateSampleGrid` | Optional one-pass `density > 0` `Air` polarity plus exact scalar `Density` capture exported by measurement functions; bounded by `MaxCells`, consumed by editor/automation previews, never retained on ordinary metrics calls. | | `VF_MeasureStrate` | One-grid/one-flood-fill strate metrics; resolves either the legacy centered square or the two-point fitted AABB, and refuses invalid bounds or a grid over `MaxCells`. | | `VF_AreConnected` | Coarse 6-connected BFS plus deterministic blocked-edge retries, with a full-resolution air recheck for every candidate route and explicit endpoint-solid, out-of-window, `NotConnectedAtThisResolution`, `CoarseLiedBudgetExhausted` (unknown), and connected outcomes; diagnostics include endpoint component facts and the retry count from that same grid. | @@ -535,9 +537,11 @@ and scalar density=0 contour views side by side without blurring the sampled ras 512 pixels (including its scale footer). `FVoxelStrateFinePreviewSettings` is a separate caller-selected fine pass: the composer selects only survivors at step 1, radius 64 (a 128×128 XY ROI), and `MaxCells=2,000,000`; the caller centres it on each coarse survivor's `LargestComponentPoint` and the writer labels it -“largest open space” with its exact ROI and step; cap refusals are recorded in the page. The 64-card -sheet remains sorted by survivor status then descending corpus-centroid distance, repeats exact measurement -windows, marks rejection reasons, explains the 2D-connectivity limitation, and has no runtime generation hook. +“largest open space” with its exact ROI and step; cap refusals and blank fine ROIs are recorded explicitly +instead of showing empty evidence. The ordinary 64-card sheet remains sorted by survivor status then +descending corpus-centroid distance. The `VoxelForge.Composer.Showcase` test additionally writes an +alphabetized one-card-per-archetype PIE hand-off page with the Part A survivor distributions, exact seed / +candidate / target-slot values, and no runtime generation hook. **`Public/VoxelSeasonManifest.h` + `Private/VoxelSeasonManifest.cpp`** — Tier 4c's offline season artifact. `VF_ComposeSeason` generates a bounded configurable candidate batch, applies the Tier 2 @@ -621,6 +625,7 @@ The plugin's first tests (`OPSTACK-PLAN.md` Phase 0.5). Run them from the editor | ″ | `VoxelForge.Composer.TerrainDetailLiveness` | Fixed 4,096-point `GetDensityAt` lattice, legacy and operator-stack paths; changes one terrain-detail group at a time with an empty terrain-op pool. Proves 9 live groups / 23 fields and 3 dead groups / 11 fields; all 24 rows match. | | `VoxelForgeComposerPromotionTest.cpp` | `VoxelForge.Composer.Promotion` | Re-measures the 12 project/default members, simulates five deterministic 24-candidate seasons with normalized measured-metric novelty (`<0.20`), cap 6, cumulative JSON promotion, provenance counts, corpus-hash checks, fresh-load gate verification, spread/survival reporting, and deliberate stale-metric corruption. Final run: **9 promoted**, corpus **4/8/9**, survival **20.8/54.2/54.2/45.8/50.0%**, spread **0.869214→0.916302**, **63.556 s**, 0 failures. | | `VoxelForgeComposerStructureRollTest.cpp` | `VoxelForge.Composer.StructureRoll` | Rolls root polarity → legal shape source → polarity-derived conversion → 4–8 declaration-legal modifiers → mandatory structural posts; blends the six native parameter families independently, measures 64 novel stacks, captures the same grid for the deterministic filled/contour XZ/XY preview, runs a separate step-1 radius-64 ROI pass for the 42 survivors centred on `LargestComponentPoint`, checks exact arrival→departure connectivity, rerolls every recipe/stack for determinism, and brute-forces every uniform box verdict. Final run: **42/64 survival (65.6%)**, **64 distinct recipes**, **0 invalid recipes**, **64 coarse filled + 64 coarse contour pairs**, **42 fine filled + 42 fine contour pairs**, blank plan/card **6/42→3/42**, **243.649 s**, **0 refusals**. | +| `VoxelForgeComposerShowcaseTest.cpp` | `VoxelForge.Composer.Showcase` | Exhausts the bounded parameter-roll set (seeds **0, 7331**, indices **0–63**), excludes multi-region rolls while the lateral gate is off, and measures every missing-family candidate in all six interior target slots at step 4 / radius 256 / `MaxCells=8,000,000`. Hard gates are non-vacuous, largest air share ≥ **0.50**, and exact unsnapped arrival→departure connectivity; selection score is floor-area fraction + clearance tie-break + projected-surface tie-break. It asserts roll/manifest determinism, density sign, zero `WorldRadiusVoxels`, bit-identical metric reruns, and writes one alphabetized card per archetype to `Saved/VoxelForge/Showcase/index.html`, with step-1 radius-64 filled/contour plan + vertical ROI images centred on `LargestComponentPoint`. | | `VoxelForgeComposerSeasonTest.cpp` | `VoxelForge.Composer.Season` | Composes a six-slot offline season from 24 structure candidates; asserts **19 hard-gate survivors**, **6 selected**, deterministic byte-identical JSON, absolute descent order, all selected primordial-law facts, and a selected-only review page with metrics/reasons/boss markers. Loads the JSON, rebuilds every selected stack, and compares **393,216** bounded density samples bit-for-bit. Rejection audit: **4 vacuous**, **1 primordial-law budget**, **13 policy-not-selected**. | | `VoxelForgeComposerCorpusFreeTest.cpp` | `VoxelForge.Composer.CorpusFree` | Shares each structure recipe across today's corpus blend, naive independent uniform rolls, and constraint-sampled rolls. The completed equal-arm run uses **16 candidates per arm** (256×3 and 64×3 were stopped before aggregate output for runtime), step 4 / radius 256 / `MaxCells=8,000,000`, fixed passage-law mouths, and 40 box probes per candidate. Result: **13/16, 7/16, 7/16** survival; survivor walkable means **0.066071, 0.012566, 0.012851** and feature-scale means **92.307693, 64.571426, 31.428572**. Box checks: **196/260,876**, **310/412,610**, **262/348,722** proved/voxels, **0 violations** in every arm. | | `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. | diff --git a/COMPOSER-NOTES.md b/COMPOSER-NOTES.md index 9b607d4..d656a6b 100644 --- a/COMPOSER-NOTES.md +++ b/COMPOSER-NOTES.md @@ -840,6 +840,75 @@ capture allocation was **9,338,880 bytes** and the peak single RGBA raster was * full StructureRoll test completed in **243.649 s**, with 64 coarse filled pairs, 64 coarse contour pairs, 42 fine filled pairs, 42 fine contour pairs, and 0 refusals. +### ✅ Morning archetype showcase + walkable floor-area audit (2026-09-05) + +The owner-facing review artifact is Saved/VoxelForge/Showcase/index.html. It is generated only by +the editor automation test VoxelForge.Composer.Showcase; opening it is offline and has no runtime +generation hook. It contains one alphabetized card for each native archetype, with the exact four PIE +hand-off values, recipe, coarse step-4 filled/contour XZ+XY views, and a separate step-1 filled/contour +ROI centred on that card's coarse LargestComponentPoint. The fine ROI is bounded at radius 64 and +MaxCells=2,000,000; a refusal or blank ROI is written as text rather than represented by an empty +image. All eight cards in the final run rendered both fine views; refusals **0**, blank cards **0**. + +The Part A table adds three measurements to the original volumetric walkability ratio: + +* WalkableFloorAreaFraction = distinct sampled XY columns containing at least one walkable cell / + the sampled XY footprint. It is an area/footprint proxy, not a continuous world-area integral. +* MedianVerticalClearance is the lower median of the sampled air run above walkable cells, reported + in voxels. Both the walkability predicate and this median use the stated sample step. +* LargestWalkableSurfaceShare is the largest 4-neighbour connected component of the projected + walkable columns / all walkable columns. It is a 2D surface-continuity indicator, not a proof that + floors at different Z values connect in 3D; the separate exact unsnapped arrival→departure route + check remains the traversal gate. + +The showcase selection exhausts seeds **0** and **7331**, candidate indices **0–63**, and excludes +multi-region rolls while VF_LateralRegionsAreShippable() is false. The hard gates are non-vacuous, +largest air component share ≥ **0.50**, and exact unsnapped arrival→departure connectivity. Among +those survivors the deterministic score is floor_area + 0.25*clamp(clearance/64,0,1) + +0.10*largest_surface; floor area is primary, clearance and projected continuity are tie-breakers. +The fixture and PIE hand-off use the project generator seed **0**, bComposerRollStructure=false, +and leave the existing slot layout, passage endpoints, placement, and WorldRadiusVoxels=0 intact. + +Selected-card measurements (each card prints its own exact derived-margin Z window; common XY window +is [-256,256) × [-256,256) voxels, step **4**, HeadroomCells=2, MaxCells=8,000,000): + +| Archetype | walkable fraction | floor area fraction | median clearance | largest surface share | +|-----------|------------------:|--------------------:|-----------------:|----------------------:| +| CrystalChamber | 0.059105 | 0.999878 | 68 | 1.000000 | +| FlatPlain | 0.056107 | 0.999878 | 72 | 1.000000 | +| FloatingIslands | 0.018869 | 0.400146 | 36 | 0.678005 | +| Maze | 0.286020 | 0.290466 | 8 | 0.990544 | +| SurfaceWorld | 0.064233 | 0.937378 | 64 | 0.993619 | +| TunnelNetwork | 0.155047 | 0.208374 | 20 | 0.463972 | +| Underwater | 0.228995 | 0.206055 | 12 | 0.331161 | +| VerticalShafts | 0.108252 | 0.113770 | 12 | 0.144850 | + +The result is mixed, and that is the finding. CrystalChamber, FlatPlain, and SurfaceWorld retain +roughly **0.94–1.00** walkable XY footprint while their volumetric walkable ratios are only +**0.056–0.064**: the original ratio was primarily measuring corridor occupancy and was the wrong +quality target for broad slab/chamber floors. Maze is genuinely corridor-rich: its ratio and floor +area are both about **0.29**, and **0.991** of its projected walkable surface is in the largest +component. Conversely, TunnelNetwork, Underwater, and VerticalShafts have only **0.114–0.208** floor +area and **0.145–0.464** largest-surface share; FloatingIslands is intermediate (**0.400** area, +**0.678** largest share). Those are real fragmentation/ledge findings, not just a strict-volume +artifact. All selected cards pass the separate mouth-to-mouth route check, so “walkable area” still +does not mean every walkable patch is on the traversable route. + +The web sanity check did not produce a defensible universal “good” floor-to-volume threshold. The +official NPS descriptions instead support a heterogeneous target: solution caves grow along connected +fractures and streams, passages branch and join, and caves can contain multiple levels, narrow +walking passages, rooms, and vertical pits. NPS's Lehman Cave dimensions give a useful scale example +(the Talus Room is reported as 90 ft wide × 376 ft long, with 21,511 sq ft of floor area and 113 ft +floor-to-ceiling), not a universal ratio. The official Deep Rock Galactic cave-design note likewise +describes rooms connected by narrow winding tunnels, while the Team Cherry map-design interview +emphasizes interconnected spaces and backtracking. References: NPS Solution Caves +(https://www.nps.gov/subjects/caves/solution-caves.htm), NPS Mammoth Cave morphology +(https://home.nps.gov/maca/learn/nature/how-mammoth-cave-formed.htm), NPS Lehman Cave dimensions +(https://www.nps.gov/grba/learn/nature/lehman-caves-dimensions.htm), Deep Rock Galactic procedural +cave design +(https://store.steampowered.com/news/posts/?appgroupname=Deep+Rock+Galactic&appids=548430&enddate=1729500950&feed=steam_community_announcements), +and Team Cherry map design (https://www.pcgamer.com/how-to-design-a-great-metroidvania-map/). + ### 3.5 Validation in three layers, and how "good" is ever judged 1. **Construct by design** so the law cannot break — build the connections FIRST and generate around diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeComposerShowcaseTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeComposerShowcaseTest.cpp new file mode 100644 index 0000000..cb7eae9 --- /dev/null +++ b/Source/VoxelForge/Private/Tests/VoxelForgeComposerShowcaseTest.cpp @@ -0,0 +1,1043 @@ +// Morning review: one hard-gated, walkable candidate per archetype plus an offline page. + +#if WITH_DEV_AUTOMATION_TESTS + +#include "Misc/AutomationTest.h" +#include "HAL/PlatformTime.h" +#include "Misc/Paths.h" + +#include "VoxelForgeTestFixture.h" +#include "VoxelDensityOpStack.h" +#include "VoxelSettings.h" +#include "VoxelStrateComposer.h" +#include "VoxelStrateMeasure.h" +#include "VoxelStratePreview.h" + +namespace +{ + // DA_Settings currently leaves UVoxelSettings::Seed at its declared default (0). The + // composer seed is independent, but keeping the fixture's live generator seed equal to the + // project seed makes the standalone stack oracle reproduce the PIE hand-off exactly. + constexpr int32 BaseComposerSeed = 0; + constexpr int32 DefaultTargetStrateIndex = VoxelForgeTest::FTestWorld::SlotFlatPlain; + constexpr int32 CandidateIndicesPerSeed = 64; + constexpr float LargestComponentSurvivalThreshold = 0.50f; + constexpr float ClearanceNormalizationVoxels = 64.0f; + constexpr int32 FinePreviewSampleStep = 1; + constexpr int32 FinePreviewRadiusVoxels = 64; + constexpr int32 FinePreviewMaxCells = 2000000; + + const ECaveGeneratorType GShowcaseArchetypes[] = { + ECaveGeneratorType::CrystalChamber, + ECaveGeneratorType::FlatPlain, + ECaveGeneratorType::FloatingIslands, + ECaveGeneratorType::Maze, + ECaveGeneratorType::SurfaceWorld, + ECaveGeneratorType::TunnelNetwork, + ECaveGeneratorType::Underwater, + ECaveGeneratorType::VerticalShafts, + }; + + int32 VF_ShowcaseArchetypeIndex(ECaveGeneratorType Archetype) + { + for (int32 Index = 0; Index < UE_ARRAY_COUNT(GShowcaseArchetypes); ++Index) + { + if (GShowcaseArchetypes[Index] == Archetype) + { + return Index; + } + } + return INDEX_NONE; + } + + const TCHAR* VF_ShowcaseConnectivityName(EVoxelConnectivityResult Result) + { + switch (Result) + { + case EVoxelConnectivityResult::Connected: return TEXT("Connected"); + case EVoxelConnectivityResult::NotConnectedAtThisResolution: return TEXT("NotConnectedAtThisResolution"); + case EVoxelConnectivityResult::StartCellSolid: return TEXT("StartCellSolid"); + case EVoxelConnectivityResult::GoalCellSolid: return TEXT("GoalCellSolid"); + case EVoxelConnectivityResult::OutOfWindow: return TEXT("OutOfWindow"); + case EVoxelConnectivityResult::CoarseLiedBudgetExhausted: return TEXT("CoarseLiedBudgetExhausted"); + } + return TEXT("Unknown"); + } + + float VF_ShowcaseBoundarySeal(const FVoxelStrateArchetypeParams& Params, + ECaveGeneratorType Archetype) + { + switch (Archetype) + { + case ECaveGeneratorType::TunnelNetwork: + case ECaveGeneratorType::Underwater: + return Params.TunnelNetworkParams.BoundarySealThickness; + case ECaveGeneratorType::FlatPlain: + case ECaveGeneratorType::CrystalChamber: + return Params.SlabParams.BoundarySealThickness; + case ECaveGeneratorType::Maze: + return Params.MazeParams.BoundarySealThickness; + case ECaveGeneratorType::SurfaceWorld: + return Params.SurfaceParams.BoundarySealThickness; + case ECaveGeneratorType::VerticalShafts: + return Params.VerticalShaftParams.BoundarySealThickness; + case ECaveGeneratorType::FloatingIslands: + return Params.FloatingIslandParams.BoundarySealThickness; + default: + return 0.0f; + } + } + + class FShowcaseStackSampler final : public IVoxelStrateDensitySampler + { + public: + explicit FShowcaseStackSampler(const FVoxelOpStack& InStack) : Stack(InStack) {} + + float SampleDensity(float WorldX, float WorldY, float WorldZ) const override + { + return Stack.EvalMC(WorldX, WorldY, WorldZ); + } + + private: + const FVoxelOpStack& Stack; + }; + + struct FShowcaseSample + { + FVoxelStrateComposerCandidate Candidate; + FVoxelStrateMetrics Metrics; + double SelectionScore = -1.0; + FString LawText; + int32 TargetStrateIndex = INDEX_NONE; + int32 TopVoxelZ = 0; + int32 BottomVoxelZ = 0; + }; + + struct FShowcaseStats + { + int32 EvaluatedCandidates = 0; + TArray Survivors; + TOptional Best; + FString LastFailure; + }; + + void VF_AddMetricSample(FShowcaseStats& Stats, const FShowcaseSample& Sample) + { + Stats.Survivors.Add(Sample); + if (!Stats.Best.IsSet() + || Sample.SelectionScore > Stats.Best->SelectionScore + || (Sample.SelectionScore == Stats.Best->SelectionScore + && (Sample.Candidate.Seed < Stats.Best->Candidate.Seed + || (Sample.Candidate.Seed == Stats.Best->Candidate.Seed + && Sample.Candidate.Index < Stats.Best->Candidate.Index)))) + { + Stats.Best = Sample; + } + } + + bool VF_BuildShowcaseStack( + const VoxelForgeTest::FTestWorld& World, + const FVoxelStrateComposerCandidate& Candidate, + int32 TopVoxelZ, + int32 BottomVoxelZ, + FVoxelOpStack& OutStack, + FVoxelOpContext& OutContext, + FString& OutError) + { + if (!Candidate.bValid || !Candidate.Regions.IsSingleRegion()) + { + OutError = TEXT("showcase candidates must be valid single-region candidates"); + return false; + } + + FVoxelStrateArchetypeParams Params = Candidate.ArchetypeParams; + VF_SetStrateArchetypeRuntimeBounds( + Params, static_cast(TopVoxelZ) + 1.0f, static_cast(BottomVoxelZ)); + const bool bBuilt = VF_BuildNativeStrateStackForCandidate( + Candidate.Archetype, Params, World.Generator->Seed, + World.Generator->OriginSpineRadius, World.Generator->WorldRadiusVoxels, + VF_ShowcaseBoundarySeal(Candidate.ArchetypeParams, Candidate.Archetype), + World.StrateManager.Get(), OutStack, OutContext); + if (!bBuilt) + { + OutError = TEXT("native candidate stack materialisation failed"); + return false; + } + if (OutContext.WorldRadiusVoxels != 0.0f) + { + OutError = TEXT("native showcase stack changed WorldRadiusVoxels from zero"); + return false; + } + return true; + } + + double VF_ShowcaseSelectionScore(const FVoxelStrateMetrics& Metrics) + { + const double Area = FMath::Max(0.0, static_cast(Metrics.WalkableFloorAreaFraction)); + const double Clearance = FMath::Clamp( + static_cast(Metrics.MedianVerticalClearance) + / static_cast(ClearanceNormalizationVoxels), + 0.0, 1.0); + const double Surface = FMath::Max( + 0.0, static_cast(Metrics.LargestWalkableSurfaceShare)); + // Floor area is the primary signal; clearance and projected continuity break ties in a + // deterministic way without making a tall but tiny ledge beat a broad floor. + return Area + 0.25 * Clearance + 0.10 * Surface; + } + + bool VF_GetShowcaseMouthPair( + const VoxelForgeTest::FTestWorld& World, + int32 InTargetStrateIndex, + FVector& OutArrivalPoint, + FVector& OutDeparturePoint) + { + int32 ArrivalCount = 0; + int32 DepartureCount = 0; + for (const FVoxelPassage& Passage : World.StrateManager->GetPassages()) + { + if (Passage.LowerStrateIndex == InTargetStrateIndex + && Passage.UpperStrateIndex + 1 == InTargetStrateIndex) + { + OutArrivalPoint = Passage.LowerPoint; + ++ArrivalCount; + } + if (Passage.UpperStrateIndex == InTargetStrateIndex + && Passage.LowerStrateIndex == InTargetStrateIndex + 1) + { + OutDeparturePoint = Passage.UpperPoint; + ++DepartureCount; + } + } + return ArrivalCount == 1 && DepartureCount == 1; + } + + bool VF_EvaluateShowcaseCandidate( + const VoxelForgeTest::FTestWorld& World, + const FVoxelStrateComposerCandidate& Candidate, + int32 InTargetStrateIndex, + const FVoxelStrateMeasureSettings& MeasureSettings, + FShowcaseSample& OutSample, + FString* OutFailureReason = nullptr) + { + auto Fail = [OutFailureReason](const FString& Reason) -> bool + { + if (OutFailureReason != nullptr) + { + *OutFailureReason = Reason; + } + return false; + }; + if (!Candidate.bValid || !Candidate.Regions.IsSingleRegion()) + { + return Fail(TEXT("invalid or multi-region candidate")); + } + + int32 TopVoxelZ = 0; + int32 BottomVoxelZ = 0; + if (!World.GetSlotVoxelZRange(InTargetStrateIndex, TopVoxelZ, BottomVoxelZ)) + { + return Fail(TEXT("target slot has no Z range")); + } + FVector ArrivalPoint = FVector::ZeroVector; + FVector DeparturePoint = FVector::ZeroVector; + if (!VF_GetShowcaseMouthPair(World, InTargetStrateIndex, + ArrivalPoint, DeparturePoint)) + { + return Fail(TEXT("target slot does not have exactly one arrival/departure mouth")); + } + + FVoxelOpStack Stack; + FVoxelOpContext Context; + FString BuildError; + if (!VF_BuildShowcaseStack(World, Candidate, TopVoxelZ, BottomVoxelZ, + Stack, Context, BuildError)) + { + return Fail(BuildError); + } + Stack.PrepareChunk(Context); + FShowcaseStackSampler Sampler(Stack); + const FVoxelStrateMetrics Metrics = VF_MeasureStrateWithSampler( + Sampler, BottomVoxelZ, TopVoxelZ + 1, Context.EdgeSealThickness, + MeasureSettings, nullptr); + if (!Metrics.bValid) + { + return Fail(FString::Printf(TEXT("measurement refused: %s"), *Metrics.RefusalReason)); + } + const FVoxelConnectivityDiagnostics Law = VF_DiagnoseConnectivityWithSampler( + Sampler, BottomVoxelZ, TopVoxelZ + 1, Context.EdgeSealThickness, + ArrivalPoint, DeparturePoint, MeasureSettings); + const bool bNonVacuous = Metrics.NumSampled > 0 + && Metrics.NumAir > 0 && Metrics.NumSolid > 0; + const bool bLargestEnough = bNonVacuous + && Metrics.LargestComponentShare >= LargestComponentSurvivalThreshold; + const bool bLawPass = Law.bValid + && Law.Result == EVoxelConnectivityResult::Connected + && !Law.bStartSnapped && !Law.bGoalSnapped; + if (!(bNonVacuous && bLargestEnough && bLawPass)) + { + return Fail(FString::Printf( + TEXT("nonv=%d largest=%d law=%s snapped=%d/%d air=%.6f largest_share=%.6f " + "walk_area=%.6f clearance=%d"), + bNonVacuous ? 1 : 0, + bLargestEnough ? 1 : 0, + VF_ShowcaseConnectivityName(Law.Result), + Law.bStartSnapped ? 1 : 0, + Law.bGoalSnapped ? 1 : 0, + Metrics.AirFraction, + Metrics.LargestComponentShare, + Metrics.WalkableFloorAreaFraction, + Metrics.MedianVerticalClearance)); + } + + OutSample = FShowcaseSample(); + OutSample.Candidate = Candidate; + OutSample.Metrics = Metrics; + OutSample.SelectionScore = VF_ShowcaseSelectionScore(Metrics); + OutSample.LawText = FString::Printf( + TEXT("%s%s"), VF_ShowcaseConnectivityName(Law.Result), + (Law.bStartSnapped || Law.bGoalSnapped) ? TEXT(" (snapped)") : TEXT("")); + OutSample.TargetStrateIndex = InTargetStrateIndex; + OutSample.TopVoxelZ = TopVoxelZ; + OutSample.BottomVoxelZ = BottomVoxelZ; + return true; + } + + FString VF_FormatNativeParameterRecipe(const FVoxelStrateComposerCandidate& Candidate) + { + FString Parents; + for (int32 ParentIndex = 0; + ParentIndex < Candidate.ParameterRoll.ParentEntryIndices.Num(); ++ParentIndex) + { + if (ParentIndex > 0) + { + Parents += TEXT(","); + } + const float Weight = Candidate.ParameterRoll.ParentWeights.IsValidIndex(ParentIndex) + ? Candidate.ParameterRoll.ParentWeights[ParentIndex] : 0.0f; + Parents += FString::Printf( + TEXT("#%d@%.6f"), + Candidate.ParameterRoll.ParentEntryIndices[ParentIndex], Weight); + } + return FString::Printf( + TEXT("parameter-roll|native=%s|parents=[%s]|dominant_parent=%d|single_region=true"), + VF_GetStrateArchetypeName(Candidate.Archetype), *Parents, + Candidate.ParameterRoll.DominantParentPosition); + } + + float VF_MedianFloat(TArray Values) + { + if (Values.IsEmpty()) + { + return 0.0f; + } + Values.Sort(); + const int32 Middle = Values.Num() / 2; + return (Values.Num() & 1) != 0 + ? Values[Middle] + : 0.5f * (Values[Middle - 1] + Values[Middle]); + } + + int32 VF_LowerMedianInt(TArray Values) + { + if (Values.IsEmpty()) + { + return 0; + } + Values.Sort(); + return Values[(Values.Num() - 1) / 2]; + } + + FString VF_FormatFloatDistribution(const TArray& Samples, + TFunctionRef Getter) + { + if (Samples.IsEmpty()) + { + return TEXT("none (0 hard-gate survivors)"); + } + TArray Values; + Values.Reserve(Samples.Num()); + for (const FShowcaseSample& Sample : Samples) + { + Values.Add(Getter(Sample.Metrics)); + } + TArray Sorted = Values; + Sorted.Sort(); + return FString::Printf( + TEXT("%.6f; %.6f–%.6f"), VF_MedianFloat(MoveTemp(Values)), + Sorted[0], Sorted.Last()); + } + + FString VF_FormatIntDistribution(const TArray& Samples, + TFunctionRef Getter) + { + if (Samples.IsEmpty()) + { + return TEXT("none (0 hard-gate survivors)"); + } + TArray Values; + Values.Reserve(Samples.Num()); + for (const FShowcaseSample& Sample : Samples) + { + Values.Add(Getter(Sample.Metrics)); + } + TArray Sorted = Values; + Sorted.Sort(); + return FString::Printf( + TEXT("%d; %d–%d voxels"), VF_LowerMedianInt(MoveTemp(Values)), + Sorted[0], Sorted.Last()); + } + + FVoxelStratePreviewArchetypeSummary VF_MakeSummary( + const FShowcaseStats& Stats, + const FVoxelStratePreviewWindow& SelectedWindow, + const FString& WindowSummary, + ECaveGeneratorType Archetype) + { + FVoxelStratePreviewArchetypeSummary Summary; + Summary.ArchetypeName = VF_GetStrateArchetypeName(Archetype); + Summary.EvaluatedCandidates = Stats.EvaluatedCandidates; + Summary.HardGateSurvivors = Stats.Survivors.Num(); + Summary.Window = SelectedWindow; + Summary.WindowSummary = WindowSummary; + Summary.WalkableFractionSummary = VF_FormatFloatDistribution( + Stats.Survivors, + [](const FVoxelStrateMetrics& Metrics) { return Metrics.WalkableFraction; }); + Summary.WalkableFloorAreaSummary = VF_FormatFloatDistribution( + Stats.Survivors, + [](const FVoxelStrateMetrics& Metrics) { return Metrics.WalkableFloorAreaFraction; }); + Summary.MedianVerticalClearanceSummary = VF_FormatIntDistribution( + Stats.Survivors, + [](const FVoxelStrateMetrics& Metrics) { return Metrics.MedianVerticalClearance; }); + Summary.LargestWalkableSurfaceSummary = VF_FormatFloatDistribution( + Stats.Survivors, + [](const FVoxelStrateMetrics& Metrics) { return Metrics.LargestWalkableSurfaceShare; }); + return Summary; + } + + FString VF_FormatShowcaseSummaryWindow(const FShowcaseStats& Stats) + { + TArray SurvivorWindows; + for (const FShowcaseSample& Sample : Stats.Survivors) + { + const FVoxelStrateMetrics& Metrics = Sample.Metrics; + SurvivorWindows.AddUnique(FString::Printf( + TEXT("target=%d; margin=%d; Z=[%d,%d); XY=[%.0f,%.0f) x [%.0f,%.0f); grid=%dx%dx%d"), + Sample.TargetStrateIndex, + Metrics.ResolvedMarginVoxels, + Metrics.SampledMinZ, + Metrics.SampledMaxZ, + Metrics.SampledMinX, + Metrics.SampledMaxX, + Metrics.SampledMinY, + Metrics.SampledMaxY, + Metrics.SampledNumX, + Metrics.SampledNumY, + Metrics.SampledNumZ)); + } + SurvivorWindows.Sort(); + return FString::Printf( + TEXT("settings: step=4; HeadroomCells=2; MaxCells=8000000; exact survivor windows: %s"), + SurvivorWindows.IsEmpty() ? TEXT("none") : *FString::Join(SurvivorWindows, TEXT(" | "))); + } + + bool VF_ShowcaseMetricsEqual(const FVoxelStrateMetrics& A, + const FVoxelStrateMetrics& B) + { + return A.bValid == B.bValid + && A.RefusalReason == B.RefusalReason + && A.NumSampled == B.NumSampled + && A.NumAir == B.NumAir + && A.NumSolid == B.NumSolid + && A.AirFraction == B.AirFraction + && A.NumAirComponents == B.NumAirComponents + && A.LargestComponentShare == B.LargestComponentShare + && A.LargestComponentPoint == B.LargestComponentPoint + && A.LargestComponentCells == B.LargestComponentCells + && A.NumComponentsAtLeast1Pct == B.NumComponentsAtLeast1Pct + && A.AirComponentCells == B.AirComponentCells + && A.WalkableFraction == B.WalkableFraction + && A.WalkableFloorColumns == B.WalkableFloorColumns + && A.WalkableFloorAreaFraction == B.WalkableFloorAreaFraction + && A.NumWalkableSurfaceComponents == B.NumWalkableSurfaceComponents + && A.LargestWalkableSurfaceColumns == B.LargestWalkableSurfaceColumns + && A.LargestWalkableSurfaceShare == B.LargestWalkableSurfaceShare + && A.MedianFeatureScale == B.MedianFeatureScale + && A.MedianVerticalClearance == B.MedianVerticalClearance + && A.ResolvedMarginVoxels == B.ResolvedMarginVoxels + && A.SampledMinZ == B.SampledMinZ + && A.SampledMaxZ == B.SampledMaxZ + && A.SampledNumX == B.SampledNumX + && A.SampledNumY == B.SampledNumY + && A.SampledNumZ == B.SampledNumZ + && A.SampledMinX == B.SampledMinX + && A.SampledMaxX == B.SampledMaxX + && A.SampledMinY == B.SampledMinY + && A.SampledMaxY == B.SampledMaxY; + } + + bool VF_ShowcaseCandidatesEqual( + const FVoxelStrateComposerCandidate& A, + const FVoxelStrateComposerCandidate& B) + { + if (A.bValid != B.bValid + || A.FailureReason != B.FailureReason + || A.Seed != B.Seed + || A.Index != B.Index + || A.bStructureRoll != B.bStructureRoll + || A.Archetype != B.Archetype + || !VF_AreStrateParamsBitIdentical(A.ParameterRoll.Params, B.ParameterRoll.Params) + || A.ParameterRoll.ParentEntryIndices != B.ParameterRoll.ParentEntryIndices + || A.ParameterRoll.ParentWeights != B.ParameterRoll.ParentWeights + || A.ParameterRoll.DominantParentPosition != B.ParameterRoll.DominantParentPosition + || A.ParameterRoll.BootstrapJitterFieldCount != B.ParameterRoll.BootstrapJitterFieldCount + || A.ParameterRoll.BootstrapJitterFieldNames + != B.ParameterRoll.BootstrapJitterFieldNames) + { + return false; + } + if (A.bValid + && !VF_AreStrateArchetypeParamsBitIdentical( + A.ArchetypeParams, B.ArchetypeParams, A.Archetype)) + { + return false; + } + if (A.ParameterRoll.bValid != B.ParameterRoll.bValid + || A.ParameterRoll.FailureReason != B.ParameterRoll.FailureReason + || A.ParameterRoll.Archetype != B.ParameterRoll.Archetype + || (A.ParameterRoll.bValid + && !VF_AreStrateArchetypeParamsBitIdentical( + A.ParameterRoll.ArchetypeParams, + B.ParameterRoll.ArchetypeParams, + A.ParameterRoll.Archetype))) + { + return false; + } + + const FVoxelStrateRegionManifest& AR = A.Regions; + const FVoxelStrateRegionManifest& BR = B.Regions; + if (AR.bValid != BR.bValid + || AR.FailureReason != BR.FailureReason + || AR.Seed != BR.Seed + || AR.StrateIndex != BR.StrateIndex + || AR.RegionCount != BR.RegionCount + || AR.PartitionSeed != BR.PartitionSeed + || AR.LatticeCellSize != BR.LatticeCellSize + || AR.BlendWidth != BR.BlendWidth + || AR.bHasGlobalStructuralParams != BR.bHasGlobalStructuralParams + || AR.StructuralParamBlock != BR.StructuralParamBlock + || AR.StrateTopWorldZ != BR.StrateTopWorldZ + || AR.StrateBottomWorldZ != BR.StrateBottomWorldZ + || AR.BoundarySealThickness != BR.BoundarySealThickness + || AR.BaseDensity != BR.BaseDensity + || AR.Regions.Num() != BR.Regions.Num()) + { + return false; + } + for (int32 RegionIndex = 0; RegionIndex < AR.Regions.Num(); ++RegionIndex) + { + const FVoxelStrateRegion& RegionA = AR.Regions[RegionIndex]; + const FVoxelStrateRegion& RegionB = BR.Regions[RegionIndex]; + if (RegionA.RegionIndex != RegionB.RegionIndex + || RegionA.Seed != RegionB.Seed + || RegionA.Archetype != RegionB.Archetype + || RegionA.bUsesRecipe != RegionB.bUsesRecipe + || !VF_AreStrateArchetypeParamsBitIdentical( + RegionA.ArchetypeParams, RegionB.ArchetypeParams, RegionA.Archetype) + || !VF_AreStrateStructureRecipesIdentical(RegionA.Recipe, RegionB.Recipe)) + { + return false; + } + } + return true; + } +} + +IMPLEMENT_SIMPLE_AUTOMATION_TEST( + FVoxelForgeComposerShowcaseTest, + "VoxelForge.Composer.Showcase", + EAutomationTestFlags_ApplicationContextMask | EAutomationTestFlags::EngineFilter) + +bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters) +{ + using namespace VoxelForgeTest; + (void)Parameters; + const double TestStartSeconds = FPlatformTime::Seconds(); + + TestTrue(TEXT("lateral regions remain gated off for the showcase"), + !VF_LateralRegionsAreShippable()); + + UVoxelSettings* AuthoredSettings = LoadObject( + nullptr, TEXT("/Game/VoxelForge/DA_Settings.DA_Settings")); + if (AuthoredSettings == nullptr) + { + AddError(TEXT("Could not load /Game/VoxelForge/DA_Settings.DA_Settings.")); + return false; + } + + FVoxelStrateCorpus Corpus; + FString CorpusReport; + const bool bCorpusLoaded = Corpus.LoadFromAssetRegistry(CorpusReport); + AddInfo(CorpusReport); + TestTrue(TEXT("showcase corpus is usable"), bCorpusLoaded && Corpus.IsValid()); + if (!bCorpusLoaded || !Corpus.IsValid()) + { + return false; + } + + FTestWorld World; + World.Build(BaseComposerSeed, 2, true); + if (!World.IsValid()) + { + AddError(World.WhyInvalid()); + return false; + } + TestTrue(TEXT("showcase fixture keeps Settings WorldRadiusVoxels at zero"), + World.Settings->WorldRadiusVoxels == 0.0f); + TestTrue(TEXT("showcase fixture keeps Generator WorldRadiusVoxels at zero"), + World.Generator->WorldRadiusVoxels == 0.0f); + + int32 TopVoxelZ = 0; + int32 BottomVoxelZ = 0; + if (!World.GetSlotVoxelZRange(DefaultTargetStrateIndex, TopVoxelZ, BottomVoxelZ)) + { + AddError(TEXT("showcase target slot has no Z range")); + return false; + } + + FVector ArrivalPoint = FVector::ZeroVector; + FVector DeparturePoint = FVector::ZeroVector; + int32 ArrivalCount = 0; + int32 DepartureCount = 0; + for (const FVoxelPassage& Passage : World.StrateManager->GetPassages()) + { + if (Passage.LowerStrateIndex == DefaultTargetStrateIndex + && Passage.UpperStrateIndex + 1 == DefaultTargetStrateIndex) + { + ArrivalPoint = Passage.LowerPoint; + ++ArrivalCount; + } + if (Passage.UpperStrateIndex == DefaultTargetStrateIndex + && Passage.LowerStrateIndex == DefaultTargetStrateIndex + 1) + { + DeparturePoint = Passage.UpperPoint; + ++DepartureCount; + } + } + TestEqual(TEXT("showcase target has one arrival mouth"), ArrivalCount, 1); + TestEqual(TEXT("showcase target has one departure mouth"), DepartureCount, 1); + + FVoxelStrateMeasureSettings MeasureSettings; + MeasureSettings.SampleStep = 4; + MeasureSettings.RadiusInVoxels = 256; + MeasureSettings.CenterXY = FVector2D::ZeroVector; + MeasureSettings.MaxCells = 8000000; + MeasureSettings.MaxRouteRetries = 16; + MeasureSettings.HeadroomCells = 2; + MeasureSettings.InteriorMarginVoxels = -1; + + FVoxelStrateFinePreviewSettings FinePreviewSettings; + FinePreviewSettings.SampleStep = FinePreviewSampleStep; + FinePreviewSettings.RadiusInVoxels = FinePreviewRadiusVoxels; + FinePreviewSettings.MaxCells = FinePreviewMaxCells; + TestTrue(TEXT("showcase fine ROI settings are valid and bounded"), + FinePreviewSettings.IsValid()); + + FShowcaseStats Stats[UE_ARRAY_COUNT(GShowcaseArchetypes)]; + bool bHaveAllArchetypes = false; + int32 NumRollAttempts = 0; + int32 NumSingleRegionRolls = 0; + int32 NumLateralRollsSkipped = 0; + const int32 SearchSeeds[] = { BaseComposerSeed, 7331 }; + + // Exhaust the bounded roll set before selecting. This makes "best" mean best among the + // stated seed/index search, rather than the first candidate that happens to fill a family. + for (const int32 SearchSeed : SearchSeeds) + { + for (int32 CandidateIndex = 0; + CandidateIndex < CandidateIndicesPerSeed; + ++CandidateIndex) + { + ++NumRollAttempts; + const FVoxelStrateComposerCandidate Candidate = VF_RollStrateCandidate( + Corpus, SearchSeed, CandidateIndex, false); + const FVoxelStrateComposerCandidate RepeatCandidate = VF_RollStrateCandidate( + Corpus, SearchSeed, CandidateIndex, false); + TestTrue(FString::Printf(TEXT("seed %d candidate %d rerolls bit-identically"), + SearchSeed, CandidateIndex), + VF_ShowcaseCandidatesEqual(Candidate, RepeatCandidate)); + if (!Candidate.bValid) + { + continue; + } + TestTrue(FString::Printf(TEXT("seed %d candidate %d has a valid native roll"), + SearchSeed, CandidateIndex), + Candidate.ParameterRoll.bValid + && Candidate.Archetype == Candidate.ParameterRoll.Archetype); + + // A multi-region manifest is intentionally not considered for the morning page. It + // would be a use of the currently gated seam feature, even though it can be measured. + if (!Candidate.Regions.IsSingleRegion()) + { + ++NumLateralRollsSkipped; + continue; + } + ++NumSingleRegionRolls; + const int32 ArchetypeIndex = VF_ShowcaseArchetypeIndex(Candidate.Archetype); + if (ArchetypeIndex == INDEX_NONE) + { + continue; + } + ++Stats[ArchetypeIndex].EvaluatedCandidates; + FShowcaseSample Sample; + FString FailureReason; + if (VF_EvaluateShowcaseCandidate( + World, Candidate, DefaultTargetStrateIndex, MeasureSettings, Sample, + &FailureReason)) + { + VF_AddMetricSample(Stats[ArchetypeIndex], Sample); + } + else + { + Stats[ArchetypeIndex].LastFailure = MoveTemp(FailureReason); + } + + } + } + + bHaveAllArchetypes = true; + for (const FShowcaseStats& ArchetypeStats : Stats) + { + bHaveAllArchetypes = bHaveAllArchetypes && ArchetypeStats.Best.IsSet(); + } + + // Some native families only fit the generated landing mouths when installed into a different + // existing slot. That is still a density-only PIE override: layout, placement, and passages + // are unchanged. Search the six interior slots as a deterministic fallback for any archetype + // absent from the first target-slot screen. Topmost/bottommost slots are excluded because one + // of the two primordial mouths does not exist there. + if (!bHaveAllArchetypes) + { + // Every missing family is measured at the exact reported step-4 settings for every + // interior target slot. There is no coarse pre-gate here: a coarse route can be a false + // negative, so it must not silently remove a candidate from the "best" set. + for (const int32 SearchSeed : SearchSeeds) + { + for (int32 CandidateIndex = 0; + CandidateIndex < CandidateIndicesPerSeed; + ++CandidateIndex) + { + ++NumRollAttempts; + const FVoxelStrateComposerCandidate Candidate = VF_RollStrateCandidate( + Corpus, SearchSeed, CandidateIndex, false); + if (!Candidate.bValid) + { + continue; + } + if (!Candidate.Regions.IsSingleRegion()) + { + ++NumLateralRollsSkipped; + continue; + } + ++NumSingleRegionRolls; + const int32 ArchetypeIndex = VF_ShowcaseArchetypeIndex(Candidate.Archetype); + if (ArchetypeIndex == INDEX_NONE || Stats[ArchetypeIndex].Best.IsSet()) + { + continue; + } + for (int32 FallbackTarget = FTestWorld::SlotFlatPlain; + FallbackTarget <= FTestWorld::SlotFloatingIsland; + ++FallbackTarget) + { + FString FailureReason; + FShowcaseSample Sample; + if (VF_EvaluateShowcaseCandidate( + World, Candidate, FallbackTarget, MeasureSettings, Sample, + &FailureReason)) + { + VF_AddMetricSample(Stats[ArchetypeIndex], Sample); + } + else + { + Stats[ArchetypeIndex].LastFailure = MoveTemp(FailureReason); + } + } + + } + } + } + + bHaveAllArchetypes = true; + for (const FShowcaseStats& ArchetypeStats : Stats) + { + bHaveAllArchetypes = bHaveAllArchetypes && ArchetypeStats.Best.IsSet(); + } + + AddInfo(FString::Printf( + TEXT("Showcase search: %d roll evaluations (fallback replays included), " + "%d eligible single-region evaluations (fallback replays included), " + "%d multi-region evaluations skipped while the lateral gate is OFF."), + NumRollAttempts, NumSingleRegionRolls, NumLateralRollsSkipped)); + for (int32 ArchetypeIndex = 0; + ArchetypeIndex < UE_ARRAY_COUNT(GShowcaseArchetypes); + ++ArchetypeIndex) + { + AddInfo(FString::Printf( + TEXT("Showcase search detail %s: eligible=%d hard_gate_survivors=%d last_failure=%s"), + VF_GetStrateArchetypeName(GShowcaseArchetypes[ArchetypeIndex]), + Stats[ArchetypeIndex].EvaluatedCandidates, + Stats[ArchetypeIndex].Survivors.Num(), + Stats[ArchetypeIndex].LastFailure.IsEmpty() + ? TEXT("none") : *Stats[ArchetypeIndex].LastFailure)); + } + for (int32 ArchetypeIndex = 0; + ArchetypeIndex < UE_ARRAY_COUNT(GShowcaseArchetypes); + ++ArchetypeIndex) + { + const FShowcaseStats& ArchetypeStats = Stats[ArchetypeIndex]; + TestTrue(FString::Printf(TEXT("%s has at least one hard-gate survivor"), + VF_GetStrateArchetypeName(GShowcaseArchetypes[ArchetypeIndex])), + ArchetypeStats.Best.IsSet()); + } + if (!bHaveAllArchetypes) + { + AddError(TEXT("The bounded deterministic search did not find one hard-gate survivor for every archetype.")); + return false; + } + + const FString CoarseWindowSummary = FString::Printf( + TEXT("coarse settings: step=%d; XY=[-256,256) x [-256,256) voxels; HeadroomCells=%d; " + "MaxCells=%d; interior Z margin derived per candidate; exact survivor windows " + "are listed in this row and selected windows are repeated on cards"), + MeasureSettings.SampleStep, MeasureSettings.HeadroomCells, MeasureSettings.MaxCells); + TArray Summaries; + Summaries.Reserve(UE_ARRAY_COUNT(GShowcaseArchetypes)); + for (int32 ArchetypeIndex = 0; + ArchetypeIndex < UE_ARRAY_COUNT(GShowcaseArchetypes); + ++ArchetypeIndex) + { + Summaries.Add(VF_MakeSummary( + Stats[ArchetypeIndex], + FVoxelStratePreviewWindow(), + CoarseWindowSummary, + GShowcaseArchetypes[ArchetypeIndex])); + } + + const FString ShowcaseDirectory = FPaths::ProjectSavedDir() + / TEXT("VoxelForge/Showcase"); + TArray PreviewCandidates; + PreviewCandidates.Reserve(UE_ARRAY_COUNT(GShowcaseArchetypes)); + FVoxelStratePreviewWindow CommonWindow; + bool bCommonWindowSet = false; + int32 FineRendered = 0; + int32 FineRefused = 0; + int32 FineBlank = 0; + + for (int32 ArchetypeIndex = 0; + ArchetypeIndex < UE_ARRAY_COUNT(GShowcaseArchetypes); + ++ArchetypeIndex) + { + const FShowcaseSample& Selected = Stats[ArchetypeIndex].Best.GetValue(); + const FVoxelStrateComposerCandidate& Candidate = Selected.Candidate; + + // The output filename namespace is local to this page. Keep it distinct even if a future + // search selects equal candidate indices from two different composer seeds; the PIE fields + // below still retain the exact real candidate index. + const int32 RenderIndex = 1000 + ArchetypeIndex; + FVoxelOpStack Stack; + FVoxelOpContext Context; + FString BuildError; + TestTrue(FString::Printf(TEXT("selected %s rebuilds for preview"), + VF_GetStrateArchetypeName(Candidate.Archetype)), + VF_BuildShowcaseStack( + World, Candidate, Selected.TopVoxelZ, Selected.BottomVoxelZ, + Stack, Context, BuildError)); + if (!BuildError.IsEmpty()) + { + AddError(FString::Printf(TEXT("selected %s rebuild error: %s"), + VF_GetStrateArchetypeName(Candidate.Archetype), *BuildError)); + return false; + } + Stack.PrepareChunk(Context); + FShowcaseStackSampler Sampler(Stack); + + const FVoxelStrateMetrics RepeatMetrics = VF_MeasureStrateWithSampler( + Sampler, Selected.BottomVoxelZ, Selected.TopVoxelZ + 1, Context.EdgeSealThickness, + MeasureSettings, nullptr); + TestTrue(FString::Printf(TEXT("selected %s measurement is bit-identical on rerun"), + VF_GetStrateArchetypeName(Candidate.Archetype)), + VF_ShowcaseMetricsEqual(Selected.Metrics, RepeatMetrics)); + + FVoxelStrateSampleGrid SampleGrid; + const FVoxelStrateMetrics Metrics = VF_MeasureStrateWithSampler( + Sampler, Selected.BottomVoxelZ, Selected.TopVoxelZ + 1, Context.EdgeSealThickness, + MeasureSettings, &SampleGrid); + int32 DensitySignMismatches = 0; + for (int32 CellIndex = 0; CellIndex < SampleGrid.CellCount; ++CellIndex) + { + const bool bAirByDensity = SampleGrid.Density[CellIndex] > 0.0f; + const bool bAirByBit = SampleGrid.Air[CellIndex] != 0u; + if (bAirByDensity != bAirByBit) + { + ++DensitySignMismatches; + } + } + TestEqual(FString::Printf(TEXT("selected %s preserves density sign polarity"), + VF_GetStrateArchetypeName(Candidate.Archetype)), + DensitySignMismatches, 0); + + FVoxelStratePreviewCandidate PreviewCandidate; + FString PreviewError; + const bool bCoarseWritten = VF_WriteStratePreviewCandidate( + ShowcaseDirectory, RenderIndex, + VF_FormatNativeParameterRecipe(Candidate), + SampleGrid, Metrics, MeasureSettings.HeadroomCells, + Selected.SelectionScore, Selected.LawText, false, TEXT(""), + PreviewCandidate, PreviewError); + TestTrue(FString::Printf(TEXT("selected %s coarse preview writes"), + VF_GetStrateArchetypeName(Candidate.Archetype)), + bCoarseWritten && PreviewCandidate.bRendered + && PreviewCandidate.bContourRendered); + if (!bCoarseWritten) + { + AddError(FString::Printf(TEXT("selected %s coarse preview failed: %s"), + VF_GetStrateArchetypeName(Candidate.Archetype), *PreviewError)); + return false; + } + + if (!bCommonWindowSet) + { + CommonWindow = PreviewCandidate.Window; + bCommonWindowSet = true; + } + + FVoxelStrateMeasureSettings FineMeasureSettings = + FinePreviewSettings.MakeMeasureSettings(MeasureSettings); + FineMeasureSettings.CenterXY = FVector2D( + Metrics.LargestComponentPoint.X, Metrics.LargestComponentPoint.Y); + FVoxelStrateSampleGrid FineGrid; + const FVoxelStrateMetrics FineMetrics = VF_MeasureStrateWithSampler( + Sampler, Selected.BottomVoxelZ, Selected.TopVoxelZ + 1, Context.EdgeSealThickness, + FineMeasureSettings, &FineGrid); + const FVector2D FineResolvedCenter( + 0.5f * (FineGrid.MinX + FineGrid.MaxX), + 0.5f * (FineGrid.MinY + FineGrid.MaxY)); + TestTrue(FString::Printf(TEXT("selected %s fine ROI is centred on LargestComponentPoint"), + VF_GetStrateArchetypeName(Candidate.Archetype)), + !FineMetrics.bValid + || (FMath::IsNearlyEqual( + FineResolvedCenter.X, Metrics.LargestComponentPoint.X, 0.5f) + && FMath::IsNearlyEqual( + FineResolvedCenter.Y, Metrics.LargestComponentPoint.Y, 0.5f))); + TestTrue(FString::Printf(TEXT("selected %s fine ROI stays within its cap"), + VF_GetStrateArchetypeName(Candidate.Archetype)), + !FineMetrics.bValid || FineGrid.CellCount <= FinePreviewMaxCells); + const bool bFineWritten = VF_WriteStratePreviewFineCandidate( + ShowcaseDirectory, RenderIndex, FineGrid, + FineMeasureSettings.HeadroomCells, + FineMetrics.bValid ? FString() : FineMetrics.RefusalReason, + PreviewCandidate, PreviewError); + TestTrue(FString::Printf(TEXT("selected %s fine preview records"), + VF_GetStrateArchetypeName(Candidate.Archetype)), + bFineWritten); + if (!bFineWritten) + { + AddError(FString::Printf(TEXT("selected %s fine preview failed: %s"), + VF_GetStrateArchetypeName(Candidate.Archetype), *PreviewError)); + return false; + } + if (FineMetrics.bValid) + { + TestTrue(FString::Printf(TEXT("selected %s fine preview has filled and contour views"), + VF_GetStrateArchetypeName(Candidate.Archetype)), + PreviewCandidate.bFineRendered && PreviewCandidate.bFineContourRendered); + } + if (FineMetrics.bValid && PreviewCandidate.bFineRendered) + { + ++FineRendered; + if (PreviewCandidate.bFineBlank) + { + ++FineBlank; + } + } + else if (!FineMetrics.bValid) + { + ++FineRefused; + } + + PreviewCandidate.CandidateIndex = Candidate.Index; + PreviewCandidate.ArchetypeName = VF_GetStrateArchetypeName(Candidate.Archetype); + PreviewCandidate.bShowcaseCard = true; + PreviewCandidate.ComposerSeed = Candidate.Seed; + PreviewCandidate.ComposerTargetStrateIndex = Selected.TargetStrateIndex; + PreviewCandidate.bComposerRollStructure = false; + PreviewCandidate.SelectionReason = FString::Printf( + TEXT("Hard gates passed: non-vacuous, largest air component >= %.2f, and exact " + "unsnapped arrival → departure connectivity. Selected by score " + "floor_area + 0.25*clamp(clearance/%d,0,1) + 0.10*largest_surface = %.6f; " + "floor area is the primary walking signal. Applied to existing target slot %d."), + LargestComponentSurvivalThreshold, + static_cast(ClearanceNormalizationVoxels), + Selected.SelectionScore, + Selected.TargetStrateIndex); + PreviewCandidates.Add(MoveTemp(PreviewCandidate)); + + TestTrue(FString::Printf(TEXT("selected %s retains a valid exact coarse window"), + VF_GetStrateArchetypeName(Candidate.Archetype)), + Metrics.bValid && SampleGrid.IsValid()); + } + + for (int32 ArchetypeIndex = 0; + ArchetypeIndex < UE_ARRAY_COUNT(GShowcaseArchetypes); + ++ArchetypeIndex) + { + const FShowcaseSample& Selected = Stats[ArchetypeIndex].Best.GetValue(); + Summaries[ArchetypeIndex].Window = PreviewCandidates[ArchetypeIndex].Window; + Summaries[ArchetypeIndex].WindowSummary = VF_FormatShowcaseSummaryWindow( + Stats[ArchetypeIndex]); + AddInfo(FString::Printf( + TEXT("%s: hard-gate survivors=%d, selected seed=%d index=%d, " + "walkable=%.6f, floor_area=%.6f, clearance=%d voxels, largest_surface=%.6f, " + "window=%s"), + VF_GetStrateArchetypeName(GShowcaseArchetypes[ArchetypeIndex]), + Stats[ArchetypeIndex].Survivors.Num(), + Selected.Candidate.Seed, Selected.Candidate.Index, + Selected.Metrics.WalkableFraction, + Selected.Metrics.WalkableFloorAreaFraction, + Selected.Metrics.MedianVerticalClearance, + Selected.Metrics.LargestWalkableSurfaceShare, + *PreviewCandidates[ArchetypeIndex].Window.Describe())); + } + + FString IndexPath; + FString IndexError; + const bool bIndexWritten = VF_WriteStratePreviewIndex( + ShowcaseDirectory, + TEXT("VoxelForge Showcase — one strate per archetype"), + CommonWindow, + PreviewCandidates, + IndexPath, + IndexError, + &Summaries); + TestTrue(TEXT("showcase index writes to Saved/VoxelForge/Showcase"), + bIndexWritten && IndexPath == (ShowcaseDirectory / TEXT("index.html"))); + if (!bIndexWritten) + { + AddError(IndexError); + return false; + } + + TestEqual(TEXT("showcase page has exactly eight archetype cards"), + PreviewCandidates.Num(), static_cast(UE_ARRAY_COUNT(GShowcaseArchetypes))); + TestEqual(TEXT("showcase fine preview rendered count"), FineRendered, 8 - FineRefused); + AddInfo(FString::Printf( + TEXT("Showcase output: %s; fine rendered=%d, fine refused=%d, fine blank=%d. " + "Cards use existing interior target slots and bComposerRollStructure=false; each " + "card names its target slot, and no target layout/passages were regenerated."), + *IndexPath, FineRendered, FineRefused, FineBlank)); + + const double TestSeconds = FPlatformTime::Seconds() - TestStartSeconds; + AddInfo(FString::Printf(TEXT("Composer.Showcase total runtime %.3fs."), TestSeconds)); + return true; +} + +#endif // WITH_DEV_AUTOMATION_TESTS diff --git a/Source/VoxelForge/Private/VoxelSeasonManifest.cpp b/Source/VoxelForge/Private/VoxelSeasonManifest.cpp index 9792b77..61a4ae8 100644 --- a/Source/VoxelForge/Private/VoxelSeasonManifest.cpp +++ b/Source/VoxelForge/Private/VoxelSeasonManifest.cpp @@ -873,6 +873,16 @@ namespace VoxelSeasonManifestPrivate Object->SetNumberField(TEXT("num_components_at_least_1_pct"), Metrics.NumComponentsAtLeast1Pct); SetMetricFloat(Object, TEXT("walkable_fraction"), Metrics.WalkableFraction); + Object->SetNumberField(TEXT("walkable_floor_columns"), + static_cast(Metrics.WalkableFloorColumns)); + SetMetricFloat(Object, TEXT("walkable_floor_area_fraction"), + Metrics.WalkableFloorAreaFraction); + Object->SetNumberField(TEXT("num_walkable_surface_components"), + Metrics.NumWalkableSurfaceComponents); + Object->SetNumberField(TEXT("largest_walkable_surface_columns"), + static_cast(Metrics.LargestWalkableSurfaceColumns)); + SetMetricFloat(Object, TEXT("largest_walkable_surface_share"), + Metrics.LargestWalkableSurfaceShare); SetMetricFloat(Object, TEXT("median_feature_scale"), Metrics.MedianFeatureScale); Object->SetNumberField(TEXT("median_vertical_clearance"), Metrics.MedianVerticalClearance); Object->SetNumberField(TEXT("resolved_margin_voxels"), Metrics.ResolvedMarginVoxels); @@ -914,6 +924,19 @@ namespace VoxelSeasonManifestPrivate bValid = ReadJsonInt64(Object, TEXT("largest_component_cells"), OutMetrics.LargestComponentCells) && bValid; bValid = ReadJsonInt32(Object, TEXT("num_components_at_least_1_pct"), OutMetrics.NumComponentsAtLeast1Pct) && bValid; bValid = ReadMetricFloat(Object, TEXT("walkable_fraction"), OutMetrics.WalkableFraction) && bValid; + if (Object->HasField(TEXT("walkable_floor_columns"))) + { + bValid = ReadJsonInt64(Object, TEXT("walkable_floor_columns"), + OutMetrics.WalkableFloorColumns) && bValid; + bValid = ReadMetricFloat(Object, TEXT("walkable_floor_area_fraction"), + OutMetrics.WalkableFloorAreaFraction) && bValid; + bValid = ReadJsonInt32(Object, TEXT("num_walkable_surface_components"), + OutMetrics.NumWalkableSurfaceComponents) && bValid; + bValid = ReadJsonInt64(Object, TEXT("largest_walkable_surface_columns"), + OutMetrics.LargestWalkableSurfaceColumns) && bValid; + bValid = ReadMetricFloat(Object, TEXT("largest_walkable_surface_share"), + OutMetrics.LargestWalkableSurfaceShare) && bValid; + } bValid = ReadMetricFloat(Object, TEXT("median_feature_scale"), OutMetrics.MedianFeatureScale) && bValid; bValid = ReadJsonInt32(Object, TEXT("median_vertical_clearance"), OutMetrics.MedianVerticalClearance) && bValid; bValid = ReadJsonInt32(Object, TEXT("resolved_margin_voxels"), OutMetrics.ResolvedMarginVoxels) && bValid; diff --git a/Source/VoxelForge/Private/VoxelStrateComposer.cpp b/Source/VoxelForge/Private/VoxelStrateComposer.cpp index 372ef7b..51c91b3 100644 --- a/Source/VoxelForge/Private/VoxelStrateComposer.cpp +++ b/Source/VoxelForge/Private/VoxelStrateComposer.cpp @@ -2275,6 +2275,8 @@ bool FVoxelStrateMeasuredMetrics::IsUsable() const && FMath::IsFinite(AirFraction) && FMath::IsFinite(LargestComponentShare) && FMath::IsFinite(WalkableFraction) + && FMath::IsFinite(WalkableFloorAreaFraction) + && FMath::IsFinite(LargestWalkableSurfaceShare) && FMath::IsFinite(MedianFeatureScale) && FMath::IsFinite(LargestComponentPoint.X) && FMath::IsFinite(LargestComponentPoint.Y) @@ -2296,6 +2298,11 @@ FVoxelStrateMeasuredMetrics VF_SummarizeStrateMetrics( Result.LargestComponentCells = Metrics.LargestComponentCells; Result.NumComponentsAtLeast1Pct = Metrics.NumComponentsAtLeast1Pct; Result.WalkableFraction = Metrics.WalkableFraction; + Result.WalkableFloorColumns = Metrics.WalkableFloorColumns; + Result.WalkableFloorAreaFraction = Metrics.WalkableFloorAreaFraction; + Result.NumWalkableSurfaceComponents = Metrics.NumWalkableSurfaceComponents; + Result.LargestWalkableSurfaceColumns = Metrics.LargestWalkableSurfaceColumns; + Result.LargestWalkableSurfaceShare = Metrics.LargestWalkableSurfaceShare; Result.MedianFeatureScale = Metrics.MedianFeatureScale; Result.MedianVerticalClearance = Metrics.MedianVerticalClearance; Result.ResolvedMarginVoxels = Metrics.ResolvedMarginVoxels; @@ -2779,6 +2786,16 @@ namespace Object->SetNumberField(TEXT("num_components_at_least_1_pct"), Metrics.NumComponentsAtLeast1Pct); Object->SetNumberField(TEXT("walkable_fraction"), Metrics.WalkableFraction); + Object->SetNumberField(TEXT("walkable_floor_columns"), + static_cast(Metrics.WalkableFloorColumns)); + Object->SetNumberField(TEXT("walkable_floor_area_fraction"), + Metrics.WalkableFloorAreaFraction); + Object->SetNumberField(TEXT("num_walkable_surface_components"), + Metrics.NumWalkableSurfaceComponents); + Object->SetNumberField(TEXT("largest_walkable_surface_columns"), + static_cast(Metrics.LargestWalkableSurfaceColumns)); + Object->SetNumberField(TEXT("largest_walkable_surface_share"), + Metrics.LargestWalkableSurfaceShare); Object->SetNumberField(TEXT("median_feature_scale"), Metrics.MedianFeatureScale); Object->SetNumberField(TEXT("median_vertical_clearance"), Metrics.MedianVerticalClearance); Object->SetNumberField(TEXT("resolved_margin_voxels"), Metrics.ResolvedMarginVoxels); @@ -2836,6 +2853,21 @@ namespace bValid = VF_ReadJsonInt32(Object, TEXT("num_components_at_least_1_pct"), OutMetrics.NumComponentsAtLeast1Pct) && bValid; bValid = VF_ReadJsonFloat(Object, TEXT("walkable_fraction"), OutMetrics.WalkableFraction) && bValid; + // These fields were added after the first promotion-store schema. Keep old records + // readable: their absence means the new diagnostic is unknown, not zero evidence. + if (Object->HasField(TEXT("walkable_floor_columns"))) + { + bValid = VF_ReadJsonInt64(Object, TEXT("walkable_floor_columns"), + OutMetrics.WalkableFloorColumns) && bValid; + bValid = VF_ReadJsonFloat(Object, TEXT("walkable_floor_area_fraction"), + OutMetrics.WalkableFloorAreaFraction) && bValid; + bValid = VF_ReadJsonInt32(Object, TEXT("num_walkable_surface_components"), + OutMetrics.NumWalkableSurfaceComponents) && bValid; + bValid = VF_ReadJsonInt64(Object, TEXT("largest_walkable_surface_columns"), + OutMetrics.LargestWalkableSurfaceColumns) && bValid; + bValid = VF_ReadJsonFloat(Object, TEXT("largest_walkable_surface_share"), + OutMetrics.LargestWalkableSurfaceShare) && bValid; + } bValid = VF_ReadJsonFloat(Object, TEXT("median_feature_scale"), OutMetrics.MedianFeatureScale) && bValid; bValid = VF_ReadJsonInt32(Object, TEXT("median_vertical_clearance"), @@ -2976,6 +3008,11 @@ namespace && A.LargestComponentCells == B.LargestComponentCells && A.NumComponentsAtLeast1Pct == B.NumComponentsAtLeast1Pct && A.WalkableFraction == B.WalkableFraction + && A.WalkableFloorColumns == B.WalkableFloorColumns + && A.WalkableFloorAreaFraction == B.WalkableFloorAreaFraction + && A.NumWalkableSurfaceComponents == B.NumWalkableSurfaceComponents + && A.LargestWalkableSurfaceColumns == B.LargestWalkableSurfaceColumns + && A.LargestWalkableSurfaceShare == B.LargestWalkableSurfaceShare && A.MedianFeatureScale == B.MedianFeatureScale && A.MedianVerticalClearance == B.MedianVerticalClearance && A.ResolvedMarginVoxels == B.ResolvedMarginVoxels @@ -3055,6 +3092,8 @@ namespace return FMath::IsFinite(Metrics.AirFraction) && FMath::IsFinite(Metrics.LargestComponentShare) && FMath::IsFinite(Metrics.WalkableFraction) + && FMath::IsFinite(Metrics.WalkableFloorAreaFraction) + && FMath::IsFinite(Metrics.LargestWalkableSurfaceShare) && FMath::IsFinite(Metrics.MedianFeatureScale) && FMath::IsFinite(Metrics.LargestComponentPoint.X) && FMath::IsFinite(Metrics.LargestComponentPoint.Y) diff --git a/Source/VoxelForge/Private/VoxelStrateMeasure.cpp b/Source/VoxelForge/Private/VoxelStrateMeasure.cpp index d5e2d0a..8b72fbf 100644 --- a/Source/VoxelForge/Private/VoxelStrateMeasure.cpp +++ b/Source/VoxelForge/Private/VoxelStrateMeasure.cpp @@ -639,6 +639,10 @@ namespace VoxelStrateMeasurePrivate TArray ClearanceValues; ClearanceValues.Reserve(static_cast(NumAir)); int64 NumWalkable = 0; + const int64 XYColumnCount64 = static_cast(Grid.NumX) + * static_cast(Grid.NumY); + TArray WalkableColumns; + WalkableColumns.Init(0u, static_cast(XYColumnCount64)); for (int32 Z = 0; Z < Grid.NumZ; ++Z) { @@ -667,6 +671,7 @@ namespace VoxelStrateMeasurePrivate if (!bHasHeadroom) continue; ++NumWalkable; + WalkableColumns[X + Grid.NumX * Y] = 1u; int32 ClearanceCells = 0; while (static_cast(Z) + ClearanceCells < Grid.NumZ && Grid.Air[Grid.Index(X, Y, Z + ClearanceCells)] != 0u) @@ -685,6 +690,64 @@ namespace VoxelStrateMeasurePrivate ? static_cast(static_cast(NumWalkable) / static_cast(NumAir)) : 0.0f; + for (const uint8 bWalkable : WalkableColumns) + { + InOutMetrics.WalkableFloorColumns += bWalkable != 0u ? 1 : 0; + } + InOutMetrics.WalkableFloorAreaFraction = XYColumnCount64 > 0 + ? static_cast(static_cast(InOutMetrics.WalkableFloorColumns) + / static_cast(XYColumnCount64)) + : 0.0f; + + // The area graph is intentionally projected to XY: each column contributes one floor + // footprint even when several sampled Z cells in it meet the walkability predicate. + // Keep the flood fill deterministic and bounded by the already bounded XY grid. + TArray VisitedWalkableColumns; + VisitedWalkableColumns.Init(0u, static_cast(XYColumnCount64)); + TArray SurfaceQueue; + SurfaceQueue.Reserve(static_cast(XYColumnCount64)); + for (int32 Column = 0; Column < WalkableColumns.Num(); ++Column) + { + if (WalkableColumns[Column] == 0u || VisitedWalkableColumns[Column] != 0u) + { + continue; + } + + ++InOutMetrics.NumWalkableSurfaceComponents; + VisitedWalkableColumns[Column] = 1u; + SurfaceQueue.Reset(); + SurfaceQueue.Add(Column); + int64 ComponentColumns = 0; + for (int32 QueueIndex = 0; QueueIndex < SurfaceQueue.Num(); ++QueueIndex) + { + const int32 CurrentColumn = SurfaceQueue[QueueIndex]; + ++ComponentColumns; + const int32 CurrentX = CurrentColumn % Grid.NumX; + const int32 CurrentY = CurrentColumn / Grid.NumX; + const int32 NeighbourColumns[4] = { + CurrentX > 0 ? CurrentColumn - 1 : INDEX_NONE, + CurrentX + 1 < Grid.NumX ? CurrentColumn + 1 : INDEX_NONE, + CurrentY > 0 ? CurrentColumn - Grid.NumX : INDEX_NONE, + CurrentY + 1 < Grid.NumY ? CurrentColumn + Grid.NumX : INDEX_NONE + }; + for (const int32 Neighbour : NeighbourColumns) + { + if (Neighbour != INDEX_NONE + && WalkableColumns[Neighbour] != 0u + && VisitedWalkableColumns[Neighbour] == 0u) + { + VisitedWalkableColumns[Neighbour] = 1u; + SurfaceQueue.Add(Neighbour); + } + } + } + InOutMetrics.LargestWalkableSurfaceColumns = FMath::Max( + InOutMetrics.LargestWalkableSurfaceColumns, ComponentColumns); + } + InOutMetrics.LargestWalkableSurfaceShare = InOutMetrics.WalkableFloorColumns > 0 + ? static_cast(static_cast(InOutMetrics.LargestWalkableSurfaceColumns) + / static_cast(InOutMetrics.WalkableFloorColumns)) + : 0.0f; if (ClearanceValues.Num() > 0) { diff --git a/Source/VoxelForge/Private/VoxelStratePreview.cpp b/Source/VoxelForge/Private/VoxelStratePreview.cpp index f4ce3a3..9f5e94c 100644 --- a/Source/VoxelForge/Private/VoxelStratePreview.cpp +++ b/Source/VoxelForge/Private/VoxelStratePreview.cpp @@ -89,6 +89,11 @@ namespace return bValid ? FString::Printf(TEXT("%d"), Value) : TEXT("invalid"); } + FString VF_FormatInteger64Metric(bool bValid, int64 Value) + { + return bValid ? FString::Printf(TEXT("%lld"), static_cast(Value)) : TEXT("invalid"); + } + bool VF_IsWalkableCell(const FVoxelStrateSampleGrid& Grid, int32 X, int32 Y, int32 Z, int32 HeadroomCells) { @@ -704,6 +709,11 @@ bool VF_WriteStratePreviewCandidate( OutCandidate.AirFraction = Metrics.AirFraction; OutCandidate.LargestComponentShare = Metrics.LargestComponentShare; OutCandidate.WalkableFraction = Metrics.WalkableFraction; + OutCandidate.WalkableFloorColumns = Metrics.WalkableFloorColumns; + OutCandidate.WalkableFloorAreaFraction = Metrics.WalkableFloorAreaFraction; + OutCandidate.NumWalkableSurfaceComponents = Metrics.NumWalkableSurfaceComponents; + OutCandidate.LargestWalkableSurfaceColumns = Metrics.LargestWalkableSurfaceColumns; + OutCandidate.LargestWalkableSurfaceShare = Metrics.LargestWalkableSurfaceShare; OutCandidate.MedianFeatureScale = Metrics.MedianFeatureScale; OutCandidate.MedianVerticalClearance = Metrics.MedianVerticalClearance; OutCandidate.DistanceFromCorpusCentroid = FMath::IsFinite(DistanceFromCorpusCentroid) @@ -961,7 +971,8 @@ bool VF_WriteStratePreviewIndex( const FVoxelStratePreviewWindow& Window, const TArray& Candidates, FString& OutIndexPath, - FString& OutError) + FString& OutError, + const TArray* ArchetypeSummaries) { if (!Window.IsValid()) { @@ -979,6 +990,15 @@ bool VF_WriteStratePreviewIndex( SortedCandidates.Sort([](const FVoxelStratePreviewCandidate& A, const FVoxelStratePreviewCandidate& B) { + if (A.bShowcaseCard != B.bShowcaseCard) + { + return A.bShowcaseCard; + } + if (A.bShowcaseCard && B.bShowcaseCard + && A.ArchetypeName != B.ArchetypeName) + { + return A.ArchetypeName < B.ArchetypeName; + } if (A.bSeasonOrder != B.bSeasonOrder) { return A.bSeasonOrder; @@ -1036,10 +1056,71 @@ bool VF_WriteStratePreviewIndex( ".reason{color:#ffb4b8;margin:6px 0}.recipe{color:#c9d1d9;overflow-wrap:anywhere;margin-bottom:8px}.recipe code{font:12px ui-monospace,SFMono-Regular,monospace}\n" ".window{font-size:11px;color:#8b949e;margin:5px 0 10px}.view-section{margin:12px 0}.view-section h3{font-size:12px;letter-spacing:.03em;color:#e6edf3;margin:10px 0 3px}.pair{display:grid;grid-template-columns:1fr 1fr;gap:8px}.pair figure{margin:0}.pair img{display:block;width:100%;height:auto;background:#090c11;image-rendering:pixelated;border:1px solid #30363d}.pair figcaption{font-size:11px;color:#aab4c0;margin-top:3px}.fine-section{border-top:1px solid #6e4e1f;margin-top:14px;padding-top:4px}.fine-section h3{color:#ffd580}.not-requested{color:#8b949e;font-size:12px;margin:8px 0}.contour-note{color:#8cebd1}\n" ".no-image{min-height:40px;border:1px dashed #8b3b42;color:#ffb4b8;padding:12px;font-size:12px}\n" + ".pie-banner{border:2px solid #f5a636;background:#2b2112;color:#ffe5b0;border-radius:8px;padding:14px;margin:16px 0;font-size:15px}\n" + ".pie-card{border:2px solid #f5a636;background:#2b2112;color:#ffe5b0;border-radius:6px;padding:10px;margin:8px 0 12px;font-size:13px;line-height:1.6}\n" + ".pie-card code{font:13px ui-monospace,SFMono-Regular,monospace;color:#fff1ce}\n" + ".sources a{color:#8cebd1}.summary{display:block;overflow-x:auto;white-space:nowrap}.summary th,.summary td{vertical-align:top}.summary-window{white-space:normal;min-width:260px}\n" "table{width:100%;border-collapse:collapse;margin-top:10px;font-size:12px}th,td{padding:3px 4px;border-bottom:1px solid #30363d;text-align:left}th{color:#aab4c0;font-weight:500}td{text-align:right;font-variant-numeric:tabular-nums}.verdict{font-weight:650}\n" ".footer{color:#8b949e;font-size:11px;margin-top:18px}\n" "\n"); Html += FString::Printf(TEXT("

%s

\n"), *VF_HtmlEscape(RunTitle)); + const bool bShowcase = ArchetypeSummaries != nullptr && !ArchetypeSummaries->IsEmpty(); + if (bShowcase) + { + Html += TEXT( + "
PIE WALK-THROUGH — exact values per card " + "Set the four displayed properties on the VoxelWorld actor, enter PIE, then click " + "Apply Composer Candidate. The candidate seed/index are the same " + "offline inputs used to produce these images. This page never changes runtime " + "generation by itself.
\n" + "

Showcase scope: all cards use the native " + "single-region path with bComposerRollStructure=false; lateral regions " + "are deliberately gated off. The target slot is reported explicitly on each card. " + "The candidate archetype is the density replacement, while the live slot layout, " + "height, passages, placement, and world seed remain unchanged.

\n" + "

Part A — what the numbers mean

\n" + "

Walkable fraction is walkable sampled air cells / all sampled air " + "cells; it is a volumetric occupancy ratio and is expected to understate a large " + "room. Walkable floor area is distinct XY columns containing at least one " + "walkable cell / sampled XY footprint. Largest connected surface is the largest " + "four-neighbour component of those projected columns / all walkable columns. It is " + "not a 3D traversal proof; the arrival → departure route check is separate.

\n" + "

Part A measurement settings: step=4 voxels, " + "HeadroomCells=2, XY radius=256 voxels, MaxCells=8,000,000. Each summary row lists " + "the exact target, derived interior margin, Z range, XY range, and grid for every " + "hard-gate survivor; each card repeats its selected window.

\n" + "

Reality check: real caves mix long, narrow passages " + "with rooms, branches, multiple levels, and occasional vertical shafts; there is no " + "universal target floor-to-volume ratio. The quantitative table below is therefore a " + "diagnostic comparison across this corpus, not a tuned magic number.

\n"); + Html += TEXT( + "

References used: " + "NPS Solution Caves; " + "NPS Mammoth Cave morphology; " + "NPS Lehman Caves dimensions; " + "Deep Rock Galactic procedural cave design; " + "Team Cherry map-design interview.

\n" + "\n" + "" + "" + "\n"); + for (const FVoxelStratePreviewArchetypeSummary& Summary : *ArchetypeSummaries) + { + const FString SummaryWindow = !Summary.WindowSummary.IsEmpty() + ? Summary.WindowSummary + : Summary.Window.IsValid() ? Summary.Window.Describe() : TEXT("unavailable"); + Html += FString::Printf( + TEXT("\n"), + *VF_HtmlEscape(Summary.ArchetypeName), Summary.EvaluatedCandidates, + Summary.HardGateSurvivors, + *VF_HtmlEscape(Summary.WalkableFractionSummary), + *VF_HtmlEscape(Summary.WalkableFloorAreaSummary), + *VF_HtmlEscape(Summary.MedianVerticalClearanceSummary), + *VF_HtmlEscape(Summary.LargestWalkableSurfaceSummary), + *VF_HtmlEscape(SummaryWindow)); + } + Html += TEXT("
Part A — survivor distributions; every value uses the exact window printed in its row
archetypeeligible single-region rolls / hard-gate survivorswalkable fraction
(median; min–max)
floor area fraction
(median; min–max)
median clearance over walkable cells
(median; min–max voxels)
largest connected surface
(median; min–max)
window
%s%d / %d%s%s%s%s%s
\n"); + } Html += FString::Printf( TEXT("

How to read coarse images: At sample step %d, " "hard edges can come from sampling, not necessarily hard world walls. A " @@ -1085,20 +1166,41 @@ bool VF_WriteStratePreviewIndex( ? Candidate.Window.Describe() : WindowDescription; const int32 DisplayIndex = Candidate.bSeasonOrder && Candidate.DepthIndex != INDEX_NONE ? Candidate.DepthIndex : Candidate.CandidateIndex; + const FString CardTitle = Candidate.bShowcaseCard && !Candidate.ArchetypeName.IsEmpty() + ? FString::Printf(TEXT("%s · candidate %d"), *Candidate.ArchetypeName, Candidate.CandidateIndex) + : FString::Printf(TEXT("#%d"), DisplayIndex); Html += FString::Printf( - TEXT("

#%d %s

\n"), - *CardClass, DisplayIndex, *StatusClass, *Status); - Html += FString::Printf(TEXT("
%s
\n"), - *VF_HtmlEscape(Candidate.RecipeString)); - if (Candidate.bSeasonOrder) + TEXT("

%s %s

\n"), + *CardClass, *VF_HtmlEscape(CardTitle), *StatusClass, *Status); + if (Candidate.bShowcaseCard) { + Html += FString::Printf( + TEXT("
PIE — type exactly, then click Apply Composer Candidate" + "
ComposerSeed = %d" + "
ComposerCandidateIndex = %d" + "
ComposerTargetStrateIndex = %d" + "
bComposerRollStructure = %s
\n"), + Candidate.ComposerSeed, Candidate.CandidateIndex, + Candidate.ComposerTargetStrateIndex, + Candidate.bComposerRollStructure ? TEXT("true") : TEXT("false")); + } + Html += FString::Printf(TEXT("
%s
\n"), + *VF_HtmlEscape(Candidate.RecipeString)); + if (Candidate.bSeasonOrder || Candidate.bShowcaseCard) + { + const FString SelectionExtra = Candidate.bSeasonOrder + ? FString::Printf(TEXT("
Boss slot: %s"), + Candidate.bBossSlot ? TEXT("yes") : TEXT("no")) + : FString(); Html += FString::Printf( TEXT("

Why selected: %s" - "
Boss slot: %s

\n"), + "%s

\n"), *VF_HtmlEscape(Candidate.SelectionReason.IsEmpty() - ? TEXT("selected by the provisional season policy") + ? (Candidate.bShowcaseCard + ? TEXT("selected from the hard-gate survivor set by floor area, then vertical clearance") + : TEXT("selected by the provisional season policy")) : Candidate.SelectionReason), - Candidate.bBossSlot ? TEXT("yes") : TEXT("no")); + *SelectionExtra); } if (Candidate.bRejected) { @@ -1182,6 +1284,12 @@ bool VF_WriteStratePreviewIndex( TEXT("
Fine ROI requested but refused/not rendered: %s
\n"), *VF_HtmlEscape(FineFailureText)); } + else if (Candidate.bFineBlank) + { + Html += TEXT("
Fine ROI is blank at both selected slices. " + "That is reported as a content finding; empty images are intentionally not " + "shown as evidence of a visible room.
\n"); + } else { const FString FineWindowDescription = Candidate.FineWindow.Describe(); @@ -1235,6 +1343,10 @@ bool VF_WriteStratePreviewIndex( TEXT("air fraction%s\n" "largest component share%s\n" "walkable fraction%s\n" + "walkable floor area fraction%s\n" + "walkable floor columns%s\n" + "largest connected surface share%s\n" + "walkable surface components%s\n" "feature scale (voxels)%s\n" "vertical clearance (voxels)%s\n" "arrival → departure%s\n" @@ -1242,6 +1354,10 @@ bool VF_WriteStratePreviewIndex( *VF_FormatMetric(Candidate.bMetricsValid, Candidate.AirFraction), *VF_FormatMetric(Candidate.bMetricsValid, Candidate.LargestComponentShare), *VF_FormatMetric(Candidate.bMetricsValid, Candidate.WalkableFraction), + *VF_FormatMetric(Candidate.bMetricsValid, Candidate.WalkableFloorAreaFraction), + *VF_FormatInteger64Metric(Candidate.bMetricsValid, Candidate.WalkableFloorColumns), + *VF_FormatMetric(Candidate.bMetricsValid, Candidate.LargestWalkableSurfaceShare), + *VF_FormatIntegerMetric(Candidate.bMetricsValid, Candidate.NumWalkableSurfaceComponents), *VF_FormatMetric(Candidate.bMetricsValid, Candidate.MedianFeatureScale), *VF_FormatIntegerMetric(Candidate.bMetricsValid, Candidate.MedianVerticalClearance), *VF_HtmlEscape(Candidate.ArrivalDepartureVerdict), @@ -1317,12 +1433,14 @@ bool VF_WriteStratePreviewIndex( const FVoxelStratePreviewWindow& Window, const TArray& Candidates, FString& OutIndexPath, - FString& OutError) + FString& OutError, + const TArray* ArchetypeSummaries) { (void)OutputDirectory; (void)RunTitle; (void)Window; (void)Candidates; + (void)ArchetypeSummaries; OutIndexPath.Reset(); OutError = TEXT("strate preview rendering is editor-only"); return false; diff --git a/Source/VoxelForge/Public/VoxelStrateComposer.h b/Source/VoxelForge/Public/VoxelStrateComposer.h index 5b8797f..b0919d7 100644 --- a/Source/VoxelForge/Public/VoxelStrateComposer.h +++ b/Source/VoxelForge/Public/VoxelStrateComposer.h @@ -192,6 +192,11 @@ struct VOXELFORGE_API FVoxelStrateMeasuredMetrics int64 LargestComponentCells = 0; int32 NumComponentsAtLeast1Pct = 0; float WalkableFraction = 0.0f; + int64 WalkableFloorColumns = 0; + float WalkableFloorAreaFraction = 0.0f; + int32 NumWalkableSurfaceComponents = 0; + int64 LargestWalkableSurfaceColumns = 0; + float LargestWalkableSurfaceShare = 0.0f; float MedianFeatureScale = 0.0f; int32 MedianVerticalClearance = 0; diff --git a/Source/VoxelForge/Public/VoxelStrateMeasure.h b/Source/VoxelForge/Public/VoxelStrateMeasure.h index f672bbb..af579e6 100644 --- a/Source/VoxelForge/Public/VoxelStrateMeasure.h +++ b/Source/VoxelForge/Public/VoxelStrateMeasure.h @@ -121,6 +121,17 @@ struct VOXELFORGE_API FVoxelStrateMetrics int64 LargestComponentCells = 0; int32 NumComponentsAtLeast1Pct = 0; float WalkableFraction = 0.f; + // Distinct sampled XY columns that contain at least one walkable cell, divided by the + // sampled XY footprint. This is a floor-area proxy, not a volume ratio: a large chamber + // can have a small WalkableFraction while retaining a substantial walkable floor. + int64 WalkableFloorColumns = 0; + float WalkableFloorAreaFraction = 0.f; + // Four-neighbour connected components of the projected walkable columns. This deliberately + // remains a 2D surface indicator; it does not prove that floors at different Z values are + // traversable in 3D. The separate arrival/departure route check is the traversal gate. + int32 NumWalkableSurfaceComponents = 0; + int64 LargestWalkableSurfaceColumns = 0; + float LargestWalkableSurfaceShare = 0.f; float MedianFeatureScale = 0.f; int32 MedianVerticalClearance = 0; diff --git a/Source/VoxelForge/Public/VoxelStratePreview.h b/Source/VoxelForge/Public/VoxelStratePreview.h index 2ae89fd..9d9e04f 100644 --- a/Source/VoxelForge/Public/VoxelStratePreview.h +++ b/Source/VoxelForge/Public/VoxelStratePreview.h @@ -71,6 +71,13 @@ struct VOXELFORGE_API FVoxelStrateFinePreviewSettings struct VOXELFORGE_API FVoxelStratePreviewCandidate { int32 CandidateIndex = INDEX_NONE; + // Showcase cards carry their archetype and the exact editor hand-off values. Ordinary + // contact-sheet callers may leave these unset and retain the legacy candidate heading. + FString ArchetypeName; + bool bShowcaseCard = false; + int32 ComposerSeed = INDEX_NONE; + int32 ComposerTargetStrateIndex = INDEX_NONE; + bool bComposerRollStructure = false; // Season pages deliberately opt into descent order. The ordinary candidate contact sheet // keeps its survivor/outlier ordering; a season review must read top-to-bottom like play. bool bSeasonOrder = false; @@ -110,6 +117,11 @@ struct VOXELFORGE_API FVoxelStratePreviewCandidate float AirFraction = 0.0f; float LargestComponentShare = 0.0f; float WalkableFraction = 0.0f; + int64 WalkableFloorColumns = 0; + float WalkableFloorAreaFraction = 0.0f; + int32 NumWalkableSurfaceComponents = 0; + int64 LargestWalkableSurfaceColumns = 0; + float LargestWalkableSurfaceShare = 0.0f; float MedianFeatureScale = 0.0f; int32 MedianVerticalClearance = 0; double DistanceFromCorpusCentroid = 0.0; @@ -135,6 +147,21 @@ struct VOXELFORGE_API FVoxelStratePreviewCandidate int32 FinePlanImageHeight = 0; }; +/** Aggregate Part A row for a stable archetype-level review table. */ +struct VOXELFORGE_API FVoxelStratePreviewArchetypeSummary +{ + FString ArchetypeName; + int32 EvaluatedCandidates = 0; + int32 HardGateSurvivors = 0; + FVoxelStratePreviewWindow Window; + FString WindowSummary; + + FString WalkableFractionSummary; + FString WalkableFloorAreaSummary; + FString MedianVerticalClearanceSummary; + FString LargestWalkableSurfaceSummary; +}; + /** Copy the exact measurement-window metadata needed by the index page. */ VOXELFORGE_API FVoxelStratePreviewWindow VF_GetStratePreviewWindow( const FVoxelStrateSampleGrid& Grid); @@ -184,4 +211,5 @@ VOXELFORGE_API bool VF_WriteStratePreviewIndex( const FVoxelStratePreviewWindow& Window, const TArray& Candidates, FString& OutIndexPath, - FString& OutError); + FString& OutError, + const TArray* ArchetypeSummaries = nullptr);