feat(composer): F1 -- the candidates are VISIBLE, and detail is invisible

PART B: the preview. Jahni, asked what "good" means for ranking a season:
"good is hard to describe, i believe it'll be easier once i'm able to see
the strates candidates." So the ranking policy was blocked on this, not on
more metrics. COMPOSER-NOTES §3.4 promised the preview comes nearly free
once the measurement exists; collected.

Per candidate, rendered from the grid the measurement ALREADY built (no
second sampling of the world): a vertical XZ section and a plan XY slice,
solid dark, air light, walkable cells highlighted, with a chunk scale bar
so a corridor is distinguishable from a cathedral. Plus a self-contained
index.html contact sheet of all 64 -- survivors first, outliers first
within them, recipes, metrics, and every rejected candidate labelled with
WHY (vacuous 3, fragmented 2, law failed 19).

  Saved/ComposerPreview/<runid>/index.html   64 candidates, 128 PNGs

Two honesty rules are on the page itself, not buried in a comment: every
number states the window that produced it (margin/step/Z span), and a
2D slice CANNOT show connectivity -- two regions that look joined in one
slice may not be.

Plan-Z is chosen by highest solid/air boundary-transition count, not
hardcoded mid-height; a slab strate would otherwise render as a blank
sheet. Codex flagged the ambiguity in my spec and picked correctly.

PART A: bootstrap jitter for zero-spread fields. ±25% of a field's own
magnitude when corpus spread is ~0, ±15% of measured spread otherwise,
clamps last. Marked in code as a BOOTSTRAP for a starved corpus that
promotion (Tier 5) should retire. 1,422 applications over 168 fields in
the parameter roll; 6,848 over 107 in the structure roll.

⚠️ AND IT DID NOT BROADEN THE DISTRIBUTIONS. Feature scale 0-376 either
way; walkable actually NARROWED (.389 -> .287). Codex's conclusion, and I
agree: the measurement is INSENSITIVE to these fields, not merely
corpus-starved.

The reason is now obvious and worth writing down: the grid samples every
4 voxels, and cave DETAIL -- terraces, scallops, ribbing -- lives at 1-3
voxels. It is below the sampling resolution. A step-4 grid structurally
cannot see feel; it sees size. So "vary the detail" and "score the detail"
need different instruments, and the preview images inherit the same limit.

Box proofs stayed sound under wider jitter: parameter roll 1,204 proved /
1,602,524 voxels / 0 violations; structure roll 659 proved / 877,129 /
0 violations. §6.2 did not reopen.

fable-idea.md F1 ticked (measurement + preview).

Build clean, 25 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-04 17:35:07 +02:00
1 parent f088c19f88
commit 993512f0bc
11 files changed
+1294 -38

No files matched your search

+14 -3
View File
@@ -319,6 +319,8 @@ the inclusive/exclusive voxel Z window used, the exact sampled dimensions/bounds
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,
so diagnostics can classify small non-largest components without a second source sample.
Callers may optionally receive that exact polarity grid as `FVoxelStrateSampleGrid`; the editor-only
composer preview consumes this hand-off rather than sampling the world again.
`VF_AreConnected` recovers a deterministic BFS parent path from the same kind of grid and checks
candidate routes at full voxel resolution before returning `Connected`. When a candidate is
refuted, its specific coarse cell edge is added to a deterministic array blocklist and BFS is
@@ -334,6 +336,7 @@ 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. |
| `FVoxelStrateSampleGrid` | Optional one-pass `density > 0` air/solid capture exported by the 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. |
| `VF_DiagnoseConnectivity` | The same query plus each mouth's air-component size/share and exact geometric distance to the nearest cell of the other component; proximity is measured in coarse-cell coordinates and does not claim a route through solid. |
@@ -453,7 +456,8 @@ remain excluded. Live fields receive one matching `UVoxelTerrainOpDefinition` he
in addition to authored strate values, so an all-default strate corpus still has useful activation
spread. Rolls are pure in `(corpus contents, seed, index)`: weighted 2–3-parent same-archetype
selection → `FStrateGenerationParams::Lerp` or reflected native-family blend → ±15% of measured
field range jitter → editor-reflection clamps → ordered-pair repair. Bool
field range jitter (or the season-zero ±25% own-magnitude bootstrap for near-zero ranges) →
editor-reflection clamps → ordered-pair repair. Bool
`bTunnelsFlowTowardOrigin` inherits from the dominant parent; integer/enum SNAP fields are not
jittered. This API is not called by runtime generation. The settings audit found **one unique path**,
`/Game/VoxelForge/DA_Strate3`, which explains the old one-vector corpus; the project assets were
@@ -468,6 +472,13 @@ it. `RequiredResources` / `ProvidedResources` close the room-state hole that cha
could not express. `VoxelForgeComposerStructureRollTest` rolls 64 candidates, measures them through
the offline sampler, and brute-forces every uniform box verdict from the novel stack itself.
**`Public/VoxelStratePreview.h` + `Private/VoxelStratePreview.cpp`** — editor/automation-only PNG
renderer and self-contained contact sheet. It consumes `FVoxelStrateSampleGrid` from the measurement
pass, emits deterministic centre-Y XZ and data-selected XY slices via `IImageWrapper`, caps each
image at 512 pixels (including its scale footer), and sorts the 64-card sheet by survivor status then
descending corpus-centroid distance. The page repeats each candidate's exact measurement window,
marks rejection reasons, explains the 2D-connectivity limitation, and has no runtime generation hook.
### 3.9 Player edits — `Public/VoxelDiffLayer.h` + `.cpp`
`UVoxelDiffLayer : UObject` (h:77). Stores `FVoxelModification` (h:43: Center/Radius/Strength;
**negative Strength = carve, positive = fill**) grouped by chunk in `TMap ChunkMods`.
@@ -531,9 +542,9 @@ The plugin's first tests (`OPSTACK-PLAN.md` Phase 0.5). Run them from the editor
| `VoxelForgeOpStackIslandTest.cpp` | `VoxelForge.OpStack.FloatingIslandEquivalence` | The port that runs the stack **backwards** — void + fill vs rock + carve, same classes with the opposite sign. Counts interior-solid and open-void samples separately (on this archetype an aggregate "N solid" is dominated by the seal bands and says nothing about the islands). Counts `AllSolid` and `AllAir` verdicts **separately** too: `AllAir` is the one no cave archetype could ever prove, and it is the entire perf argument here. |
| `VoxelForgeOpStackMazeTest.cpp` | `VoxelForge.OpStack.MazeEquivalence` | **Phase 1's load-bearing test.** The 7-op Maze stack vs `GetMazeDensity` over 20k points (aiming for bit-identity; a side-of-iso disagreement is the hard fail), plus purity across workers and brute force on every box verdict the stack emits. Reports how many tiles the stack can prove uniform — today's `ClassifyTile` proves **zero** for any cave archetype. |
| `VoxelForgeStrateParamCoverageTest.cpp` | `VoxelForge.Determinism.StrateParamBlendCoverage` | **The X-macro guard** (added 2026-08-17). `FStrateGenerationParams::Lerp` blends the hand-written `VF_STRATE_PARAM_FIELDS` list, **not** the struct — so a field added to one and not the other compiles, tests green, and silently takes its **default** inside every Gradient/Interleaved transition band. This expands the X-macro a **third** way (after LERP and SNAP), into a name list, and diffs it against the struct's UObject reflection. Pure shape test: no fixture, no world, instant. `GExemptFieldNames` is **empty** — every reflected field is covered today, and any exemption must be written down as a decision. Stakes rise with the world composer, which intends to invent parameter sets through this same `Lerp` (`COMPOSER-NOTES.md`). |
| `VoxelForgeComposerParameterRollTest.cpp` | `VoxelForge.Composer.ParameterRoll` | Asset-Registry corpus audit + complete per-archetype spread/exclusion/clamp table; asserts bit-identical deterministic rerolls and same-archetype parents; measures 64 transient candidate strates with `VF_MeasureStrate` plus the exact unsnapped arrival→departure law; brute-forces every rolled production box verdict. Current detail-enabled run: **59/64 survival (92.2%)**, **1,332 Mixed + 553 AllSolid + 675 AllAir = 1,228 proved boxes**, **1,634,468 lattice voxels checked, 0 violations**, 113.317 s total. Feature scale **0→376**; walkable **0.000000→0.388506**; all 17 TunnelNetwork/Underwater candidates carried live detail activation. |
| `VoxelForgeComposerParameterRollTest.cpp` | `VoxelForge.Composer.ParameterRoll` | Asset-Registry corpus audit + complete per-archetype spread/exclusion/clamp table; asserts bit-identical deterministic rerolls and same-archetype parents; measures 64 transient candidate strates with `VF_MeasureStrate` plus the exact unsnapped arrival→departure law; brute-forces every rolled production box verdict. Bootstrap follow-up: **1,422 applications / 168 distinct fields**; **38/64 survival**, feature scale **0→376**, walkable **0.000000→0.286766** (no broadening beyond the prior run); **1,356 Mixed + 608 AllSolid + 596 AllAir = 1,204 proved boxes**, **1,602,524 lattice voxels checked, 0 violations**, 146.250 s total in the final full-namespace run. |
| ″ | `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. |
| `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, checks exact arrival→departure connectivity, rerolls every recipe/stack for determinism, and brute-forces every uniform box verdict. Focused run: **46/64 survival (71.9%)**, **64 distinct recipes**, **RoomGraph 15 / Lattice 19 / Shaft 8 / Island 14 / Noise 8**, **k=4:11 / 5:13 / 6:11 / 7:10 / 8:19**, **489 proved boxes / 650,859 voxels / 0 violations**. |
| `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 XZ/XY preview, checks exact arrival→departure connectivity, rerolls every recipe/stack for determinism, and brute-forces every uniform box verdict. Focused run: **42/64 survival (65.6%)**, **64 distinct recipes**, **6,848 bootstrap applications / 107 distinct fields**, **659 proved boxes / 877,129 voxels / 0 violations**, **64 rendered pairs + index.html**. |
| `VoxelForgeLayoutOrderIndependenceTest.cpp` | `VoxelForge.Determinism.LayoutOrderIndependence` | Builds a known transient soft-pointer pool, then rebuilds it in original, reversed, and swapped orders. Requires a non-empty layout and passage set, and compares every slot's definition/Z/height plus passage endpoints, radius, type, control geometry, and bounds bit-for-bit. |
| `VoxelForgePassageOpenSpaceTest.cpp` | `VoxelForge.Determinism.PassageLandsInOpenSpace` | Uses the real fixture density path to check every generated inter-strate passage whose destination query answers: a 16-point ring outside the mouth's carve/blend band has at least half its samples in destination air, and the endpoint matches the pure open-point result within the mouth's float envelope. This is a connectivity proxy, not a flood-fill proof. Reports checked passages and false/unanswerable archetypes; fails if it inspects zero passages. |
| `VoxelForgeStrateConnectivityTest.cpp` | `VoxelForge.Generation.StrateConnectivity` / `VoxelForge.Generation.StrateConnectivityRefinement` / `VoxelForge.Generation.VerticalShaftSeamFreedom` | Bounded strate metrics with density-polarity and solid-gap controls, deterministic route rechecks, and refinement sweeps. The refinement test fits the measurement AABB to each arrival/departure mouth pair, reports exact before/after seed-6 cell counts, reruns every negative with doubled margin, measures tree orphan candidates/path reachability and roughness-bubble proxies, asserts axis landings, checks source/mirror physical paths, and requires 16/16 effective arrival→departure results across the 16 VerticalShafts seeds. Margin-binding negatives are reported as measurement limits, never as gap findings. The seam test re-evaluates cell-boundary positions after warming distinct neighbouring chunk contexts in both legacy and operator-stack paths. |
+61 -8
View File
@@ -424,6 +424,29 @@ while the candidate sweep retained step 4. The test asserts zero violations and
to future changed candidates rather than treating this clean run as a license to tune the survival
rate.
### Season-zero bootstrap follow-up (2026-09-04)
The homogeneous-field trap is now handled explicitly. A field whose measured corpus range is at
or below `1e-6` takes a **bootstrap** jitter of ±25% of its own magnitude, with a one-native-unit
floor for values near zero; fields with useful spread still take the real ±15% of `max-min` rule.
The safety clamps remain a final guard and never become roll ranges. `FVoxelStrateRollInfo` reports
both the number of bootstrap applications and the distinct `archetype:field` names. The comment in
the implementation makes the lifecycle explicit: this is a bootstrap for a starved season-zero
corpus, and Tier 5 promotion should retire it as the corpus grows.
The follow-up 64-roll measurement applied the bootstrap **1,422 times across 168 distinct fields**.
It did **not** broaden the measured distributions beyond the previous run: feature scale stayed
**0→376 voxels** and walkable fraction was **0.000000→0.286766**, below the previous **0.388506**
maximum. The bootstrap is therefore active, but these two measurements are insensitive to the
changed fields in this sample; this is more useful than pretending the new knob created visible
world variety. The run had **38/64 survivors**, 60 non-vacuous candidates, 55 meeting the largest
component threshold, and 42 exact law passes; those counts are observations, not tuning targets.
The wider structure-roll jitter remained sound: **659 boxes proved**, **877,129 voxels checked**,
and **0 violations** (1,901 Mixed, 266 AllSolid, 393 AllAir). The parameter-roll box check likewise
proved **1,204 boxes**, checked **1,602,524 voxels**, and found **0 violations** (1,356 Mixed, 608
AllSolid, 596 AllAir). A violation would reopen §6.2; this run did not.
### 3.4 The measurement pass — one grid, one flood fill, three jobs
Sample a candidate strate into a coarse voxel grid, flood-fill the air **once**, derive everything from
@@ -456,8 +479,37 @@ one path at full resolution** — cheap, because only one corridor is verified.
describes a place nobody will stand in. Report **distributions (median + spread), not single values**.
**This is also F1.** `fable-idea.md`'s top-pick 2D world-preview tool is this pass with a visualisation
on top. Build the measurement, get the preview nearly free — and it is needed anyway to review a season
before publishing.
on top. The measurement hand-off and deterministic preview are now built; they are needed anyway to
review a season before publishing.
### ✅ F1 measurement + preview built (2026-09-04)
`FVoxelStrateSampleGrid` is an opt-in export of the exact `Air` polarity grid already sampled by
`VF_MeasureStrate` / `VF_MeasureStrateWithSampler`. The editor/automation renderer consumes that
buffer and never calls a density sampler, so each candidate's images and numbers come from one
window and one sample pass. `VoxelForge.Composer.StructureRoll` now writes two PNGs per candidate
and one self-contained `index.html` to:
`Saved/ComposerPreview/structure_seed_0_corpus_9dbcea85_step_4_radius_256/`
The sheet contains all **64 candidates / 128 PNGs**, survivors first and then descending normalized
distance from the corpus centroid so outliers are near the top. Every card repeats its exact resolved
interior margin, sample step, XY bounds, and inclusive/exclusive Z span before listing air, largest
component share, walkable fraction, feature scale, clearance, and arrival→departure. Rejected cards
are visibly marked and name `vacuous`, `fragmented`, and/or `law failed`.
The vertical image is an XZ section through the sampled cell nearest the strate-window centre Y. The
plan image scans every sampled Z layer, keeps layers containing both air and solid, and chooses the
one with the largest number of XY solid/air boundary transitions; ties prefer more mixed cells, more
walkable cells, then the layer nearest the vertical centre. If no mixed layer exists it chooses the
most solid fallback layer, so a slab does not silently produce a blank mid-height sheet. The focused
run's first candidate was **128×48** for vertical and **128×148** for plan; dimensions vary with the
resolved Z window. Both images use dark solid, light air, orange walkable cells, and a one-chunk
footer scale bar. The page states **one chunk = 32 voxels = 8 m**, the density sign (`> 0` is air),
the exact measurement window, the 512-pixel-per-dimension cap (including a 20-pixel footer), and
that larger source grids aggregate already-sampled cells rather than resampling. It also carries the
required caveat: a 2D apparent join is not a 3D connectivity proof; the separate arrival→departure
verdict is the connectivity check.
### 3.5 Validation in three layers, and how "good" is ever judged
@@ -739,14 +791,14 @@ sealed pocket. Worth doing even if the composer never happens. **It was real**:
### Tier 2 — the measurement pass (§3.4) — ✅ BUILT (2026-09-03), see §13
`VoxelStrateMeasure.h/.cpp` + `VoxelForge.Generation.StrateConnectivity` / `...Refinement`.
*Standalone value:* it **is** F1, `fable-idea.md`'s top-pick world-preview tool — the measurement
half. The visualisation on top is not built yet.
half plus the deterministic editor/automation visualisation described in §3.4.
**It paid for itself immediately**: it found the `VerticalShafts` primordial-law violation (§14),
which no amount of playing was likely to surface, and it caught three of its own artifacts before
they became "findings" (§13).
⬜ Remaining: the *preview* (F1's visualisation), and the danger/rarity metric families from §3.4
(fall exposure, openness, traversal mix, tortuosity, corpus-centroid distance).
⬜ Remaining: the danger/rarity metric families from §3.4 (fall exposure, openness, traversal mix,
tortuosity, corpus-centroid distance).
### Tier 3 — op-system prerequisites for free composition
- ✅ **Channel read/write declarations** on every op, plus the stack DAG validator (§3.2; built
@@ -762,11 +814,12 @@ roughness and should stay late in the stack.
### Tier 4 — the composer proper
- ✅ **Parameter roll (§3.3, Tier 4a)** — corpus spread + deterministic blend/jitter implementation
and the 64-candidate measurement pass are built; the first run exposed a one-vector settings corpus
and a vacuous cave box-verdict scan, both recorded above.
and the 64-candidate measurement pass are built; the bootstrap follow-up is 38/64 survival with
0 box-verdict violations (the first-run corpus/vacuity findings remain recorded above).
- ✅ **Structure roll (§3.2, Tier 4b)** — serialisable recipes, root polarity, declaration-derived
resource-safe modifier legality, mandatory posts, 64-candidate measurement, and direct box-verdict
brute force are built; focused result is 46/64 survival with 0 invalid recipes and 0 violations.
brute force are built; the bootstrap follow-up is 42/64 survival with 0 invalid recipes and 0
violations, plus 64 rendered preview pairs and an index.html contact sheet.
- ⬜ Reject-and-resample driven by Tier 2 (§3.5) · the **offline season pipeline** with Jahni's review
and veto (§3.1).
@@ -19,6 +19,12 @@ namespace
constexpr int32 NumCandidates = 64;
constexpr float LargestComponentSurvivalThreshold = 0.50f;
constexpr int32 BoxChecksPerCandidate = 40;
// Baseline reported by the pre-bootstrap 64-candidate run. Kept as a comparison point in the
// automation output; it is not a pass/fail threshold.
constexpr float PreviousFeatureScaleMin = 0.0f;
constexpr float PreviousFeatureScaleMax = 376.0f;
constexpr float PreviousWalkableMin = 0.0f;
constexpr float PreviousWalkableMax = 0.388506f;
const TCHAR* VF_FieldKindName(EVoxelStrateFieldKind Kind)
{
@@ -704,7 +710,8 @@ bool FVoxelForgeComposerParameterRollTest::RunTest(const FString& Parameters)
static_cast<int32>(UE_ARRAY_COUNT(DeadTerrainDetailFields)), 11);
FString SpreadTable = TEXT(
"CORPUS SPREAD (population stddev; jitter range = ±15% of max-min; clamps are safety only)\n"
"CORPUS SPREAD (population stddev; spread jitter = ±15% of max-min; near-zero spread "
"uses ±25% of field magnitude with a one-unit floor; clamps are safety only)\n"
"archetype | struct | field | kind | excluded | reflected | authored | op-defaults | samples | min | max | mean | stddev | clamp\n");
for (const FVoxelStrateFieldSpread& Spread : Corpus.GetFieldSpreads())
{
@@ -799,6 +806,12 @@ bool FVoxelForgeComposerParameterRollTest::RunTest(const FString& Parameters)
int32 LiveActivationVariationCounts[9] = {};
TArray<int32> BoxViolationCandidates;
FString FirstBoxViolation;
int32 TotalBootstrapJitterFields = 0;
TSet<FString> BootstrapJitterFieldNames;
float FeatureScaleMin = FLT_MAX;
float FeatureScaleMax = -FLT_MAX;
float WalkableMin = FLT_MAX;
float WalkableMax = -FLT_MAX;
const double RollAndMeasureStartSeconds = FPlatformTime::Seconds();
for (int32 CandidateIndex = 0; CandidateIndex < NumCandidates; ++CandidateIndex)
@@ -807,6 +820,12 @@ bool FVoxelForgeComposerParameterRollTest::RunTest(const FString& Parameters)
Corpus, AuthoredSettings->Seed, CandidateIndex);
TestTrue(FString::Printf(TEXT("candidate %d roll is valid"), CandidateIndex), Roll.bValid);
TotalBootstrapJitterFields += Roll.BootstrapJitterFieldCount;
for (const FString& FieldName : Roll.BootstrapJitterFieldNames)
{
BootstrapJitterFieldNames.Add(FieldName);
}
FString ParentText = VF_FormatParents(Corpus, Roll);
FString AirText = TEXT("invalid");
FString LargestText = TEXT("invalid");
@@ -879,7 +898,9 @@ bool FVoxelForgeComposerParameterRollTest::RunTest(const FString& Parameters)
&& VF_AreStrateArchetypeParamsBitIdentical(
RepeatRoll.ArchetypeParams, Roll.ArchetypeParams, Roll.Archetype)
&& RepeatRoll.ParentEntryIndices == Roll.ParentEntryIndices
&& RepeatRoll.ParentWeights == Roll.ParentWeights);
&& RepeatRoll.ParentWeights == Roll.ParentWeights
&& RepeatRoll.BootstrapJitterFieldCount == Roll.BootstrapJitterFieldCount
&& RepeatRoll.BootstrapJitterFieldNames == Roll.BootstrapJitterFieldNames);
// Put the invented vector into a transient fixture definition. This is an offline test
// world only; the runtime manager/generation code and authored assets are untouched.
@@ -957,6 +978,10 @@ bool FVoxelForgeComposerParameterRollTest::RunTest(const FString& Parameters)
LargestText = FString::Printf(TEXT("%.6f"), Metrics.LargestComponentShare);
WalkableText = FString::Printf(TEXT("%.6f"), Metrics.WalkableFraction);
FeatureText = FString::Printf(TEXT("%.6f"), Metrics.MedianFeatureScale);
FeatureScaleMin = FMath::Min(FeatureScaleMin, Metrics.MedianFeatureScale);
FeatureScaleMax = FMath::Max(FeatureScaleMax, Metrics.MedianFeatureScale);
WalkableMin = FMath::Min(WalkableMin, Metrics.WalkableFraction);
WalkableMax = FMath::Max(WalkableMax, Metrics.WalkableFraction);
}
const FBoxVerdictReport BoxReport = VF_CheckRolledBoxVerdicts(
@@ -996,6 +1021,31 @@ bool FVoxelForgeComposerParameterRollTest::RunTest(const FString& Parameters)
const double RollAndMeasureSeconds = FPlatformTime::Seconds() - RollAndMeasureStartSeconds;
AddInfo(CandidateTable);
const bool bHaveFeatureDistribution = FeatureScaleMin != FLT_MAX;
const bool bHaveWalkableDistribution = WalkableMin != FLT_MAX;
const bool bFeatureBroadened = bHaveFeatureDistribution
&& (FeatureScaleMin < PreviousFeatureScaleMin
|| FeatureScaleMax > PreviousFeatureScaleMax);
const bool bWalkableBroadened = bHaveWalkableDistribution
&& (WalkableMin < PreviousWalkableMin
|| WalkableMax > PreviousWalkableMax);
AddInfo(FString::Printf(
TEXT("Bootstrap jitter: %d field applications across 64 rolls; %d distinct "
"archetype fields took the bootstrap path. Spread-derived jitter remains ±15%%; "
"bootstrap is ±25%% of own magnitude with a one-unit near-zero floor."),
TotalBootstrapJitterFields, BootstrapJitterFieldNames.Num()));
AddInfo(FString::Printf(
TEXT("Measured distribution after bootstrap: feature scale %.6f..%.6f voxels "
"(previous %.6f..%.6f; broadened=%s); walkable %.6f..%.6f "
"(previous %.6f..%.6f; broadened=%s)."),
FeatureScaleMin, FeatureScaleMax,
PreviousFeatureScaleMin, PreviousFeatureScaleMax,
bFeatureBroadened ? TEXT("yes") : TEXT("no"),
WalkableMin, WalkableMax,
PreviousWalkableMin, PreviousWalkableMax,
bWalkableBroadened ? TEXT("yes") : TEXT("no")));
TestTrue(TEXT("zero-spread bootstrap path is exercised by the 64-roll corpus"),
TotalBootstrapJitterFields > 0 && BootstrapJitterFieldNames.Num() > 0);
AddInfo(FString::Printf(
TEXT("Survival: %d/%d (%.1f%%). Criteria: non-vacuous (valid, sampled, air>0, solid>0), "
"largest component share >= %.2f, and exact unsnapped arrival->departure law "
@@ -11,6 +11,8 @@
#include "VoxelSettings.h"
#include "VoxelStrateComposer.h"
#include "VoxelStrateMeasure.h"
#include "VoxelStratePreview.h"
#include "Misc/Paths.h"
namespace
{
@@ -321,6 +323,18 @@ bool FVoxelForgeComposerStructureRollTest::RunTest(const FString& Parameters)
int64 TotalBoxCheckedVoxels = 0;
int32 TotalBoxViolations = 0;
FString FirstBoxViolation;
int32 TotalBootstrapJitterFields = 0;
TSet<FString> BootstrapJitterFieldNames;
TArray<FVoxelStratePreviewCandidate> PreviewCandidates;
PreviewCandidates.Reserve(NumCandidates);
FVoxelStratePreviewWindow PreviewWindow;
bool bPreviewWindowSet = false;
const FString PreviewRunId = FString::Printf(
TEXT("structure_seed_%d_corpus_%08x_step_%d_radius_%d"),
AuthoredSettings->Seed, Corpus.GetContentsHash(),
MeasureSettings.SampleStep, MeasureSettings.RadiusInVoxels);
const FString PreviewDirectory = FPaths::ProjectSavedDir()
/ TEXT("ComposerPreview") / PreviewRunId;
TArray<int32> ShapeCounts;
ShapeCounts.Init(0, 5);
int32 KCounts[9] = { 0 };
@@ -366,6 +380,7 @@ bool FVoxelForgeComposerStructureRollTest::RunTest(const FString& Parameters)
FVoxelStrateArchetypeParams CandidateParams;
bool bParameterRollsValid = true;
double CandidateDistanceSquared = 0.0;
for (const FBlockSpec& Spec : GBlockSpecs)
{
const FVoxelStrateRollInfo BlockRoll = VF_RollStrateParamsDetailedForArchetype(
@@ -380,6 +395,14 @@ bool FVoxelForgeComposerStructureRollTest::RunTest(const FString& Parameters)
*BlockRoll.FailureReason));
continue;
}
TotalBootstrapJitterFields += BlockRoll.BootstrapJitterFieldCount;
for (const FString& FieldName : BlockRoll.BootstrapJitterFieldNames)
{
BootstrapJitterFieldNames.Add(FieldName);
}
const double BlockDistance = VF_DistanceFromStrateCorpusCentroid(
Corpus, BlockRoll.ArchetypeParams, Spec.Archetype);
CandidateDistanceSquared += BlockDistance * BlockDistance;
VF_CopyRollBlock(CandidateParams, BlockRoll, Spec.Archetype);
}
VF_SetAllRuntimeBounds(CandidateParams, TopWorldZ, BottomWorldZ);
@@ -417,9 +440,10 @@ bool FVoxelForgeComposerStructureRollTest::RunTest(const FString& Parameters)
Stack.PrepareChunk(Context);
FStackDensitySampler Sampler(Stack);
FVoxelStrateSampleGrid SampleGrid;
const FVoxelStrateMetrics Metrics = VF_MeasureStrateWithSampler(
Sampler, BottomVoxelZ, TopVoxelZ + 1,
Context.EdgeSealThickness, MeasureSettings);
Context.EdgeSealThickness, MeasureSettings, &SampleGrid);
FVoxelConnectivityDiagnostics Law;
if (ArrivalCount == 1 && DepartureCount == 1)
@@ -455,6 +479,27 @@ bool FVoxelForgeComposerStructureRollTest::RunTest(const FString& Parameters)
FeatureText = FString::Printf(TEXT("%.6f"), Metrics.MedianFeatureScale);
}
FString RejectionReason;
if (!bNonVacuous)
{
RejectionReason = TEXT("vacuous");
}
else
{
if (!bLargestEnough)
{
RejectionReason = TEXT("fragmented");
}
if (!bLawPass)
{
if (!RejectionReason.IsEmpty())
{
RejectionReason += TEXT("; ");
}
RejectionReason += TEXT("law failed");
}
}
const FBoxVerdictReport BoxReport = VF_CheckStackBoxVerdicts(
Stack, Context, CandidateIndex, BottomVoxelZ, TopVoxelZ);
TotalBoxMixed += BoxReport.Mixed;
@@ -468,6 +513,50 @@ bool FVoxelForgeComposerStructureRollTest::RunTest(const FString& Parameters)
FirstBoxViolation = BoxReport.FirstViolation;
}
const double CandidateDistance = FMath::Sqrt(
FMath::Max(0.0, CandidateDistanceSquared));
const FVoxelStratePreviewWindow CandidateWindow =
VF_GetStratePreviewWindow(SampleGrid);
if (CandidateWindow.IsValid())
{
if (!bPreviewWindowSet)
{
PreviewWindow = CandidateWindow;
bPreviewWindowSet = true;
}
}
TestTrue(FString::Printf(TEXT("candidate %d preview captures its measurement grid"),
CandidateIndex),
!Metrics.bValid || SampleGrid.IsValid());
FVoxelStratePreviewCandidate PreviewCandidate;
FString PreviewError;
const bool bPreviewWritten = VF_WriteStratePreviewCandidate(
PreviewDirectory,
CandidateIndex,
VF_FormatStrateStructureRecipe(Recipe),
SampleGrid,
Metrics,
MeasureSettings.HeadroomCells,
CandidateDistance,
LawText,
!(bNonVacuous && bLargestEnough && bLawPass),
RejectionReason,
PreviewCandidate,
PreviewError);
TestTrue(FString::Printf(TEXT("candidate %d preview rasterises without a second sample"),
CandidateIndex),
bPreviewWritten);
TestTrue(FString::Printf(TEXT("candidate %d has images when its measurement is valid"),
CandidateIndex),
!Metrics.bValid || PreviewCandidate.bRendered);
if (!bPreviewWritten)
{
AddError(FString::Printf(TEXT("candidate %d preview failed: %s"),
CandidateIndex, *PreviewError));
}
PreviewCandidates.Add(MoveTemp(PreviewCandidate));
// Same recipe + same block vectors + same seed must yield the same world values. This is
// checked on a fixed point array for every candidate, including cache-backed sources.
FVoxelOpStack RepeatStack;
@@ -548,6 +637,51 @@ bool FVoxelForgeComposerStructureRollTest::RunTest(const FString& Parameters)
TotalBoxMixed, TotalBoxAllSolid, TotalBoxAllAir, TotalBoxProved,
TotalBoxCheckedVoxels, TotalBoxViolations,
FirstBoxViolation.IsEmpty() ? TEXT("No violations.") : *FirstBoxViolation));
FString PreviewIndexPath;
FString PreviewError;
const bool bPreviewIndexWritten = bPreviewWindowSet
&& PreviewCandidates.Num() == NumCandidates
&& VF_WriteStratePreviewIndex(
PreviewDirectory,
FString::Printf(TEXT("VoxelForge composer structure preview — seed %d"),
AuthoredSettings->Seed),
PreviewWindow,
PreviewCandidates,
PreviewIndexPath,
PreviewError);
TestTrue(TEXT("64-candidate composer preview index is written"), bPreviewIndexWritten);
if (!bPreviewIndexWritten)
{
AddError(FString::Printf(TEXT("composer preview index failed: %s"), *PreviewError));
}
if (bPreviewWindowSet && PreviewCandidates.Num() > 0)
{
const FVoxelStratePreviewCandidate& FirstPreview = PreviewCandidates[0];
int32 NumRenderedPairs = 0;
for (const FVoxelStratePreviewCandidate& Candidate : PreviewCandidates)
{
if (Candidate.bRendered)
{
++NumRenderedPairs;
}
}
AddInfo(FString::Printf(
TEXT("Composer preview: %d candidates, %d rendered pairs, vertical=%dx%d, plan=%dx%d, "
"plan Z chosen from each candidate's measured layer with the highest solid/air "
"boundary count (first candidate Z=%d); index=%s; first-candidate window=%s; "
"image cap=512 px including 20 px scale footer."),
PreviewCandidates.Num(),
NumRenderedPairs,
FirstPreview.VerticalImageWidth, FirstPreview.VerticalImageHeight,
FirstPreview.PlanImageWidth, FirstPreview.PlanImageHeight,
FirstPreview.PlanSliceWorldZ,
*PreviewIndexPath,
*PreviewWindow.Describe()));
}
AddInfo(FString::Printf(
TEXT("Bootstrap jitter: %d field applications across the six structure blocks; %d "
"distinct archetype fields took the bootstrap path."),
TotalBootstrapJitterFields, BootstrapJitterFieldNames.Num()));
TestEqual(TEXT("exactly 64 structure candidates were requested"), NumCandidates, 64);
TestEqual(TEXT("invalid recipes emitted by the roller/build path"), NumInvalidRecipes, 0);
@@ -22,6 +22,9 @@
namespace
{
constexpr float GJitterFraction = 0.15f;
constexpr float GBootstrapJitterFraction = 0.25f;
constexpr double GNearZeroSpread = 1.0e-6;
constexpr double GBootstrapMagnitudeFloor = 1.0;
static const ECaveGeneratorType GAllArchetypes[] =
{
@@ -861,10 +864,41 @@ namespace
}
}
void VF_JitterMeasuredField(double& Value,
const FVoxelStrateFieldSpread& Spread,
ECaveGeneratorType Archetype,
FRandomStream& Rng,
int32& OutBootstrapFieldCount,
TArray<FString>& OutBootstrapFieldNames)
{
const double CorpusRange = FMath::Max(0.0, Spread.Max - Spread.Min);
const bool bUseBootstrap = CorpusRange <= GNearZeroSpread;
// Spread-derived jitter is the real rule. This bootstrap is only for a starved corpus:
// when season zero has supplied no useful measured range, use a fraction of this field's
// own magnitude, with a one-native-unit floor near zero. Promotion (Tier 5) should retire
// this fallback as the corpus grows.
const double JitterScale = bUseBootstrap
? FMath::Max(FMath::Abs(Value), GBootstrapMagnitudeFloor)
: CorpusRange;
const float JitterFraction = bUseBootstrap
? GBootstrapJitterFraction : GJitterFraction;
Value += static_cast<double>((Rng.FRand() * 2.0f - 1.0f) * JitterFraction)
* JitterScale;
if (bUseBootstrap)
{
++OutBootstrapFieldCount;
OutBootstrapFieldNames.AddUnique(VF_SpreadKey(Archetype, Spread.FieldName));
}
}
void VF_JitterNativeParams(FVoxelStrateArchetypeParams& Params,
ECaveGeneratorType Archetype,
const FVoxelStrateCorpus& Corpus,
FRandomStream& Rng)
FRandomStream& Rng,
int32& OutBootstrapFieldCount,
TArray<FString>& OutBootstrapFieldNames)
{
void* Memory = VF_GetParamMemory(Params, Archetype);
UStruct* Struct = VF_GetParamStruct(Archetype);
@@ -889,17 +923,11 @@ namespace
continue;
}
const double CorpusRange = FMath::Max(0.0, Spread.Max - Spread.Min);
if (!(CorpusRange > 0.0))
{
continue;
}
double Value = 0.0;
if (VF_ReadNamedField(Memory, Archetype, Spread.FieldName, Value))
{
Value += static_cast<double>((Rng.FRand() * 2.0f - 1.0f) * GJitterFraction)
* CorpusRange;
VF_JitterMeasuredField(Value, Spread, Archetype, Rng,
OutBootstrapFieldCount, OutBootstrapFieldNames);
VF_WriteNamedField(Memory, Archetype, Spread.FieldName, Value);
}
}
@@ -920,17 +948,11 @@ namespace
continue;
}
const double CorpusRange = FMath::Max(0.0, Spread->Max - Spread->Min);
if (!(CorpusRange > 0.0))
{
continue;
}
double Value = 0.0;
if (VF_ReadPropertyValue(Pair.Value, Memory, Value))
{
Value += static_cast<double>((Rng.FRand() * 2.0f - 1.0f) * GJitterFraction)
* CorpusRange;
VF_JitterMeasuredField(Value, *Spread, Archetype, Rng,
OutBootstrapFieldCount, OutBootstrapFieldNames);
VF_WritePropertyValue(Pair.Value, Memory, Value);
}
}
@@ -1247,7 +1269,9 @@ namespace
VF_CopyDominantBooleans(Result.ArchetypeParams,
Entries[DominantParent].ArchetypeParams,
Result.Archetype);
VF_JitterNativeParams(Result.ArchetypeParams, Result.Archetype, Corpus, Rng);
VF_JitterNativeParams(Result.ArchetypeParams, Result.Archetype, Corpus, Rng,
Result.BootstrapJitterFieldCount,
Result.BootstrapJitterFieldNames);
VF_ClampNativeParams(Result.ArchetypeParams, Result.Archetype, Corpus);
VF_RepairNativeOrderedPairs(Result.ArchetypeParams, Result.Archetype);
VF_ResetExcludedFields(Result.ArchetypeParams, Result.Archetype);
@@ -2089,6 +2113,47 @@ bool VF_AreStrateArchetypeParamsBitIdentical(
&& FMemory::Memcmp(&ABottom, &BBottom, sizeof(ABottom)) == 0;
}
double VF_DistanceFromStrateCorpusCentroid(
const FVoxelStrateCorpus& Corpus,
const FVoxelStrateArchetypeParams& Params,
ECaveGeneratorType Archetype)
{
const void* Memory = VF_GetParamMemory(Params, Archetype);
if (Memory == nullptr)
{
return 0.0;
}
double DistanceSquared = 0.0;
for (const FVoxelStrateFieldSpread& Spread : Corpus.GetFieldSpreads())
{
if (Spread.Archetype != Archetype || Spread.bExcluded
|| Spread.Kind == EVoxelStrateFieldKind::Boolean
|| Spread.Kind == EVoxelStrateFieldKind::Enum)
{
continue;
}
double Value = 0.0;
if (!VF_ReadNamedField(Memory, Archetype, Spread.FieldName, Value))
{
continue;
}
const double CorpusRange = FMath::Max(0.0, Spread.Max - Spread.Min);
const double Scale = CorpusRange > GNearZeroSpread
? CorpusRange
: FMath::Max(FMath::Abs(Spread.Mean) * GBootstrapJitterFraction,
GBootstrapMagnitudeFloor);
if (FMath::IsFinite(Value) && FMath::IsFinite(Spread.Mean) && Scale > 0.0)
{
const double Normalized = (Value - Spread.Mean) / Scale;
DistanceSquared += Normalized * Normalized;
}
}
return FMath::Sqrt(FMath::Max(0.0, DistanceSquared));
}
FVoxelStrateRollInfo VF_RollStrateParamsDetailed(const FVoxelStrateCorpus& Corpus,
int32 Seed, int32 Index)
{
@@ -387,6 +387,29 @@ namespace VoxelStrateMeasurePrivate
return true;
}
void ExportSampleGrid(FSampleGrid& Source, FVoxelStrateSampleGrid& Destination)
{
Destination = FVoxelStrateSampleGrid();
Destination.bValid = true;
Destination.NumX = Source.NumX;
Destination.NumY = Source.NumY;
Destination.NumZ = Source.NumZ;
Destination.SampleStep = Source.SampleStep;
Destination.CellCount = Source.CellCount;
Destination.MinX = Source.MinX;
Destination.MinY = Source.MinY;
Destination.MinZ = Source.MinZ;
Destination.MaxX = Source.MaxX;
Destination.MaxY = Source.MaxY;
Destination.MaxZ = Source.MaxZ;
Destination.ResolvedMarginVoxels = Source.ResolvedMarginVoxels;
Destination.SampledMinZ = Source.SampledMinZ;
Destination.SampledMaxZ = Source.SampledMaxZ;
// Move, rather than copy, the already-built polarity grid. The caller owns this buffer
// only when it explicitly requested a capture.
Destination.Air = MoveTemp(Source.Air);
}
void DecodeIndex(const FSampleGrid& Grid, int32 Index, int32& OutX, int32& OutY, int32& OutZ)
{
const int32 Plane = Grid.NumX * Grid.NumY;
@@ -1257,9 +1280,14 @@ FVoxelStrateMetrics VF_MeasureStrate(
const UVoxelGenerator& Generator,
const UVoxelStrateManager& Manager,
int32 StrateIndex,
const FVoxelStrateMeasureSettings& Settings)
const FVoxelStrateMeasureSettings& Settings,
FVoxelStrateSampleGrid* OutSampleGrid)
{
FVoxelStrateMetrics Result;
if (OutSampleGrid != nullptr)
{
*OutSampleGrid = FVoxelStrateSampleGrid();
}
VoxelStrateMeasurePrivate::FSampleGrid Grid;
if (!VoxelStrateMeasurePrivate::BuildSampleGrid(
&Generator, &Manager, nullptr, StrateIndex, 0, 0, 0.0f, false,
@@ -1284,6 +1312,10 @@ FVoxelStrateMetrics VF_MeasureStrate(
{
Result.RefusalReason = TEXT("The measurement grid was empty.");
}
if (OutSampleGrid != nullptr && Result.bValid)
{
VoxelStrateMeasurePrivate::ExportSampleGrid(Grid, *OutSampleGrid);
}
return Result;
}
@@ -1292,9 +1324,14 @@ FVoxelStrateMetrics VF_MeasureStrateWithSampler(
int32 StrateBottomWorldZ,
int32 StrateTopWorldZ,
float BoundarySealThickness,
const FVoxelStrateMeasureSettings& Settings)
const FVoxelStrateMeasureSettings& Settings,
FVoxelStrateSampleGrid* OutSampleGrid)
{
FVoxelStrateMetrics Result;
if (OutSampleGrid != nullptr)
{
*OutSampleGrid = FVoxelStrateSampleGrid();
}
VoxelStrateMeasurePrivate::FSampleGrid Grid;
if (!VoxelStrateMeasurePrivate::BuildSampleGrid(
nullptr, nullptr, &Sampler, INDEX_NONE, StrateBottomWorldZ, StrateTopWorldZ,
@@ -1319,6 +1356,10 @@ FVoxelStrateMetrics VF_MeasureStrateWithSampler(
{
Result.RefusalReason = TEXT("The measurement grid was empty.");
}
if (OutSampleGrid != nullptr && Result.bValid)
{
VoxelStrateMeasurePrivate::ExportSampleGrid(Grid, *OutSampleGrid);
}
return Result;
}
@@ -0,0 +1,723 @@
// Deterministic editor/automation strate previews.
#include "VoxelStratePreview.h"
FString FVoxelStratePreviewWindow::Describe() const
{
return FString::Printf(
TEXT("interior margin=%d voxels; sample step=%d voxels; Z=[%d,%d) voxels; "
"XY=[%.0f,%.0f) x [%.0f,%.0f) voxels; grid=%dx%dx%d"),
ResolvedMarginVoxels,
SampleStep,
SampledMinZ,
SampledMaxZ,
MinX,
MaxX,
MinY,
MaxY,
NumX,
NumY,
NumZ);
}
FVoxelStratePreviewWindow VF_GetStratePreviewWindow(const FVoxelStrateSampleGrid& Grid)
{
FVoxelStratePreviewWindow Window;
Window.SampleStep = Grid.SampleStep;
Window.ResolvedMarginVoxels = Grid.ResolvedMarginVoxels;
Window.SampledMinZ = Grid.SampledMinZ;
Window.SampledMaxZ = Grid.SampledMaxZ;
Window.NumX = Grid.NumX;
Window.NumY = Grid.NumY;
Window.NumZ = Grid.NumZ;
Window.MinX = Grid.MinX;
Window.MaxX = Grid.MaxX;
Window.MinY = Grid.MinY;
Window.MaxY = Grid.MaxY;
return Window;
}
#if WITH_EDITOR
#include "HAL/FileManager.h"
#include "IImageWrapper.h"
#include "IImageWrapperModule.h"
#include "Misc/FileHelper.h"
#include "Modules/ModuleManager.h"
namespace
{
constexpr int32 GMaxPreviewImageDimension = 512;
constexpr int32 GPreviewScaleBarFooterPixels = 20;
const FColor GSolidColor(18, 23, 30, 255);
const FColor GAirColor(218, 225, 231, 255);
const FColor GWalkableColor(245, 166, 54, 255);
const FColor GScaleBarColor(245, 245, 245, 255);
const FColor GScaleBarBackground(9, 12, 17, 255);
struct FPlanSliceChoice
{
int32 CellZ = INDEX_NONE;
int32 BoundaryTransitions = 0;
int32 MixedCells = 0;
int32 WalkableCells = 0;
int32 SolidCells = 0;
bool bHasMixedLayer = false;
};
FString VF_HtmlEscape(const FString& Source)
{
FString Escaped = Source;
Escaped = Escaped.Replace(TEXT("&"), TEXT("&amp;"));
Escaped = Escaped.Replace(TEXT("<"), TEXT("&lt;"));
Escaped = Escaped.Replace(TEXT(">"), TEXT("&gt;"));
Escaped = Escaped.Replace(TEXT("\""), TEXT("&quot;"));
Escaped = Escaped.Replace(TEXT("'"), TEXT("&#39;"));
return Escaped;
}
FString VF_FormatMetric(bool bValid, float Value)
{
return bValid ? FString::Printf(TEXT("%.6f"), Value) : TEXT("invalid");
}
FString VF_FormatIntegerMetric(bool bValid, int32 Value)
{
return bValid ? FString::Printf(TEXT("%d"), Value) : TEXT("invalid");
}
bool VF_IsWalkableCell(const FVoxelStrateSampleGrid& Grid,
int32 X, int32 Y, int32 Z, int32 HeadroomCells)
{
if (Grid.Air[Grid.Index(X, Y, Z)] == 0u || Z == 0
|| Grid.Air[Grid.Index(X, Y, Z - 1)] != 0u)
{
return false;
}
for (int32 H = 0; H < HeadroomCells; ++H)
{
const int64 HeadroomZ = static_cast<int64>(Z) + H;
if (HeadroomZ >= Grid.NumZ
|| Grid.Air[Grid.Index(X, Y, static_cast<int32>(HeadroomZ))] == 0u)
{
return false;
}
}
return true;
}
int32 VF_SourceBegin(int32 OutputIndex, int32 OutputCount, int32 SourceCount)
{
return static_cast<int32>(
(static_cast<int64>(OutputIndex) * static_cast<int64>(SourceCount))
/ static_cast<int64>(OutputCount));
}
int32 VF_SourceEnd(int32 OutputIndex, int32 OutputCount, int32 SourceCount)
{
const int32 Begin = VF_SourceBegin(OutputIndex, OutputCount, SourceCount);
const int32 End = static_cast<int32>(
(static_cast<int64>(OutputIndex + 1) * static_cast<int64>(SourceCount)
+ static_cast<int64>(OutputCount) - 1)
/ static_cast<int64>(OutputCount));
return FMath::Clamp(FMath::Max(Begin + 1, End), 0, SourceCount);
}
FColor VF_AggregateVerticalPixel(const FVoxelStrateSampleGrid& Grid,
int32 FixedY,
int32 XBegin,
int32 XEnd,
int32 ZBegin,
int32 ZEnd,
int32 HeadroomCells)
{
bool bAnyAir = false;
bool bAnyWalkable = false;
for (int32 Z = ZBegin; Z < ZEnd; ++Z)
{
for (int32 X = XBegin; X < XEnd; ++X)
{
const int32 Cell = Grid.Index(X, FixedY, Z);
if (Grid.Air[Cell] != 0u)
{
bAnyAir = true;
bAnyWalkable |= VF_IsWalkableCell(Grid, X, FixedY, Z, HeadroomCells);
}
}
}
return bAnyWalkable ? GWalkableColor : bAnyAir ? GAirColor : GSolidColor;
}
FColor VF_AggregatePlanPixel(const FVoxelStrateSampleGrid& Grid,
int32 FixedZ,
int32 XBegin,
int32 XEnd,
int32 YBegin,
int32 YEnd,
int32 HeadroomCells)
{
bool bAnyAir = false;
bool bAnyWalkable = false;
for (int32 Y = YBegin; Y < YEnd; ++Y)
{
for (int32 X = XBegin; X < XEnd; ++X)
{
const int32 Cell = Grid.Index(X, Y, FixedZ);
if (Grid.Air[Cell] != 0u)
{
bAnyAir = true;
bAnyWalkable |= VF_IsWalkableCell(Grid, X, Y, FixedZ, HeadroomCells);
}
}
}
return bAnyWalkable ? GWalkableColor : bAnyAir ? GAirColor : GSolidColor;
}
FPlanSliceChoice VF_ChoosePlanSlice(const FVoxelStrateSampleGrid& Grid,
int32 HeadroomCells)
{
FPlanSliceChoice Best;
const int32 CentreZ = Grid.NumZ / 2;
auto IsBetterMixed = [CentreZ](const FPlanSliceChoice& Candidate,
const FPlanSliceChoice& Existing)
{
if (Candidate.BoundaryTransitions != Existing.BoundaryTransitions)
{
return Candidate.BoundaryTransitions > Existing.BoundaryTransitions;
}
if (Candidate.MixedCells != Existing.MixedCells)
{
return Candidate.MixedCells > Existing.MixedCells;
}
if (Candidate.WalkableCells != Existing.WalkableCells)
{
return Candidate.WalkableCells > Existing.WalkableCells;
}
return FMath::Abs(Candidate.CellZ - CentreZ)
< FMath::Abs(Existing.CellZ - CentreZ);
};
auto IsBetterFallback = [CentreZ](const FPlanSliceChoice& Candidate,
const FPlanSliceChoice& Existing)
{
if (Candidate.SolidCells != Existing.SolidCells)
{
return Candidate.SolidCells > Existing.SolidCells;
}
if (Candidate.MixedCells != Existing.MixedCells)
{
return Candidate.MixedCells > Existing.MixedCells;
}
return FMath::Abs(Candidate.CellZ - CentreZ)
< FMath::Abs(Existing.CellZ - CentreZ);
};
for (int32 Z = 0; Z < Grid.NumZ; ++Z)
{
FPlanSliceChoice Candidate;
Candidate.CellZ = Z;
for (int32 Y = 0; Y < Grid.NumY; ++Y)
{
for (int32 X = 0; X < Grid.NumX; ++X)
{
const bool bAir = Grid.Air[Grid.Index(X, Y, Z)] != 0u;
if (bAir)
{
++Candidate.WalkableCells;
if (!VF_IsWalkableCell(Grid, X, Y, Z, HeadroomCells))
{
--Candidate.WalkableCells;
}
}
else
{
++Candidate.SolidCells;
}
if (X + 1 < Grid.NumX
&& bAir != (Grid.Air[Grid.Index(X + 1, Y, Z)] != 0u))
{
++Candidate.BoundaryTransitions;
}
if (Y + 1 < Grid.NumY
&& bAir != (Grid.Air[Grid.Index(X, Y + 1, Z)] != 0u))
{
++Candidate.BoundaryTransitions;
}
}
}
Candidate.MixedCells = FMath::Min(
Candidate.SolidCells,
Grid.NumX * Grid.NumY - Candidate.SolidCells);
Candidate.bHasMixedLayer = Candidate.SolidCells > 0
&& Candidate.SolidCells < Grid.NumX * Grid.NumY;
if (Candidate.bHasMixedLayer)
{
if (!Best.bHasMixedLayer || IsBetterMixed(Candidate, Best))
{
Best = Candidate;
}
}
else if (!Best.bHasMixedLayer
&& (Best.CellZ == INDEX_NONE || IsBetterFallback(Candidate, Best)))
{
Best = Candidate;
}
}
return Best;
}
int32 VF_CellWorldCoordinate(float Min, float Max, int32 SampleStep, int32 Cell)
{
const float Extent = Max - Min;
const float Offset = FMath::Min(
static_cast<float>(Cell) * static_cast<float>(SampleStep)
+ 0.5f * static_cast<float>(SampleStep),
Extent - 0.5f);
return FMath::RoundToInt(Min + Offset);
}
void VF_DrawScaleBar(TArray<FColor>& Pixels, int32 Width, int32 ContentHeight,
int32 SourceWidth, int32 SampleStep)
{
if (Width <= 0 || ContentHeight < 0 || Pixels.Num() <= 0 || SourceWidth <= 0
|| SampleStep <= 0)
{
return;
}
const int32 ImageHeight = Pixels.Num() / Width;
const int32 BarY = FMath::Clamp(ContentHeight + 8, 0, ImageHeight - 1);
const int32 BarHeight = FMath::Min(4, ImageHeight - BarY);
const int32 BarX = FMath::Min(8, Width - 1);
const double ChunkPixels = static_cast<double>(Width) * 32.0
/ (static_cast<double>(SourceWidth) * static_cast<double>(SampleStep));
const int32 BarWidth = FMath::Clamp(
FMath::RoundToInt(static_cast<float>(ChunkPixels)), 1, FMath::Max(1, Width - BarX));
for (int32 Y = 0; Y < ImageHeight; ++Y)
{
for (int32 X = 0; X < Width; ++X)
{
if (Y >= ContentHeight)
{
Pixels[Y * Width + X] = GScaleBarBackground;
}
}
}
for (int32 Y = BarY; Y < BarY + BarHeight; ++Y)
{
for (int32 X = BarX; X < BarX + BarWidth && X < Width; ++X)
{
Pixels[Y * Width + X] = GScaleBarColor;
}
}
}
bool VF_RenderVertical(const FVoxelStrateSampleGrid& Grid, int32 HeadroomCells,
int32 FixedY, TArray<FColor>& OutPixels,
int32& OutWidth, int32& OutHeight)
{
const int32 MaxContentDimension = GMaxPreviewImageDimension
- GPreviewScaleBarFooterPixels;
OutWidth = FMath::Min(Grid.NumX, MaxContentDimension);
const int32 ContentHeight = FMath::Min(Grid.NumZ, MaxContentDimension);
OutHeight = ContentHeight + GPreviewScaleBarFooterPixels;
OutPixels.SetNumUninitialized(OutWidth * OutHeight);
for (int32 PixelZ = 0; PixelZ < ContentHeight; ++PixelZ)
{
// Put the highest sampled cells at the top of the image, as a person reading an XZ
// section expects; the source grid itself remains in its deterministic Z order.
const int32 ImageZ = ContentHeight - 1 - PixelZ;
const int32 ZBegin = VF_SourceBegin(ImageZ, ContentHeight, Grid.NumZ);
const int32 ZEnd = VF_SourceEnd(ImageZ, ContentHeight, Grid.NumZ);
for (int32 PixelX = 0; PixelX < OutWidth; ++PixelX)
{
const int32 XBegin = VF_SourceBegin(PixelX, OutWidth, Grid.NumX);
const int32 XEnd = VF_SourceEnd(PixelX, OutWidth, Grid.NumX);
OutPixels[PixelZ * OutWidth + PixelX] = VF_AggregateVerticalPixel(
Grid, FixedY, XBegin, XEnd, ZBegin, ZEnd, HeadroomCells);
}
}
VF_DrawScaleBar(OutPixels, OutWidth, ContentHeight, Grid.NumX, Grid.SampleStep);
return OutWidth > 0 && OutHeight > 0;
}
bool VF_RenderPlan(const FVoxelStrateSampleGrid& Grid, int32 HeadroomCells,
int32 FixedZ, TArray<FColor>& OutPixels,
int32& OutWidth, int32& OutHeight)
{
const int32 MaxContentDimension = GMaxPreviewImageDimension
- GPreviewScaleBarFooterPixels;
OutWidth = FMath::Min(Grid.NumX, MaxContentDimension);
const int32 ContentHeight = FMath::Min(Grid.NumY, MaxContentDimension);
OutHeight = ContentHeight + GPreviewScaleBarFooterPixels;
OutPixels.SetNumUninitialized(OutWidth * OutHeight);
for (int32 PixelY = 0; PixelY < ContentHeight; ++PixelY)
{
const int32 YBegin = VF_SourceBegin(PixelY, ContentHeight, Grid.NumY);
const int32 YEnd = VF_SourceEnd(PixelY, ContentHeight, Grid.NumY);
for (int32 PixelX = 0; PixelX < OutWidth; ++PixelX)
{
const int32 XBegin = VF_SourceBegin(PixelX, OutWidth, Grid.NumX);
const int32 XEnd = VF_SourceEnd(PixelX, OutWidth, Grid.NumX);
OutPixels[PixelY * OutWidth + PixelX] = VF_AggregatePlanPixel(
Grid, FixedZ, XBegin, XEnd, YBegin, YEnd, HeadroomCells);
}
}
VF_DrawScaleBar(OutPixels, OutWidth, ContentHeight, Grid.NumX, Grid.SampleStep);
return OutWidth > 0 && OutHeight > 0;
}
bool VF_WritePng(const FString& Path, const TArray<FColor>& Pixels,
int32 Width, int32 Height, FString& OutError)
{
IImageWrapperModule& Module = FModuleManager::LoadModuleChecked<IImageWrapperModule>(
FName("ImageWrapper"));
const TSharedPtr<IImageWrapper> Wrapper = Module.CreateImageWrapper(EImageFormat::PNG);
if (!Wrapper.IsValid()
|| !Wrapper->SetRaw(Pixels.GetData(),
static_cast<int64>(Pixels.Num()) * sizeof(FColor),
Width, Height, ERGBFormat::BGRA, 8))
{
OutError = FString::Printf(TEXT("failed to encode PNG %s"), *Path);
return false;
}
const TArray64<uint8>& Png = Wrapper->GetCompressed(100);
if (!FFileHelper::SaveArrayToFile(Png, *Path))
{
OutError = FString::Printf(TEXT("failed to write PNG %s"), *Path);
return false;
}
return true;
}
}
bool VF_WriteStratePreviewCandidate(
const FString& OutputDirectory,
int32 CandidateIndex,
const FString& RecipeString,
const FVoxelStrateSampleGrid& Grid,
const FVoxelStrateMetrics& Metrics,
int32 HeadroomCells,
double DistanceFromCorpusCentroid,
const FString& ArrivalDepartureVerdict,
bool bRejected,
const FString& RejectionReason,
FVoxelStratePreviewCandidate& OutCandidate,
FString& OutError)
{
OutCandidate = FVoxelStratePreviewCandidate();
OutCandidate.CandidateIndex = CandidateIndex;
OutCandidate.RecipeString = RecipeString;
OutCandidate.ArrivalDepartureVerdict = ArrivalDepartureVerdict;
OutCandidate.RejectionReason = RejectionReason;
OutCandidate.bRejected = bRejected;
OutCandidate.bMetricsValid = Metrics.bValid;
OutCandidate.AirFraction = Metrics.AirFraction;
OutCandidate.LargestComponentShare = Metrics.LargestComponentShare;
OutCandidate.WalkableFraction = Metrics.WalkableFraction;
OutCandidate.MedianFeatureScale = Metrics.MedianFeatureScale;
OutCandidate.MedianVerticalClearance = Metrics.MedianVerticalClearance;
OutCandidate.DistanceFromCorpusCentroid = FMath::IsFinite(DistanceFromCorpusCentroid)
? DistanceFromCorpusCentroid : 0.0;
if (!Grid.IsValid())
{
// An invalid recipe/measurement still gets a visible row in the contact sheet; there is
// simply no honest grid from which to manufacture images.
OutCandidate.RenderFailureReason = Metrics.RefusalReason.IsEmpty()
? TEXT("no sampled grid was available") : Metrics.RefusalReason;
return true;
}
OutCandidate.Window = VF_GetStratePreviewWindow(Grid);
if (HeadroomCells < 0)
{
OutError = TEXT("preview HeadroomCells cannot be negative");
return false;
}
const FPlanSliceChoice Plan = VF_ChoosePlanSlice(Grid, HeadroomCells);
const float CentreY = 0.5f * (Grid.MinY + Grid.MaxY);
const float Step = static_cast<float>(Grid.SampleStep);
const int32 FixedY = FMath::Clamp(
FMath::FloorToInt((CentreY - Grid.MinY) / Step), 0, Grid.NumY - 1);
TArray<FColor> VerticalPixels;
int32 VerticalWidth = 0;
int32 VerticalHeight = 0;
if (!VF_RenderVertical(Grid, HeadroomCells, FixedY, VerticalPixels,
VerticalWidth, VerticalHeight))
{
OutError = FString::Printf(TEXT("failed to rasterise vertical slice for candidate %d"),
CandidateIndex);
return false;
}
TArray<FColor> PlanPixels;
int32 PlanWidth = 0;
int32 PlanHeight = 0;
if (!VF_RenderPlan(Grid, HeadroomCells, Plan.CellZ, PlanPixels, PlanWidth, PlanHeight))
{
OutError = FString::Printf(TEXT("failed to rasterise plan slice for candidate %d"),
CandidateIndex);
return false;
}
if (!IFileManager::Get().MakeDirectory(*OutputDirectory, true))
{
OutError = FString::Printf(TEXT("failed to create preview directory %s"),
*OutputDirectory);
return false;
}
const FString Stem = FString::Printf(TEXT("candidate_%03d"), CandidateIndex);
OutCandidate.VerticalImageFile = Stem + TEXT("_vertical.png");
OutCandidate.PlanImageFile = Stem + TEXT("_plan.png");
const FString VerticalPath = OutputDirectory / OutCandidate.VerticalImageFile;
const FString PlanPath = OutputDirectory / OutCandidate.PlanImageFile;
if (!VF_WritePng(VerticalPath, VerticalPixels, VerticalWidth, VerticalHeight, OutError)
|| !VF_WritePng(PlanPath, PlanPixels, PlanWidth, PlanHeight, OutError))
{
return false;
}
OutCandidate.bRendered = true;
OutCandidate.VerticalSliceCellY = FixedY;
OutCandidate.VerticalSliceWorldY = VF_CellWorldCoordinate(
Grid.MinY, Grid.MaxY, Grid.SampleStep, FixedY);
OutCandidate.VerticalImageWidth = VerticalWidth;
OutCandidate.VerticalImageHeight = VerticalHeight;
OutCandidate.PlanSliceCellZ = Plan.CellZ;
OutCandidate.PlanSliceWorldZ = VF_CellWorldCoordinate(
Grid.MinZ, Grid.MaxZ, Grid.SampleStep, Plan.CellZ);
OutCandidate.PlanImageWidth = PlanWidth;
OutCandidate.PlanImageHeight = PlanHeight;
OutCandidate.PlanSliceBoundaryTransitions = Plan.BoundaryTransitions;
OutCandidate.PlanSliceMixedCells = Plan.MixedCells;
return true;
}
bool VF_WriteStratePreviewIndex(
const FString& OutputDirectory,
const FString& RunTitle,
const FVoxelStratePreviewWindow& Window,
const TArray<FVoxelStratePreviewCandidate>& Candidates,
FString& OutIndexPath,
FString& OutError)
{
if (!Window.IsValid())
{
OutError = TEXT("cannot write preview index for an invalid measurement window");
return false;
}
if (!IFileManager::Get().MakeDirectory(*OutputDirectory, true))
{
OutError = FString::Printf(TEXT("failed to create preview directory %s"),
*OutputDirectory);
return false;
}
TArray<FVoxelStratePreviewCandidate> SortedCandidates = Candidates;
SortedCandidates.Sort([](const FVoxelStratePreviewCandidate& A,
const FVoxelStratePreviewCandidate& B)
{
if (A.bRejected != B.bRejected)
{
return !A.bRejected;
}
if (A.DistanceFromCorpusCentroid != B.DistanceFromCorpusCentroid)
{
// Outliers should be visible near the top of each group.
return A.DistanceFromCorpusCentroid > B.DistanceFromCorpusCentroid;
}
return A.CandidateIndex < B.CandidateIndex;
});
const FString WindowDescription = Window.Describe();
bool bAllWindowsSame = true;
for (const FVoxelStratePreviewCandidate& Candidate : SortedCandidates)
{
if (Candidate.Window.IsValid()
&& Candidate.Window.Describe() != WindowDescription)
{
bAllWindowsSame = false;
break;
}
}
const FString WindowPreamble = bAllWindowsSame
? FString::Printf(TEXT("<strong>Measurement window for every metric below:</strong> %s."),
*VF_HtmlEscape(WindowDescription))
: TEXT("<strong>Measurement windows:</strong> candidates resolve their own exact window "
"from the same measurement settings; each card repeats its window.");
FString Html;
Html.Reserve(256 * FMath::Max(1, SortedCandidates.Num()));
Html += TEXT("<!doctype html>\n<html lang=\"en\"><head><meta charset=\"utf-8\">\n");
Html += FString::Printf(TEXT("<title>%s</title>\n"), *VF_HtmlEscape(RunTitle));
Html += TEXT(
"<style>\n"
":root{color-scheme:dark;background:#0d1117;color:#e6edf3;font:14px/1.4 system-ui,sans-serif}\n"
"body{margin:24px;max-width:1800px}\n"
"h1{margin:0 0 8px;font-size:24px}.note{color:#aab4c0;max-width:1100px}\n"
".legend{display:flex;gap:16px;flex-wrap:wrap;margin:16px 0}.swatch{display:inline-block;width:14px;height:14px;margin-right:5px;vertical-align:-2px;border:1px solid #68717c}\n"
".grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(390px,1fr));gap:16px}\n"
".candidate{background:#161b22;border:1px solid #30363d;border-radius:8px;padding:14px;min-width:0}\n"
".candidate.rejected{border-color:#8b3b42;background:#21171a}.candidate h2{margin:0 0 6px;font-size:18px}\n"
".badge{border-radius:999px;padding:2px 7px;font-size:11px;font-weight:700;letter-spacing:.04em}.survivor{background:#1f6f43;color:#d7ffe5}.rejected-badge{background:#9b3440;color:#fff0f1}\n"
".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}.images{display:grid;grid-template-columns:1fr 1fr;gap:8px}.images figure{margin:0}.images img{display:block;width:100%;height:auto;background:#090c11;image-rendering:pixelated;border:1px solid #30363d}.images figcaption{font-size:11px;color:#aab4c0;margin-top:3px}\n"
".no-image{min-height:40px;border:1px dashed #8b3b42;color:#ffb4b8;padding:12px;font-size:12px}\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));
Html += FString::Printf(
TEXT("<p class=\"note\">%s "
"Density polarity is explicit: density &gt; 0 is air. One chunk = 32 voxels = 8 m "
"(at this sample step, one chunk spans %d sampled cells). The image footer contains "
"a one-chunk scale bar.</p>\n"),
*WindowPreamble,
32 / Window.SampleStep);
Html += TEXT(
"<p class=\"note\"><strong>Connectivity caveat:</strong> these are only 2D slices from the "
"measurement grid. Two regions that look joined in one slice may still be disconnected "
"in 3D. The arrival→departure verdict is the separate coarse-route/full-resolution "
"connectivity check, not something the pictures prove.</p>\n"
"<div class=\"legend\"><span><i class=\"swatch\" style=\"background:#12171e\"></i>solid</span>"
"<span><i class=\"swatch\" style=\"background:#dae1e7\"></i>air</span>"
"<span><i class=\"swatch\" style=\"background:#f5a636\"></i>walkable cell</span></div>\n"
"<p class=\"note\">Ordering: survivors first, then each group by descending distance from "
"the corpus centroid so the strangest candidates appear early. The preview raster cap is "
"512 px per image dimension including a 20 px scale-bar footer; if a source grid is larger, "
"pixels aggregate its already-sampled cells and never resample the world.</p>\n"
"<p class=\"note\">The corpus-centroid distance is a normalized roll-space ordering aid, "
"not a sampled-world measurement. Every world metric and slice choice uses the exact "
"measurement window printed on its card.</p>\n"
"<div class=\"grid\">\n");
for (const FVoxelStratePreviewCandidate& Candidate : SortedCandidates)
{
const FString Status = Candidate.bRejected ? TEXT("REJECTED") : TEXT("SURVIVOR");
const FString StatusClass = Candidate.bRejected ? TEXT("rejected-badge") : TEXT("survivor");
const FString CardClass = Candidate.bRejected ? TEXT("candidate rejected") : TEXT("candidate");
const FString MetricWindow = Candidate.Window.IsValid()
? Candidate.Window.Describe() : WindowDescription;
Html += FString::Printf(
TEXT("<section class=\"%s\"><h2>#%d <span class=\"badge %s\">%s</span></h2>\n"),
*CardClass, Candidate.CandidateIndex, *StatusClass, *Status);
Html += FString::Printf(TEXT("<div class=\"recipe\"><code>%s</code></div>\n"),
*VF_HtmlEscape(Candidate.RecipeString));
if (Candidate.bRejected)
{
Html += FString::Printf(TEXT("<p class=\"reason\"><strong>Why rejected:</strong> %s</p>\n"),
*VF_HtmlEscape(Candidate.RejectionReason.IsEmpty()
? TEXT("unspecified") : Candidate.RejectionReason));
}
Html += FString::Printf(TEXT("<p class=\"window\">Numbers and slice choices use: %s</p>\n"),
*VF_HtmlEscape(MetricWindow));
Html += TEXT("<div class=\"images\">\n");
if (Candidate.bRendered)
{
Html += FString::Printf(
TEXT("<figure><img src=\"%s\" alt=\"candidate %d vertical XZ slice\"><figcaption>"
"Vertical XZ @ centre Y=%d voxels</figcaption></figure>\n"
"<figure><img src=\"%s\" alt=\"candidate %d plan XY slice\"><figcaption>"
"Plan XY @ selected Z=%d voxels; %d solid/air boundary transitions</figcaption></figure>\n"),
*VF_HtmlEscape(Candidate.VerticalImageFile), Candidate.CandidateIndex,
Candidate.VerticalSliceWorldY,
*VF_HtmlEscape(Candidate.PlanImageFile), Candidate.CandidateIndex,
Candidate.PlanSliceWorldZ, Candidate.PlanSliceBoundaryTransitions);
}
else
{
Html += FString::Printf(TEXT("<div class=\"no-image\">No image: %s</div>\n"),
*VF_HtmlEscape(Candidate.RenderFailureReason));
}
Html += TEXT("</div>\n<table><caption class=\"window\">Grid measurements use the window above; "
"centroid distance is roll-space</caption>\n");
Html += FString::Printf(
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>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"
"<tr><th>distance from corpus centroid (roll-space)</th><td>%.6f</td></tr>\n</table>\n</section>\n"),
*VF_FormatMetric(Candidate.bMetricsValid, Candidate.AirFraction),
*VF_FormatMetric(Candidate.bMetricsValid, Candidate.LargestComponentShare),
*VF_FormatMetric(Candidate.bMetricsValid, Candidate.WalkableFraction),
*VF_FormatMetric(Candidate.bMetricsValid, Candidate.MedianFeatureScale),
*VF_FormatIntegerMetric(Candidate.bMetricsValid, Candidate.MedianVerticalClearance),
*VF_HtmlEscape(Candidate.ArrivalDepartureVerdict),
Candidate.DistanceFromCorpusCentroid);
}
Html += TEXT("</div>\n<p class=\"footer\">Generated offline by VoxelForge's editor/automation "
"composer preview. No CDN, JavaScript framework, or runtime generation hook is "
"required.</p>\n</body></html>\n");
OutIndexPath = OutputDirectory / TEXT("index.html");
if (!FFileHelper::SaveStringToFile(
Html, *OutIndexPath, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM))
{
OutError = FString::Printf(TEXT("failed to write preview index %s"), *OutIndexPath);
return false;
}
return true;
}
#else
bool VF_WriteStratePreviewCandidate(
const FString& OutputDirectory,
int32 CandidateIndex,
const FString& RecipeString,
const FVoxelStrateSampleGrid& Grid,
const FVoxelStrateMetrics& Metrics,
int32 HeadroomCells,
double DistanceFromCorpusCentroid,
const FString& ArrivalDepartureVerdict,
bool bRejected,
const FString& RejectionReason,
FVoxelStratePreviewCandidate& OutCandidate,
FString& OutError)
{
(void)OutputDirectory;
(void)CandidateIndex;
(void)RecipeString;
(void)Grid;
(void)Metrics;
(void)HeadroomCells;
(void)DistanceFromCorpusCentroid;
(void)ArrivalDepartureVerdict;
(void)bRejected;
(void)RejectionReason;
OutCandidate = FVoxelStratePreviewCandidate();
OutError = TEXT("strate preview rendering is editor-only");
return false;
}
bool VF_WriteStratePreviewIndex(
const FString& OutputDirectory,
const FString& RunTitle,
const FVoxelStratePreviewWindow& Window,
const TArray<FVoxelStratePreviewCandidate>& Candidates,
FString& OutIndexPath,
FString& OutError)
{
(void)OutputDirectory;
(void)RunTitle;
(void)Window;
(void)Candidates;
OutIndexPath.Reset();
OutError = TEXT("strate preview rendering is editor-only");
return false;
}
#endif
+19 -1
View File
@@ -2,7 +2,8 @@
//
// This is deliberately not part of the runtime generation path. The composer is a build-box
// tool/test API: it loads authored vectors, measures their spread, and makes deterministic
// candidates by blending those vectors before applying a small spread-relative jitter.
// candidates by blending those vectors before applying spread-relative (or season-zero bootstrap)
// jitter.
#pragma once
@@ -203,6 +204,11 @@ struct VOXELFORGE_API FVoxelStrateRollInfo
TArray<int32> ParentEntryIndices;
TArray<float> ParentWeights;
int32 DominantParentPosition = INDEX_NONE;
// Provenance for the starved-corpus fallback. Names are keyed as "archetype:field" so a
// report can distinguish field descriptors from the number of times they were applied.
int32 BootstrapJitterFieldCount = 0;
TArray<FString> BootstrapJitterFieldNames;
};
/**
@@ -306,3 +312,15 @@ VOXELFORGE_API bool VF_AreStrateArchetypeParamsBitIdentical(
const FVoxelStrateArchetypeParams& A,
const FVoxelStrateArchetypeParams& B,
ECaveGeneratorType Archetype);
/**
* Normalised distance from the measured centroid of one exact-archetype corpus group.
*
* This is an offline ordering aid for the preview only. Continuous/integer fields are measured
* in their corpus units; zero-spread fields use a conservative scale floor so the value remains
* finite without turning clamps into roll ranges. It never participates in generation.
*/
VOXELFORGE_API double VF_DistanceFromStrateCorpusCentroid(
const FVoxelStrateCorpus& Corpus,
const FVoxelStrateArchetypeParams& Params,
ECaveGeneratorType Archetype);
+53 -2
View File
@@ -44,6 +44,55 @@ struct VOXELFORGE_API FVoxelStrateMeasureSettings
int32 InteriorMarginVoxels = -1; // <0 = derive from BoundarySealThickness (2x, clamped); explicit 0 includes the seal.
};
/**
* Optional capture of the exact coarse grid used by one measurement.
*
* Air is deliberately stored as a polarity bit: 1 means density > 0 (air), 0 means solid.
* The capture is opt-in so ordinary metrics callers do not retain an 8-million-cell buffer.
* It is an editor/automation hand-off for tools such as the composer preview, not a runtime
* generation cache.
*/
struct VOXELFORGE_API FVoxelStrateSampleGrid
{
bool bValid = false;
int32 NumX = 0;
int32 NumY = 0;
int32 NumZ = 0;
int32 SampleStep = 1;
int32 CellCount = 0;
float MinX = 0.0f;
float MinY = 0.0f;
float MinZ = 0.0f;
float MaxX = 0.0f;
float MaxY = 0.0f;
float MaxZ = 0.0f;
int32 ResolvedMarginVoxels = 0;
int32 SampledMinZ = 0;
int32 SampledMaxZ = 0;
// 1 = air (density > 0), 0 = solid (density <= 0), matching the measurement polarity.
TArray<uint8> Air;
FORCEINLINE int32 Index(int32 X, int32 Y, int32 Z) const
{
return static_cast<int32>(
static_cast<int64>(X)
+ static_cast<int64>(NumX) * (static_cast<int64>(Y)
+ static_cast<int64>(NumY) * static_cast<int64>(Z)));
}
bool IsValid() const
{
return bValid && NumX > 0 && NumY > 0 && NumZ > 0
&& static_cast<int64>(CellCount)
== static_cast<int64>(NumX) * static_cast<int64>(NumY)
* static_cast<int64>(NumZ)
&& Air.Num() == CellCount;
}
};
/** Metrics derived from one coarse grid and its single air flood fill. */
struct VOXELFORGE_API FVoxelStrateMetrics
{
@@ -128,7 +177,8 @@ VOXELFORGE_API FVoxelStrateMetrics VF_MeasureStrate(
const UVoxelGenerator& Generator,
const UVoxelStrateManager& Manager,
int32 StrateIndex,
const FVoxelStrateMeasureSettings& Settings);
const FVoxelStrateMeasureSettings& Settings,
FVoxelStrateSampleGrid* OutSampleGrid = nullptr);
/** The same Tier 2 pass over an explicit voxel Z window and a custom read-only sampler. */
VOXELFORGE_API FVoxelStrateMetrics VF_MeasureStrateWithSampler(
@@ -136,7 +186,8 @@ VOXELFORGE_API FVoxelStrateMetrics VF_MeasureStrateWithSampler(
int32 StrateBottomWorldZ,
int32 StrateTopWorldZ,
float BoundarySealThickness,
const FVoxelStrateMeasureSettings& Settings);
const FVoxelStrateMeasureSettings& Settings,
FVoxelStrateSampleGrid* OutSampleGrid = nullptr);
/**
* Test coarse connectivity and re-check recovered coarse routes at full resolution.
@@ -0,0 +1,103 @@
// Deterministic editor/automation previews for one measured strate sample grid.
//
// The renderer never calls a density function. It consumes the optional capture emitted by
// VF_MeasureStrate/VF_MeasureStrateWithSampler, so the pictures and the reported metrics describe
// the same sampled world.
#pragma once
#include "CoreMinimal.h"
#include "VoxelStrateMeasure.h"
/** The window facts printed on the contact sheet beside every measured number. */
struct VOXELFORGE_API FVoxelStratePreviewWindow
{
int32 SampleStep = 0;
int32 ResolvedMarginVoxels = 0;
int32 SampledMinZ = 0;
int32 SampledMaxZ = 0;
int32 NumX = 0;
int32 NumY = 0;
int32 NumZ = 0;
float MinX = 0.0f;
float MaxX = 0.0f;
float MinY = 0.0f;
float MaxY = 0.0f;
bool IsValid() const
{
return SampleStep > 0 && NumX > 0 && NumY > 0 && NumZ > 0
&& SampledMaxZ > SampledMinZ;
}
FString Describe() const;
};
/** One lightweight row/card record; sampled cells are rendered immediately and are not retained. */
struct VOXELFORGE_API FVoxelStratePreviewCandidate
{
int32 CandidateIndex = INDEX_NONE;
FVoxelStratePreviewWindow Window;
FString RecipeString;
FString ArrivalDepartureVerdict;
FString RejectionReason;
FString RenderFailureReason;
FString VerticalImageFile;
FString PlanImageFile;
bool bRejected = false;
bool bMetricsValid = false;
bool bRendered = false;
float AirFraction = 0.0f;
float LargestComponentShare = 0.0f;
float WalkableFraction = 0.0f;
float MedianFeatureScale = 0.0f;
int32 MedianVerticalClearance = 0;
double DistanceFromCorpusCentroid = 0.0;
int32 VerticalSliceCellY = INDEX_NONE;
int32 VerticalSliceWorldY = 0;
int32 VerticalImageWidth = 0;
int32 VerticalImageHeight = 0;
int32 PlanSliceCellZ = INDEX_NONE;
int32 PlanSliceWorldZ = 0;
int32 PlanImageWidth = 0;
int32 PlanImageHeight = 0;
int32 PlanSliceBoundaryTransitions = 0;
int32 PlanSliceMixedCells = 0;
};
/** Copy the exact measurement-window metadata needed by the index page. */
VOXELFORGE_API FVoxelStratePreviewWindow VF_GetStratePreviewWindow(
const FVoxelStrateSampleGrid& Grid);
/**
* Render one candidate's XZ and XY views and fill its contact-sheet record.
*
* The source grid is the measurement capture, not a sampler. The renderer is deterministic and
* has no random stream. It returns true for a valid measurement with no grid only when it can
* record a non-rendered candidate (for example an invalid recipe); PNG failures return false.
*/
VOXELFORGE_API bool VF_WriteStratePreviewCandidate(
const FString& OutputDirectory,
int32 CandidateIndex,
const FString& RecipeString,
const FVoxelStrateSampleGrid& Grid,
const FVoxelStrateMetrics& Metrics,
int32 HeadroomCells,
double DistanceFromCorpusCentroid,
const FString& ArrivalDepartureVerdict,
bool bRejected,
const FString& RejectionReason,
FVoxelStratePreviewCandidate& OutCandidate,
FString& OutError);
/** Write one self-contained, offline contact sheet for all candidate records. */
VOXELFORGE_API bool VF_WriteStratePreviewIndex(
const FString& OutputDirectory,
const FString& RunTitle,
const FVoxelStratePreviewWindow& Window,
const TArray<FVoxelStratePreviewCandidate>& Candidates,
FString& OutIndexPath,
FString& OutError);
+8 -1
View File
@@ -91,7 +91,14 @@ tiles.) In a tall multi-strate world most chunks in the desired set are full bed
### A. Tooling first — force multipliers for everything after
**F1 — 2D world-preview editor tool.** ★ my top pick. An editor utility that samples `f_XY` (terrain height, relief M, water mask — later the biome map) over an N×N window into a `UTexture2D`, with the Surface|Macro knobs live. Tuning ReliefStrength/biome layout becomes seconds instead of regen-and-fly. Directly addresses the standing "hard to dial blind" pain (Worm passages, today's Stage 0 knobs). Extend with a top-view passage-path overlay (the data exists in `bDebugDrawPassages`).
**F1 — 2D world-preview editor tool. ✅ BUILT 2026-09-04 (measurement + preview).** ★ my top pick. The
editor/automation pass captures the exact coarse measurement grid, writes deterministic PNG XZ and
XY slices plus a self-contained 64-candidate contact sheet, and labels walkable cells, scale, metric
windows, rejection reasons, and the 2D-connectivity caveat. Output is under
`Saved/ComposerPreview/<runid>/`; no runtime generation path is involved. The original idea remains:
sample `f_XY` (terrain height, relief M, water mask — later the biome map) over an N×N window with
the Surface|Macro knobs live, then extend with a top-view passage-path overlay (the data exists in
`bDebugDrawPassages`).
**F2 — Determinism validator button.** ✅ DONE 2026-07-04 (`AVoxelWorld::ValidateDeterminism`,
Live Edit category). CallInEditor: sample a band of densities from two different chunk-window alignments, diff, report max delta. Turns the scariest invariant (§8.4 — window invariance) into a one-click regression test *before* biome code starts landing.