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

@@ -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);