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
@@ -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);
|
||||
Reference in new issue
Block a user