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:
1 parent
f088c19f88
commit
993512f0bc
11 files changed
+1294
-38
No files matched your search
+14
-3
@@ -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
@@ -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("&"));
|
||||
Escaped = Escaped.Replace(TEXT("<"), TEXT("<"));
|
||||
Escaped = Escaped.Replace(TEXT(">"), TEXT(">"));
|
||||
Escaped = Escaped.Replace(TEXT("\""), TEXT("""));
|
||||
Escaped = Escaped.Replace(TEXT("'"), TEXT("'"));
|
||||
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 > 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
|
||||
@@ -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);
|
||||
@@ -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
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user