From 088b40216d8f276ab2f16dd1ce57267594128fff Mon Sep 17 00:00:00 2001 From: Fr0zka Date: Sat, 5 Sep 2026 06:59:51 +0200 Subject: [PATCH] feat(composer): showcase of all 8 archetypes -- and "not navigable" answered MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Jahni asked for one strate per archetype to look at, and said the previews looked "not navigable nor anything except maybe the mazes". Both delivered. ⭐ PART A -- HIS EYE WAS RIGHT, AND SO WAS THE METRIC. BOTH EXPLANATIONS WERE TRUE, and they split by archetype: archetype walkable FLOOR AREA largest connected surface CrystalChamber .059 .9999 1.00 FlatPlain .048 .9985 .9998 SurfaceWorld .064 .9374 .9936 Maze .285 .2629 .9905 Underwater .229 .2061 .3312 TunnelNetwork .155 .2084 .4640 VerticalShafts .108 .1138 .1449 FloatingIslands .004 .1016 .4992 1. THE METRIC WAS WRONG FOR CHAMBERS. Walkable fraction is volumetric: an air cell with solid one step below and headroom above. In a cathedral almost no air cell is near a floor, so a huge open room scores ~.06 while a tight maze scores ~.29. It was measuring CORRIDOR-NESS, not navigability -- and mazes win that by construction, which is exactly the pattern he spotted. Slab families actually have 94-100% walkable FLOOR AREA, fully connected. They were never the problem. 2. AND FOUR ARCHETYPES ARE GENUINELY FRAGMENTED, which is a real content finding, not an artifact. Largest connected walkable surface: VerticalShafts .14, Underwater .33, TunnelNetwork .46, FloatingIslands .50. In VerticalShafts, 86% of the floor is unreachable from the biggest patch. The primordial law still passes -- you CAN get from arrival to departure -- but most of the floor is not part of that. That is precisely "not navigable" and it needed a new metric to see. New metrics: WalkableFloorAreaFraction, NumWalkableSurfaceComponents, LargestWalkableSurfaceShare. A floor you cannot cross is not a floor. Web references checked, per his suggestion -- we invented every metric in this project without once testing them against reality: NPS solution-cave and Mammoth Cave morphology, Lehman Cave dimensions (Talus Room 90x376 ft, 21,511 sq ft floor, 113 ft ceiling), Deep Rock Galactic cave-design notes, Team Cherry on Metroidvania maps. They support heterogeneous room+passage space with branches and shafts -- NOT a universal floor-to-volume target. So there is no single number to optimise, which is itself worth knowing. PART B -- Saved/VoxelForge/Showcase/index.html, 8 cards, 64 PNGs, fine renders (step 1, radius 64, centred on LargestComponentPoint), 0 blanks. Each card carries the exact PIE values to type in, because navigability is judged by walking: CrystalChamber seed 7331 idx 34 slot 1 Maze seed 0 idx 58 slot 1 FlatPlain seed 0 idx 4 slot 1 SurfaceWorld seed 0 idx 11 slot 2 FloatingIslands seed 0 idx 34 slot 1 TunnelNetwork seed 0 idx 61 slot 1 Underwater seed 0 idx 46 slot 1 VerticalShafts seed 0 idx 15 slot 1 All bComposerRollStructure = false. Selection searched seeds 0/7331 x indices 0-63, applied the three hard gates, then scored floor_area + .25*clearance + .10*largest_surface. Caveats kept honest: floor area is a sampled XY proxy, projected connectivity is not a 3D traversal proof, "best" means best within the searched range. Build clean, 30 succeeded / 0 failed, verified by me. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01VJiTcTSDNkKBtnNdiBtdaU --- CODEMAP.md | 15 +- COMPOSER-NOTES.md | 69 ++ .../Tests/VoxelForgeComposerShowcaseTest.cpp | 1043 +++++++++++++++++ .../Private/VoxelSeasonManifest.cpp | 23 + .../Private/VoxelStrateComposer.cpp | 39 + .../VoxelForge/Private/VoxelStrateMeasure.cpp | 63 + .../VoxelForge/Private/VoxelStratePreview.cpp | 138 ++- .../VoxelForge/Public/VoxelStrateComposer.h | 5 + Source/VoxelForge/Public/VoxelStrateMeasure.h | 11 + Source/VoxelForge/Public/VoxelStratePreview.h | 30 +- 10 files changed, 1420 insertions(+), 16 deletions(-) create mode 100644 Source/VoxelForge/Private/Tests/VoxelForgeComposerShowcaseTest.cpp 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);