feat(composer): showcase of all 8 archetypes -- and "not navigable" answered

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJiTcTSDNkKBtnNdiBtdaU
This commit is contained in:
Fr0zkaandClaude Opus 5 committed 2026-09-05 06:59:51 +02:00
1 parent dd38832bb6
commit 088b40216d
10 files changed
+1420 -16

No files matched your search

+10 -5
View File
@@ -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. |
+69
View File
@@ -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
File diff suppressed because it is too large. Load diff
@@ -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<double>(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<double>(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;
@@ -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<double>(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<double>(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)
@@ -639,6 +639,10 @@ namespace VoxelStrateMeasurePrivate
TArray<int32> ClearanceValues;
ClearanceValues.Reserve(static_cast<int32>(NumAir));
int64 NumWalkable = 0;
const int64 XYColumnCount64 = static_cast<int64>(Grid.NumX)
* static_cast<int64>(Grid.NumY);
TArray<uint8> WalkableColumns;
WalkableColumns.Init(0u, static_cast<int32>(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<int64>(Z) + ClearanceCells < Grid.NumZ
&& Grid.Air[Grid.Index(X, Y, Z + ClearanceCells)] != 0u)
@@ -685,6 +690,64 @@ namespace VoxelStrateMeasurePrivate
? static_cast<float>(static_cast<double>(NumWalkable)
/ static_cast<double>(NumAir))
: 0.0f;
for (const uint8 bWalkable : WalkableColumns)
{
InOutMetrics.WalkableFloorColumns += bWalkable != 0u ? 1 : 0;
}
InOutMetrics.WalkableFloorAreaFraction = XYColumnCount64 > 0
? static_cast<float>(static_cast<double>(InOutMetrics.WalkableFloorColumns)
/ static_cast<double>(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<uint8> VisitedWalkableColumns;
VisitedWalkableColumns.Init(0u, static_cast<int32>(XYColumnCount64));
TArray<int32> SurfaceQueue;
SurfaceQueue.Reserve(static_cast<int32>(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<float>(static_cast<double>(InOutMetrics.LargestWalkableSurfaceColumns)
/ static_cast<double>(InOutMetrics.WalkableFloorColumns))
: 0.0f;
if (ClearanceValues.Num() > 0)
{
+128 -10
View File
@@ -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<long long>(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<FVoxelStratePreviewCandidate>& Candidates,
FString& OutIndexPath,
FString& OutError)
FString& OutError,
const TArray<FVoxelStratePreviewArchetypeSummary>* 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"
"</style></head><body>\n");
Html += FString::Printf(TEXT("<h1>%s</h1>\n"), *VF_HtmlEscape(RunTitle));
const bool bShowcase = ArchetypeSummaries != nullptr && !ArchetypeSummaries->IsEmpty();
if (bShowcase)
{
Html += TEXT(
"<div class=\"pie-banner\"><strong>PIE WALK-THROUGH — exact values per card</strong> "
"Set the four displayed properties on the VoxelWorld actor, enter PIE, then click "
"<strong>Apply Composer Candidate</strong>. The candidate seed/index are the same "
"offline inputs used to produce these images. This page never changes runtime "
"generation by itself.</div>\n"
"<p class=\"note\"><strong>Showcase scope:</strong> all cards use the native "
"single-region path with <code>bComposerRollStructure=false</code>; 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.</p>\n"
"<h2>Part A — what the numbers mean</h2>\n"
"<p class=\"note\">Walkable fraction is <em>walkable sampled air cells / all sampled air "
"cells</em>; it is a volumetric occupancy ratio and is expected to understate a large "
"room. Walkable floor area is <em>distinct XY columns containing at least one "
"walkable cell / sampled XY footprint</em>. 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.</p>\n"
"<p class=\"note\"><strong>Part A measurement settings:</strong> 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.</p>\n"
"<p class=\"note\"><strong>Reality check:</strong> 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.</p>\n");
Html += TEXT(
"<p class=\"note sources\"><strong>References used:</strong> "
"<a href=\"https://www.nps.gov/subjects/caves/solution-caves.htm\">NPS Solution Caves</a>; "
"<a href=\"https://home.nps.gov/maca/learn/nature/how-mammoth-cave-formed.htm\">NPS Mammoth Cave morphology</a>; "
"<a href=\"https://www.nps.gov/grba/learn/nature/lehman-caves-dimensions.htm\">NPS Lehman Caves dimensions</a>; "
"<a href=\"https://store.steampowered.com/news/posts/?appgroupname=Deep+Rock+Galactic&amp;appids=548430&amp;enddate=1729500950&amp;feed=steam_community_announcements\">Deep Rock Galactic procedural cave design</a>; "
"<a href=\"https://www.pcgamer.com/how-to-design-a-great-metroidvania-map/\">Team Cherry map-design interview</a>.</p>\n"
"<table class=\"summary\"><caption>Part A — survivor distributions; every value uses the exact window printed in its row</caption>\n"
"<tr><th>archetype</th><th>eligible single-region rolls / hard-gate survivors</th><th>walkable fraction<br>(median; min–max)</th>"
"<th>floor area fraction<br>(median; min–max)</th><th>median clearance over walkable cells<br>(median; min–max voxels)</th>"
"<th>largest connected surface<br>(median; min–max)</th><th>window</th></tr>\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("<tr><th>%s</th><td>%d / %d</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td class=\"summary-window\">%s</td></tr>\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("</table>\n");
}
Html += FString::Printf(
TEXT("<p class=\"note\"><strong>How to read coarse images:</strong> 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("<section class=\"%s\"><h2>#%d <span class=\"badge %s\">%s</span></h2>\n"),
*CardClass, DisplayIndex, *StatusClass, *Status);
Html += FString::Printf(TEXT("<div class=\"recipe\"><code>%s</code></div>\n"),
*VF_HtmlEscape(Candidate.RecipeString));
if (Candidate.bSeasonOrder)
TEXT("<section class=\"%s\"><h2>%s <span class=\"badge %s\">%s</span></h2>\n"),
*CardClass, *VF_HtmlEscape(CardTitle), *StatusClass, *Status);
if (Candidate.bShowcaseCard)
{
Html += FString::Printf(
TEXT("<div class=\"pie-card\"><strong>PIE — type exactly, then click Apply Composer Candidate</strong>"
"<br><code>ComposerSeed = %d</code>"
"<br><code>ComposerCandidateIndex = %d</code>"
"<br><code>ComposerTargetStrateIndex = %d</code>"
"<br><code>bComposerRollStructure = %s</code></div>\n"),
Candidate.ComposerSeed, Candidate.CandidateIndex,
Candidate.ComposerTargetStrateIndex,
Candidate.bComposerRollStructure ? TEXT("true") : TEXT("false"));
}
Html += FString::Printf(TEXT("<div class=\"recipe\"><code>%s</code></div>\n"),
*VF_HtmlEscape(Candidate.RecipeString));
if (Candidate.bSeasonOrder || Candidate.bShowcaseCard)
{
const FString SelectionExtra = Candidate.bSeasonOrder
? FString::Printf(TEXT("<br><strong>Boss slot:</strong> %s"),
Candidate.bBossSlot ? TEXT("yes") : TEXT("no"))
: FString();
Html += FString::Printf(
TEXT("<p class=\"reason\" style=\"color:#d7e3f4\"><strong>Why selected:</strong> %s"
"<br><strong>Boss slot:</strong> %s</p>\n"),
"%s</p>\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("<div class=\"no-image\">Fine ROI requested but refused/not rendered: %s</div>\n"),
*VF_HtmlEscape(FineFailureText));
}
else if (Candidate.bFineBlank)
{
Html += TEXT("<div class=\"no-image\"><strong>Fine ROI is blank at both selected slices.</strong> "
"That is reported as a content finding; empty images are intentionally not "
"shown as evidence of a visible room.</div>\n");
}
else
{
const FString FineWindowDescription = Candidate.FineWindow.Describe();
@@ -1235,6 +1343,10 @@ bool VF_WriteStratePreviewIndex(
TEXT("<tr><th>air fraction</th><td>%s</td></tr>\n"
"<tr><th>largest component share</th><td>%s</td></tr>\n"
"<tr><th>walkable fraction</th><td>%s</td></tr>\n"
"<tr><th>walkable floor area fraction</th><td>%s</td></tr>\n"
"<tr><th>walkable floor columns</th><td>%s</td></tr>\n"
"<tr><th>largest connected surface share</th><td>%s</td></tr>\n"
"<tr><th>walkable surface components</th><td>%s</td></tr>\n"
"<tr><th>feature scale (voxels)</th><td>%s</td></tr>\n"
"<tr><th>vertical clearance (voxels)</th><td>%s</td></tr>\n"
"<tr><th>arrival → departure</th><td class=\"verdict\">%s</td></tr>\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<FVoxelStratePreviewCandidate>& Candidates,
FString& OutIndexPath,
FString& OutError)
FString& OutError,
const TArray<FVoxelStratePreviewArchetypeSummary>* ArchetypeSummaries)
{
(void)OutputDirectory;
(void)RunTitle;
(void)Window;
(void)Candidates;
(void)ArchetypeSummaries;
OutIndexPath.Reset();
OutError = TEXT("strate preview rendering is editor-only");
return false;
@@ -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;
@@ -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;
+29 -1
View File
@@ -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<FVoxelStratePreviewCandidate>& Candidates,
FString& OutIndexPath,
FString& OutError);
FString& OutError,
const TArray<FVoxelStratePreviewArchetypeSummary>* ArchetypeSummaries = nullptr);