feat(preview): fine ROI renders + density contour -- the caves look like caves
Implements Codex's own recommendation from its critique, after it settled
the owner's two observations with evidence:
"hard walls" -> THE RENDERER. rg (room graph) re-rendered at step 1 is
rounded and organic. The hard edges were 4-voxel
sampling drawn nearest-neighbour. Fifth time in this
project that an alarming result was the instrument.
"like a maze" -> REAL AND CORRECT. lt (lattice) stays rectilinear at
step 1 because it IS a maze; 19 of 64 candidates use
that source.
"noisy" -> ALIASING. nr#22 speckle 7 solid / 1 air at step 4 ->
0 / 0 at step 1, broad shapes intact.
Built: an optional fine ROI pass at step 1, radius 64 (~1.8M cells, cap
2M), run only for the 42 coarse survivors, plus a deterministic density=0
CONTOUR rendered beside the filled view.
Both views are kept side by side on purpose. The filled view is honest
about what was sampled and carries the walkability evidence; the contour
is legible. Codex's line, and it is the right principle: anti-aliasing the
coarse image "would only cosmetically blur the evidence and could invent
geometry". Do not replace evidence with beauty.
Fine labels carry their own sample step and resolved window so a fine
image can never be mistaken for a coarse one, and a MaxCells refusal shows
in the page rather than looking like a failed render.
fine sampling 71.1 s · rendering 0.6 s · peak grid 9.34 MB
42 filled pairs, 42 contour pairs, 0 refusals, 424 image links, 0 missing
Codex corrected my spec twice in the critique, both sharp: a binary step-1
raster ALWAYS has one-voxel axis-aligned edges, so only persistent
LARGE-SCALE right angles prove boxy generation (my criterion would have
produced a false "it is boxy" verdict); and persistent speckle is not
necessarily a roughness modifier.
Roughness deliberately untouched -- its call to judge that only after fine
rendering exists, and it stands.
⚠️ KNOWN, next: the fine ROI is centred on (0,0), so a candidate whose
geometry sits elsewhere renders blank (candidate 003 is a flat sheet with
no surface in the ROI). FVoxelStrateMetrics already carries
LargestComponentPoint -- centre the ROI there instead.
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
993512f0bc
commit
90c40c8d5a
7 files changed
+867
-61
No files matched your search
+15
-9
@@ -309,7 +309,7 @@ This is **where terrain shape lives.**
|
||||
|
||||
### 3.6b Strate measurement — `Public/VoxelStrateMeasure.h` + `Private/VoxelStrateMeasure.cpp`
|
||||
Headless, read-only Tier 2 measurement pass. `VF_MeasureStrate` samples one strate between a
|
||||
boundary-seal-derived (or explicitly overridden, including explicit zero) interior margin into a bounded coarse grid.
|
||||
boundary-seal-derived (or explicitly overridden, including explicit zero) interior margin into a bounded grid.
|
||||
When both `CoverPointA` and `CoverPointB` are set, the XY window is their AABB expanded by
|
||||
`CoverMarginVoxels`; otherwise it is the legacy square centered on `CenterXY` with
|
||||
`RadiusInVoxels`.
|
||||
@@ -319,8 +319,11 @@ the inclusive/exclusive voxel Z window used, the exact sampled dimensions/bounds
|
||||
component's deterministic lowest-cell representative point, cell count, and count of components
|
||||
holding at least 1% of the air. The flood fill also retains component cell counts in discovery order,
|
||||
so diagnostics can classify small non-largest components without a second source sample.
|
||||
Callers may optionally receive that exact polarity grid as `FVoxelStrateSampleGrid`; the editor-only
|
||||
composer preview consumes this hand-off rather than sampling the world again.
|
||||
Callers may optionally receive that exact polarity grid and scalar values as `FVoxelStrateSampleGrid`; the
|
||||
editor-only composer preview consumes this hand-off rather than sampling the world again. The filled view uses
|
||||
the captured `Air` polarity, while the paired density=0 contour uses the captured scalar field directly (no blur
|
||||
of the coarse raster). Capture refuses a grid over `MaxCells` before allocating it; ordinary metric callers do
|
||||
not retain either buffer.
|
||||
`VF_AreConnected` recovers a deterministic BFS parent path from the same kind of grid and checks
|
||||
candidate routes at full voxel resolution before returning `Connected`. When a candidate is
|
||||
refuted, its specific coarse cell edge is added to a deterministic array blocklist and BFS is
|
||||
@@ -336,7 +339,7 @@ No UObject state, cache, actor, world, or PIE is required.
|
||||
| Symbol | Role |
|
||||
|--------|------|
|
||||
| `FVoxelStrateMeasureSettings` / `FVoxelStrateMetrics` | Plain settings/result structs for bounded strate sampling and derived measurements; callers may override the interior margin (including zero to sample the seal) or fit the XY window to two points with a margin, and results identify the resolved window/dimensions/Z range and deterministic component representatives/sizes. |
|
||||
| `FVoxelStrateSampleGrid` | Optional one-pass `density > 0` air/solid capture exported by the measurement functions; bounded by `MaxCells`, consumed by editor/automation previews, never retained on ordinary metrics calls. |
|
||||
| `FVoxelStrateSampleGrid` | Optional one-pass `density > 0` `Air` polarity plus exact scalar `Density` capture exported by measurement functions; bounded by `MaxCells`, consumed by editor/automation previews, never retained on ordinary metrics calls. |
|
||||
| `VF_MeasureStrate` | One-grid/one-flood-fill strate metrics; resolves either the legacy centered square or the two-point fitted AABB, and refuses invalid bounds or a grid over `MaxCells`. |
|
||||
| `VF_AreConnected` | Coarse 6-connected BFS plus deterministic blocked-edge retries, with a full-resolution air recheck for every candidate route and explicit endpoint-solid, out-of-window, `NotConnectedAtThisResolution`, `CoarseLiedBudgetExhausted` (unknown), and connected outcomes; diagnostics include endpoint component facts and the retry count from that same grid. |
|
||||
| `VF_DiagnoseConnectivity` | The same query plus each mouth's air-component size/share and exact geometric distance to the nearest cell of the other component; proximity is measured in coarse-cell coordinates and does not claim a route through solid. |
|
||||
@@ -474,10 +477,13 @@ the offline sampler, and brute-forces every uniform box verdict from the novel s
|
||||
|
||||
**`Public/VoxelStratePreview.h` + `Private/VoxelStratePreview.cpp`** — editor/automation-only PNG
|
||||
renderer and self-contained contact sheet. It consumes `FVoxelStrateSampleGrid` from the measurement
|
||||
pass, emits deterministic centre-Y XZ and data-selected XY slices via `IImageWrapper`, caps each
|
||||
image at 512 pixels (including its scale footer), and sorts the 64-card sheet by survivor status then
|
||||
descending corpus-centroid distance. The page repeats each candidate's exact measurement window,
|
||||
marks rejection reasons, explains the 2D-connectivity limitation, and has no runtime generation hook.
|
||||
pass, emits deterministic centre-Y XZ and data-selected XY slices via `IImageWrapper`, and shows filled-cell
|
||||
and scalar density=0 contour views side by side without blurring the sampled raster. Each image is capped at
|
||||
512 pixels (including its scale footer). `FVoxelStrateFinePreviewSettings` is a separate caller-selected
|
||||
fine pass: the composer selects only survivors at step 1, radius 64 (a 128×128 XY ROI), and `MaxCells=2,000,000`;
|
||||
the writer labels every image with its exact ROI and step and records cap refusals in the page. The 64-card
|
||||
sheet remains sorted by survivor status then descending corpus-centroid distance, repeats exact measurement
|
||||
windows, marks rejection reasons, explains the 2D-connectivity limitation, and has no runtime generation hook.
|
||||
|
||||
### 3.9 Player edits — `Public/VoxelDiffLayer.h` + `.cpp`
|
||||
`UVoxelDiffLayer : UObject` (h:77). Stores `FVoxelModification` (h:43: Center/Radius/Strength;
|
||||
@@ -544,7 +550,7 @@ The plugin's first tests (`OPSTACK-PLAN.md` Phase 0.5). Run them from the editor
|
||||
| `VoxelForgeStrateParamCoverageTest.cpp` | `VoxelForge.Determinism.StrateParamBlendCoverage` | **The X-macro guard** (added 2026-08-17). `FStrateGenerationParams::Lerp` blends the hand-written `VF_STRATE_PARAM_FIELDS` list, **not** the struct — so a field added to one and not the other compiles, tests green, and silently takes its **default** inside every Gradient/Interleaved transition band. This expands the X-macro a **third** way (after LERP and SNAP), into a name list, and diffs it against the struct's UObject reflection. Pure shape test: no fixture, no world, instant. `GExemptFieldNames` is **empty** — every reflected field is covered today, and any exemption must be written down as a decision. Stakes rise with the world composer, which intends to invent parameter sets through this same `Lerp` (`COMPOSER-NOTES.md`). |
|
||||
| `VoxelForgeComposerParameterRollTest.cpp` | `VoxelForge.Composer.ParameterRoll` | Asset-Registry corpus audit + complete per-archetype spread/exclusion/clamp table; asserts bit-identical deterministic rerolls and same-archetype parents; measures 64 transient candidate strates with `VF_MeasureStrate` plus the exact unsnapped arrival→departure law; brute-forces every rolled production box verdict. Bootstrap follow-up: **1,422 applications / 168 distinct fields**; **38/64 survival**, feature scale **0→376**, walkable **0.000000→0.286766** (no broadening beyond the prior run); **1,356 Mixed + 608 AllSolid + 596 AllAir = 1,204 proved boxes**, **1,602,524 lattice voxels checked, 0 violations**, 146.250 s total in the final full-namespace run. |
|
||||
| ″ | `VoxelForge.Composer.TerrainDetailLiveness` | Fixed 4,096-point `GetDensityAt` lattice, legacy and operator-stack paths; changes one terrain-detail group at a time with an empty terrain-op pool. Proves 9 live groups / 23 fields and 3 dead groups / 11 fields; all 24 rows match. |
|
||||
| `VoxelForgeComposerStructureRollTest.cpp` | `VoxelForge.Composer.StructureRoll` | Rolls root polarity → legal shape source → polarity-derived conversion → 4–8 declaration-legal modifiers → mandatory structural posts; blends the six native parameter families independently, measures 64 novel stacks, captures the same grid for the deterministic XZ/XY preview, checks exact arrival→departure connectivity, rerolls every recipe/stack for determinism, and brute-forces every uniform box verdict. Focused run: **42/64 survival (65.6%)**, **64 distinct recipes**, **6,848 bootstrap applications / 107 distinct fields**, **659 proved boxes / 877,129 voxels / 0 violations**, **64 rendered pairs + index.html**. |
|
||||
| `VoxelForgeComposerStructureRollTest.cpp` | `VoxelForge.Composer.StructureRoll` | Rolls root polarity → legal shape source → polarity-derived conversion → 4–8 declaration-legal modifiers → mandatory structural posts; blends the six native parameter families independently, measures 64 novel stacks, captures the same grid for the deterministic filled/contour XZ/XY preview, runs a separate step-1 radius-64 ROI pass for the 42 survivors, checks exact arrival→departure connectivity, rerolls every recipe/stack for determinism, and brute-forces every uniform box verdict. Focused run: **42/64 survival (65.6%)**, **64 distinct recipes**, **6,848 bootstrap applications / 107 distinct fields**, **659 proved boxes / 877,129 voxels / 0 violations**, **64 coarse filled + 64 coarse contour pairs**, **42 fine ROI filled + 42 fine contour pairs**, **184.659 s**, **0 refusals**. |
|
||||
| `VoxelForgeLayoutOrderIndependenceTest.cpp` | `VoxelForge.Determinism.LayoutOrderIndependence` | Builds a known transient soft-pointer pool, then rebuilds it in original, reversed, and swapped orders. Requires a non-empty layout and passage set, and compares every slot's definition/Z/height plus passage endpoints, radius, type, control geometry, and bounds bit-for-bit. |
|
||||
| `VoxelForgePassageOpenSpaceTest.cpp` | `VoxelForge.Determinism.PassageLandsInOpenSpace` | Uses the real fixture density path to check every generated inter-strate passage whose destination query answers: a 16-point ring outside the mouth's carve/blend band has at least half its samples in destination air, and the endpoint matches the pure open-point result within the mouth's float envelope. This is a connectivity proxy, not a flood-fill proof. Reports checked passages and false/unanswerable archetypes; fails if it inspects zero passages. |
|
||||
| `VoxelForgeStrateConnectivityTest.cpp` | `VoxelForge.Generation.StrateConnectivity` / `VoxelForge.Generation.StrateConnectivityRefinement` / `VoxelForge.Generation.VerticalShaftSeamFreedom` | Bounded strate metrics with density-polarity and solid-gap controls, deterministic route rechecks, and refinement sweeps. The refinement test fits the measurement AABB to each arrival/departure mouth pair, reports exact before/after seed-6 cell counts, reruns every negative with doubled margin, measures tree orphan candidates/path reachability and roughness-bubble proxies, asserts axis landings, checks source/mirror physical paths, and requires 16/16 effective arrival→departure results across the 16 VerticalShafts seeds. Margin-binding negatives are reported as measurement limits, never as gap findings. The seam test re-evaluates cell-boundary positions after warming distinct neighbouring chunk contexts in both legacy and operator-stack paths. |
|
||||
|
||||
+32
-18
@@ -484,32 +484,46 @@ review a season before publishing.
|
||||
|
||||
### ✅ F1 measurement + preview built (2026-09-04)
|
||||
|
||||
`FVoxelStrateSampleGrid` is an opt-in export of the exact `Air` polarity grid already sampled by
|
||||
`VF_MeasureStrate` / `VF_MeasureStrateWithSampler`. The editor/automation renderer consumes that
|
||||
buffer and never calls a density sampler, so each candidate's images and numbers come from one
|
||||
window and one sample pass. `VoxelForge.Composer.StructureRoll` now writes two PNGs per candidate
|
||||
and one self-contained `index.html` to:
|
||||
`FVoxelStrateSampleGrid` is an opt-in export of both the exact `Air` polarity and the scalar `Density`
|
||||
values already sampled by `VF_MeasureStrate` / `VF_MeasureStrateWithSampler`. The editor/automation
|
||||
renderer consumes that buffer and never calls a density sampler, so each candidate's metrics and images
|
||||
come from one window and one sample pass. The filled-cell view is paired with a marching-squares-style
|
||||
`density = 0` contour from the scalar field. The contour is not an anti-aliased or blurred coarse image:
|
||||
it is a legibility aid alongside the honest sampled-cell evidence.
|
||||
|
||||
`Saved/ComposerPreview/structure_seed_0_corpus_9dbcea85_step_4_radius_256/`
|
||||
The step-4 measurement and 64-card contact sheet are unchanged in scope. Each card now shows the coarse
|
||||
filled and contour XZ/XY pairs. `VoxelForge.Composer.StructureRoll` writes them, plus an optional fine
|
||||
pass, to:
|
||||
|
||||
The sheet contains all **64 candidates / 128 PNGs**, survivors first and then descending normalized
|
||||
distance from the corpus centroid so outliers are near the top. Every card repeats its exact resolved
|
||||
interior margin, sample step, XY bounds, and inclusive/exclusive Z span before listing air, largest
|
||||
component share, walkable fraction, feature scale, clearance, and arrival→departure. Rejected cards
|
||||
are visibly marked and name `vacuous`, `fragmented`, and/or `law failed`.
|
||||
`Saved/ComposerPreview/structure_seed_0_corpus_9dbcea85_step_4_radius_256_fine_step_1_fine_radius_64/`
|
||||
|
||||
The fine pass is explicit and bounded: the current composer selects the 42 coarse survivors, samples
|
||||
step 1 in a radius-64 (128×128 XY) ROI, and refuses before allocation when `MaxCells=2,000,000` would be
|
||||
exceeded. The default is radius 64 because it stays around the observed 1.8M-cell budget while exposing
|
||||
voxel-scale detail; radius 128 is already roughly 7.1–7.5M cells and radius 256 exceeds the 8M coarse
|
||||
cap. The public settings let another editor/automation caller choose step, radius, centre, margin, and
|
||||
cap, or the writer can be called for an explicitly selected list. Fine filenames, captions, and each
|
||||
card's text include the sample step and exact resolved XY/Z window, so they cannot be mistaken for the
|
||||
coarse render. A refused fine render is shown as a refusal with its reason rather than silently omitted.
|
||||
|
||||
The vertical image is an XZ section through the sampled cell nearest the strate-window centre Y. The
|
||||
plan image scans every sampled Z layer, keeps layers containing both air and solid, and chooses the
|
||||
one with the largest number of XY solid/air boundary transitions; ties prefer more mixed cells, more
|
||||
walkable cells, then the layer nearest the vertical centre. If no mixed layer exists it chooses the
|
||||
most solid fallback layer, so a slab does not silently produce a blank mid-height sheet. The focused
|
||||
run's first candidate was **128×48** for vertical and **128×148** for plan; dimensions vary with the
|
||||
resolved Z window. Both images use dark solid, light air, orange walkable cells, and a one-chunk
|
||||
footer scale bar. The page states **one chunk = 32 voxels = 8 m**, the density sign (`> 0` is air),
|
||||
the exact measurement window, the 512-pixel-per-dimension cap (including a 20-pixel footer), and
|
||||
that larger source grids aggregate already-sampled cells rather than resampling. It also carries the
|
||||
required caveat: a 2D apparent join is not a 3D connectivity proof; the separate arrival→departure
|
||||
verdict is the connectivity check.
|
||||
run's first candidate was **128×48** for coarse vertical, **128×130** for fine vertical, and **128×148**
|
||||
for both plan images; dimensions vary with the resolved Z window. Both views use dark solid, light air,
|
||||
orange walkable cells, a turquoise scalar contour, and a one-chunk footer scale bar. The page states
|
||||
**one chunk = 32 voxels = 8 m**, the density sign (`> 0` is air), every exact window, the 512-pixel cap
|
||||
(including a 20-pixel footer), and that larger source grids aggregate already-sampled cells rather than
|
||||
resampling. It also carries the required caveat: a 2D apparent join is not a 3D connectivity proof;
|
||||
the separate arrival→departure verdict is the connectivity check.
|
||||
|
||||
The focused run measured **0.456 s** for the coarse render, **70.271 s** for the 42 fine scalar samples,
|
||||
and **0.573 s** for their filled/contour rendering. Peak fine `Air` + `Density` capture allocation was
|
||||
**9,338,880 bytes** and the peak single RGBA raster was **75,776 bytes**. The full StructureRoll test
|
||||
completed in **184.659 s**, with 64 coarse filled pairs, 64 coarse contour pairs, 42 fine filled pairs,
|
||||
42 fine contour pairs, and 0 refusals.
|
||||
|
||||
### 3.5 Validation in three layers, and how "good" is ever judged
|
||||
|
||||
|
||||
@@ -21,6 +21,9 @@ namespace
|
||||
constexpr int32 BoxChecksPerCandidate = 40;
|
||||
constexpr int32 BoxStep = 1;
|
||||
constexpr int32 BoxCells = 8;
|
||||
constexpr int32 FinePreviewSampleStep = 1;
|
||||
constexpr int32 FinePreviewRadiusVoxels = 64;
|
||||
constexpr int32 FinePreviewMaxCells = 2000000;
|
||||
|
||||
const TCHAR* VF_ConnectivityName(EVoxelConnectivityResult Result)
|
||||
{
|
||||
@@ -308,6 +311,14 @@ bool FVoxelForgeComposerStructureRollTest::RunTest(const FString& Parameters)
|
||||
MeasureSettings.HeadroomCells = 2;
|
||||
MeasureSettings.InteriorMarginVoxels = -1;
|
||||
|
||||
FVoxelStrateFinePreviewSettings FinePreviewSettings;
|
||||
FinePreviewSettings.SampleStep = FinePreviewSampleStep;
|
||||
FinePreviewSettings.RadiusInVoxels = FinePreviewRadiusVoxels;
|
||||
FinePreviewSettings.MaxCells = FinePreviewMaxCells;
|
||||
TestTrue(TEXT("fine preview defaults are a bounded valid ROI"), FinePreviewSettings.IsValid());
|
||||
const FVoxelStrateMeasureSettings FineMeasureSettings =
|
||||
FinePreviewSettings.MakeMeasureSettings(MeasureSettings);
|
||||
|
||||
FString CandidateTable = TEXT(
|
||||
"candidate | recipe (compact) | air | largest share | walkable | feature scale | arrival->departure\n");
|
||||
int32 NumSurvivors = 0;
|
||||
@@ -329,10 +340,22 @@ bool FVoxelForgeComposerStructureRollTest::RunTest(const FString& Parameters)
|
||||
PreviewCandidates.Reserve(NumCandidates);
|
||||
FVoxelStratePreviewWindow PreviewWindow;
|
||||
bool bPreviewWindowSet = false;
|
||||
int32 NumFineRequested = 0;
|
||||
int32 NumFineRendered = 0;
|
||||
int32 NumFineContourRendered = 0;
|
||||
int32 NumFineRefused = 0;
|
||||
int64 TotalFineCells = 0;
|
||||
int64 PeakFineCaptureBytes = 0;
|
||||
int64 PeakFineRasterBytes = 0;
|
||||
double FineMeasureSeconds = 0.0;
|
||||
double FineRenderSeconds = 0.0;
|
||||
double CoarsePreviewSeconds = 0.0;
|
||||
bool bFineCapRefusalChecked = false;
|
||||
const FString PreviewRunId = FString::Printf(
|
||||
TEXT("structure_seed_%d_corpus_%08x_step_%d_radius_%d"),
|
||||
TEXT("structure_seed_%d_corpus_%08x_step_%d_radius_%d_fine_step_%d_fine_radius_%d"),
|
||||
AuthoredSettings->Seed, Corpus.GetContentsHash(),
|
||||
MeasureSettings.SampleStep, MeasureSettings.RadiusInVoxels);
|
||||
MeasureSettings.SampleStep, MeasureSettings.RadiusInVoxels,
|
||||
FinePreviewSettings.SampleStep, FinePreviewSettings.RadiusInVoxels);
|
||||
const FString PreviewDirectory = FPaths::ProjectSavedDir()
|
||||
/ TEXT("ComposerPreview") / PreviewRunId;
|
||||
TArray<int32> ShapeCounts;
|
||||
@@ -440,6 +463,23 @@ bool FVoxelForgeComposerStructureRollTest::RunTest(const FString& Parameters)
|
||||
|
||||
Stack.PrepareChunk(Context);
|
||||
FStackDensitySampler Sampler(Stack);
|
||||
|
||||
if (!bFineCapRefusalChecked)
|
||||
{
|
||||
FVoxelStrateMeasureSettings RefusalSettings = FineMeasureSettings;
|
||||
RefusalSettings.MaxCells = 1;
|
||||
FVoxelStrateSampleGrid RefusedFineGrid;
|
||||
const FVoxelStrateMetrics RefusedFineMetrics = VF_MeasureStrateWithSampler(
|
||||
Sampler, BottomVoxelZ, TopVoxelZ + 1,
|
||||
Context.EdgeSealThickness, RefusalSettings, &RefusedFineGrid);
|
||||
TestTrue(TEXT("fine preview refuses an over-cap ROI before allocation"),
|
||||
!RefusedFineMetrics.bValid
|
||||
&& RefusedFineGrid.Air.Num() == 0
|
||||
&& RefusedFineGrid.Density.Num() == 0
|
||||
&& RefusedFineMetrics.RefusalReason.Contains(TEXT("MaxCells")));
|
||||
bFineCapRefusalChecked = true;
|
||||
}
|
||||
|
||||
FVoxelStrateSampleGrid SampleGrid;
|
||||
const FVoxelStrateMetrics Metrics = VF_MeasureStrateWithSampler(
|
||||
Sampler, BottomVoxelZ, TopVoxelZ + 1,
|
||||
@@ -527,10 +567,11 @@ bool FVoxelForgeComposerStructureRollTest::RunTest(const FString& Parameters)
|
||||
}
|
||||
TestTrue(FString::Printf(TEXT("candidate %d preview captures its measurement grid"),
|
||||
CandidateIndex),
|
||||
!Metrics.bValid || SampleGrid.IsValid());
|
||||
!Metrics.bValid || SampleGrid.HasScalarDensity());
|
||||
|
||||
FVoxelStratePreviewCandidate PreviewCandidate;
|
||||
FString PreviewError;
|
||||
const double CoarsePreviewStartSeconds = FPlatformTime::Seconds();
|
||||
const bool bPreviewWritten = VF_WriteStratePreviewCandidate(
|
||||
PreviewDirectory,
|
||||
CandidateIndex,
|
||||
@@ -544,17 +585,83 @@ bool FVoxelForgeComposerStructureRollTest::RunTest(const FString& Parameters)
|
||||
RejectionReason,
|
||||
PreviewCandidate,
|
||||
PreviewError);
|
||||
CoarsePreviewSeconds += FPlatformTime::Seconds() - CoarsePreviewStartSeconds;
|
||||
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);
|
||||
!Metrics.bValid
|
||||
|| (PreviewCandidate.bRendered && PreviewCandidate.bContourRendered));
|
||||
if (!bPreviewWritten)
|
||||
{
|
||||
AddError(FString::Printf(TEXT("candidate %d preview failed: %s"),
|
||||
CandidateIndex, *PreviewError));
|
||||
}
|
||||
|
||||
const bool bCandidateSurvivor = bNonVacuous && bLargestEnough && bLawPass;
|
||||
if (bCandidateSurvivor)
|
||||
{
|
||||
++NumFineRequested;
|
||||
FVoxelStrateSampleGrid FineGrid;
|
||||
const double FineMeasureStartSeconds = FPlatformTime::Seconds();
|
||||
const FVoxelStrateMetrics FineMetrics = VF_MeasureStrateWithSampler(
|
||||
Sampler, BottomVoxelZ, TopVoxelZ + 1,
|
||||
Context.EdgeSealThickness, FineMeasureSettings, &FineGrid);
|
||||
FineMeasureSeconds += FPlatformTime::Seconds() - FineMeasureStartSeconds;
|
||||
|
||||
const FString FineFailureReason = FineMetrics.bValid
|
||||
? FString() : FineMetrics.RefusalReason;
|
||||
const double FineRenderStartSeconds = FPlatformTime::Seconds();
|
||||
FString FinePreviewError;
|
||||
const bool bFineWritten = VF_WriteStratePreviewFineCandidate(
|
||||
PreviewDirectory, CandidateIndex, FineGrid,
|
||||
FineMeasureSettings.HeadroomCells, FineFailureReason,
|
||||
PreviewCandidate, FinePreviewError);
|
||||
FineRenderSeconds += FPlatformTime::Seconds() - FineRenderStartSeconds;
|
||||
|
||||
TestTrue(FString::Printf(TEXT("candidate %d fine preview records without a second renderer sample"),
|
||||
CandidateIndex),
|
||||
bFineWritten);
|
||||
TestTrue(FString::Printf(TEXT("candidate %d fine capture keeps scalar density"),
|
||||
CandidateIndex),
|
||||
!FineMetrics.bValid || FineGrid.HasScalarDensity());
|
||||
if (FineMetrics.bValid)
|
||||
{
|
||||
TotalFineCells += FineGrid.CellCount;
|
||||
PeakFineCaptureBytes = FMath::Max(
|
||||
PeakFineCaptureBytes,
|
||||
static_cast<int64>(FineGrid.Air.GetAllocatedSize())
|
||||
+ static_cast<int64>(FineGrid.Density.GetAllocatedSize()));
|
||||
}
|
||||
if (!FineMetrics.bValid)
|
||||
{
|
||||
++NumFineRefused;
|
||||
}
|
||||
if (PreviewCandidate.bFineRendered)
|
||||
{
|
||||
++NumFineRendered;
|
||||
PeakFineRasterBytes = FMath::Max(
|
||||
PeakFineRasterBytes,
|
||||
static_cast<int64>(PreviewCandidate.FineVerticalImageWidth)
|
||||
* static_cast<int64>(PreviewCandidate.FineVerticalImageHeight)
|
||||
* static_cast<int64>(sizeof(FColor)));
|
||||
PeakFineRasterBytes = FMath::Max(
|
||||
PeakFineRasterBytes,
|
||||
static_cast<int64>(PreviewCandidate.FinePlanImageWidth)
|
||||
* static_cast<int64>(PreviewCandidate.FinePlanImageHeight)
|
||||
* static_cast<int64>(sizeof(FColor)));
|
||||
}
|
||||
if (PreviewCandidate.bFineContourRendered)
|
||||
{
|
||||
++NumFineContourRendered;
|
||||
}
|
||||
if (!bFineWritten)
|
||||
{
|
||||
AddError(FString::Printf(TEXT("candidate %d fine preview failed: %s"),
|
||||
CandidateIndex, *FinePreviewError));
|
||||
}
|
||||
}
|
||||
PreviewCandidates.Add(MoveTemp(PreviewCandidate));
|
||||
|
||||
// Same recipe + same block vectors + same seed must yield the same world values. This is
|
||||
@@ -637,6 +744,17 @@ bool FVoxelForgeComposerStructureRollTest::RunTest(const FString& Parameters)
|
||||
TotalBoxMixed, TotalBoxAllSolid, TotalBoxAllAir, TotalBoxProved,
|
||||
TotalBoxCheckedVoxels, TotalBoxViolations,
|
||||
FirstBoxViolation.IsEmpty() ? TEXT("No violations.") : *FirstBoxViolation));
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("Preview cost: coarse step=%d 64-card pass render %.3fs; fine ROI requested=%d "
|
||||
"(survivors=%d), rendered=%d, contour pairs=%d, refused=%d; fine sample %.3fs + "
|
||||
"render %.3fs; fine cells sampled=%lld; peak fine Air+Density capture=%lld bytes; "
|
||||
"peak single RGBA raster=%lld bytes; ROI step=%d radius=%d MaxCells=%d."),
|
||||
MeasureSettings.SampleStep, CoarsePreviewSeconds,
|
||||
NumFineRequested, NumSurvivors, NumFineRendered, NumFineContourRendered, NumFineRefused,
|
||||
FineMeasureSeconds, FineRenderSeconds, TotalFineCells,
|
||||
PeakFineCaptureBytes, PeakFineRasterBytes,
|
||||
FinePreviewSettings.SampleStep, FinePreviewSettings.RadiusInVoxels,
|
||||
FinePreviewSettings.MaxCells));
|
||||
FString PreviewIndexPath;
|
||||
FString PreviewError;
|
||||
const bool bPreviewIndexWritten = bPreviewWindowSet
|
||||
@@ -658,20 +776,28 @@ bool FVoxelForgeComposerStructureRollTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
const FVoxelStratePreviewCandidate& FirstPreview = PreviewCandidates[0];
|
||||
int32 NumRenderedPairs = 0;
|
||||
int32 NumRenderedContourPairs = 0;
|
||||
for (const FVoxelStratePreviewCandidate& Candidate : PreviewCandidates)
|
||||
{
|
||||
if (Candidate.bRendered)
|
||||
{
|
||||
++NumRenderedPairs;
|
||||
}
|
||||
if (Candidate.bContourRendered)
|
||||
{
|
||||
++NumRenderedContourPairs;
|
||||
}
|
||||
}
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("Composer preview: %d candidates, %d rendered pairs, vertical=%dx%d, plan=%dx%d, "
|
||||
TEXT("Composer preview: %d candidates, %d filled pairs + %d contour 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."),
|
||||
"fine previews are linked on survivor cards; image cap=512 px including 20 px "
|
||||
"scale footer."),
|
||||
PreviewCandidates.Num(),
|
||||
NumRenderedPairs,
|
||||
NumRenderedContourPairs,
|
||||
FirstPreview.VerticalImageWidth, FirstPreview.VerticalImageHeight,
|
||||
FirstPreview.PlanImageWidth, FirstPreview.PlanImageHeight,
|
||||
FirstPreview.PlanSliceWorldZ,
|
||||
@@ -686,6 +812,14 @@ bool FVoxelForgeComposerStructureRollTest::RunTest(const FString& Parameters)
|
||||
TestEqual(TEXT("exactly 64 structure candidates were requested"), NumCandidates, 64);
|
||||
TestEqual(TEXT("invalid recipes emitted by the roller/build path"), NumInvalidRecipes, 0);
|
||||
TestEqual(TEXT("custom-stack box verdict violations"), TotalBoxViolations, 0);
|
||||
TestTrue(TEXT("fine cap refusal was checked before any fine allocation"),
|
||||
bFineCapRefusalChecked);
|
||||
TestEqual(TEXT("fine previews are requested only for coarse survivors"),
|
||||
NumFineRequested, NumSurvivors);
|
||||
TestEqual(TEXT("fine preview outcomes account for every requested survivor"),
|
||||
NumFineRendered + NumFineRefused, NumFineRequested);
|
||||
TestEqual(TEXT("every rendered fine preview has a scalar contour pair"),
|
||||
NumFineContourRendered, NumFineRendered);
|
||||
TestTrue(TEXT("structure roller emits 64 distinct structural recipes"),
|
||||
DistinctRecipes.Num() == NumCandidates);
|
||||
|
||||
|
||||
@@ -32,6 +32,8 @@ namespace VoxelStrateMeasurePrivate
|
||||
|
||||
// 1 = air, 0 = solid. The polarity is deliberately explicit at the sampling site below.
|
||||
TArray<uint8> Air;
|
||||
// Optional exact scalar samples retained only for an explicit preview capture.
|
||||
TArray<float> Density;
|
||||
|
||||
FORCEINLINE int32 Index(int32 X, int32 Y, int32 Z) const
|
||||
{
|
||||
@@ -84,6 +86,7 @@ namespace VoxelStrateMeasurePrivate
|
||||
float ExplicitBoundarySealThickness,
|
||||
bool bUseExplicitBounds,
|
||||
const FVoxelStrateMeasureSettings& Settings,
|
||||
bool bCaptureDensity,
|
||||
FSampleGrid& OutGrid,
|
||||
FString& OutReason)
|
||||
{
|
||||
@@ -356,6 +359,10 @@ namespace VoxelStrateMeasurePrivate
|
||||
OutGrid.SampledMinZ = static_cast<int32>(InteriorMinZ);
|
||||
OutGrid.SampledMaxZ = static_cast<int32>(InteriorMaxZ);
|
||||
OutGrid.Air.SetNumUninitialized(OutGrid.CellCount);
|
||||
if (bCaptureDensity)
|
||||
{
|
||||
OutGrid.Density.SetNumUninitialized(OutGrid.CellCount);
|
||||
}
|
||||
|
||||
bool bSawNonFiniteDensity = false;
|
||||
int32 CellIndex = 0;
|
||||
@@ -375,6 +382,10 @@ namespace VoxelStrateMeasurePrivate
|
||||
}
|
||||
|
||||
// MC convention in this codebase: negative is solid, positive is air.
|
||||
if (bCaptureDensity)
|
||||
{
|
||||
OutGrid.Density[CellIndex] = Density;
|
||||
}
|
||||
OutGrid.Air[CellIndex++] = Density > 0.0f ? 1u : 0u;
|
||||
}
|
||||
}
|
||||
@@ -408,6 +419,7 @@ namespace VoxelStrateMeasurePrivate
|
||||
// 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);
|
||||
Destination.Density = MoveTemp(Source.Density);
|
||||
}
|
||||
|
||||
void DecodeIndex(const FSampleGrid& Grid, int32 Index, int32& OutX, int32& OutY, int32& OutZ)
|
||||
@@ -1237,7 +1249,7 @@ namespace VoxelStrateMeasurePrivate
|
||||
if (!BuildSampleGrid(Generator, Manager, Sampler, StrateIndex,
|
||||
ExplicitBottomWorldZ, ExplicitTopWorldZ,
|
||||
ExplicitBoundarySealThickness, bUseExplicitBounds,
|
||||
Settings, Grid, RefusalReason))
|
||||
Settings, false, Grid, RefusalReason))
|
||||
{
|
||||
if (OutDiagnostics != nullptr)
|
||||
{
|
||||
@@ -1291,7 +1303,7 @@ FVoxelStrateMetrics VF_MeasureStrate(
|
||||
VoxelStrateMeasurePrivate::FSampleGrid Grid;
|
||||
if (!VoxelStrateMeasurePrivate::BuildSampleGrid(
|
||||
&Generator, &Manager, nullptr, StrateIndex, 0, 0, 0.0f, false,
|
||||
Settings, Grid, Result.RefusalReason))
|
||||
Settings, OutSampleGrid != nullptr, Grid, Result.RefusalReason))
|
||||
{
|
||||
return Result;
|
||||
}
|
||||
@@ -1335,7 +1347,8 @@ FVoxelStrateMetrics VF_MeasureStrateWithSampler(
|
||||
VoxelStrateMeasurePrivate::FSampleGrid Grid;
|
||||
if (!VoxelStrateMeasurePrivate::BuildSampleGrid(
|
||||
nullptr, nullptr, &Sampler, INDEX_NONE, StrateBottomWorldZ, StrateTopWorldZ,
|
||||
BoundarySealThickness, true, Settings, Grid, Result.RefusalReason))
|
||||
BoundarySealThickness, true, Settings, OutSampleGrid != nullptr,
|
||||
Grid, Result.RefusalReason))
|
||||
{
|
||||
return Result;
|
||||
}
|
||||
|
||||
@@ -53,6 +53,8 @@ namespace
|
||||
const FColor GSolidColor(18, 23, 30, 255);
|
||||
const FColor GAirColor(218, 225, 231, 255);
|
||||
const FColor GWalkableColor(245, 166, 54, 255);
|
||||
const FColor GContourBackground(12, 19, 25, 255);
|
||||
const FColor GContourLineColor(77, 226, 190, 255);
|
||||
const FColor GScaleBarColor(245, 245, 245, 255);
|
||||
const FColor GScaleBarBackground(9, 12, 17, 255);
|
||||
|
||||
@@ -375,6 +377,262 @@ namespace
|
||||
return OutWidth > 0 && OutHeight > 0;
|
||||
}
|
||||
|
||||
struct FContourPoint
|
||||
{
|
||||
float X = 0.0f;
|
||||
float Y = 0.0f;
|
||||
};
|
||||
|
||||
FContourPoint VF_InterpolateContourPoint(const FContourPoint& A,
|
||||
const FContourPoint& B,
|
||||
float ValueA,
|
||||
float ValueB)
|
||||
{
|
||||
const float Denominator = ValueA - ValueB;
|
||||
const float Alpha = FMath::IsNearlyZero(Denominator)
|
||||
? 0.5f
|
||||
: FMath::Clamp(ValueA / Denominator, 0.0f, 1.0f);
|
||||
return {
|
||||
FMath::Lerp(A.X, B.X, Alpha),
|
||||
FMath::Lerp(A.Y, B.Y, Alpha)
|
||||
};
|
||||
}
|
||||
|
||||
void VF_DrawContourLine(TArray<FColor>& Pixels, int32 Width, int32 Height,
|
||||
const FContourPoint& A, const FContourPoint& B)
|
||||
{
|
||||
// The caller passes the content height so the scale-bar footer is never painted over.
|
||||
// The pixel buffer is intentionally taller than this region.
|
||||
if (Width <= 0 || Height <= 0 || Pixels.Num() < Width * Height)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int32 X0 = FMath::Clamp(FMath::RoundToInt(A.X), 0, Width - 1);
|
||||
int32 Y0 = FMath::Clamp(FMath::RoundToInt(A.Y), 0, Height - 1);
|
||||
const int32 X1 = FMath::Clamp(FMath::RoundToInt(B.X), 0, Width - 1);
|
||||
const int32 Y1 = FMath::Clamp(FMath::RoundToInt(B.Y), 0, Height - 1);
|
||||
const int32 DeltaX = FMath::Abs(X1 - X0);
|
||||
const int32 StepX = X0 < X1 ? 1 : -1;
|
||||
const int32 DeltaY = -FMath::Abs(Y1 - Y0);
|
||||
const int32 StepY = Y0 < Y1 ? 1 : -1;
|
||||
int32 Error = DeltaX + DeltaY;
|
||||
|
||||
for (;;)
|
||||
{
|
||||
Pixels[Y0 * Width + X0] = GContourLineColor;
|
||||
if (X0 == X1 && Y0 == Y1)
|
||||
{
|
||||
break;
|
||||
}
|
||||
const int32 DoubleError = 2 * Error;
|
||||
if (DoubleError >= DeltaY)
|
||||
{
|
||||
Error += DeltaY;
|
||||
X0 += StepX;
|
||||
}
|
||||
if (DoubleError <= DeltaX)
|
||||
{
|
||||
Error += DeltaX;
|
||||
Y0 += StepY;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void VF_DrawContourQuad(TArray<FColor>& Pixels, int32 Width, int32 Height,
|
||||
const FContourPoint& A, const FContourPoint& B,
|
||||
const FContourPoint& C, const FContourPoint& D,
|
||||
float ValueA, float ValueB, float ValueC, float ValueD)
|
||||
{
|
||||
if (!FMath::IsFinite(ValueA) || !FMath::IsFinite(ValueB)
|
||||
|| !FMath::IsFinite(ValueC) || !FMath::IsFinite(ValueD))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// A/B/C/D wind around one scalar quad. The bit mask is deliberately based on the same
|
||||
// strict sign rule as Air: density > 0 is air, density <= 0 is solid.
|
||||
const int32 Mask = (ValueA > 0.0f ? 1 : 0)
|
||||
| (ValueB > 0.0f ? 2 : 0)
|
||||
| (ValueC > 0.0f ? 4 : 0)
|
||||
| (ValueD > 0.0f ? 8 : 0);
|
||||
if (Mask == 0 || Mask == 15)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const FContourPoint Edge0 = VF_InterpolateContourPoint(A, B, ValueA, ValueB);
|
||||
const FContourPoint Edge1 = VF_InterpolateContourPoint(B, C, ValueB, ValueC);
|
||||
const FContourPoint Edge2 = VF_InterpolateContourPoint(C, D, ValueC, ValueD);
|
||||
const FContourPoint Edge3 = VF_InterpolateContourPoint(D, A, ValueD, ValueA);
|
||||
const float CentreValue = 0.25f * (ValueA + ValueB + ValueC + ValueD);
|
||||
|
||||
auto Draw = [&](const FContourPoint& First, const FContourPoint& Second)
|
||||
{
|
||||
VF_DrawContourLine(Pixels, Width, Height, First, Second);
|
||||
};
|
||||
|
||||
switch (Mask)
|
||||
{
|
||||
case 1: Draw(Edge3, Edge0); break;
|
||||
case 2: Draw(Edge0, Edge1); break;
|
||||
case 3: Draw(Edge3, Edge1); break;
|
||||
case 4: Draw(Edge1, Edge2); break;
|
||||
case 5:
|
||||
// The diagonal case is decided from the scalar centre, not from the binary image.
|
||||
// This makes the saddle deterministic while keeping the isoline tied to the field.
|
||||
if (CentreValue > 0.0f)
|
||||
{
|
||||
Draw(Edge0, Edge1);
|
||||
Draw(Edge2, Edge3);
|
||||
}
|
||||
else
|
||||
{
|
||||
Draw(Edge3, Edge0);
|
||||
Draw(Edge1, Edge2);
|
||||
}
|
||||
break;
|
||||
case 6: Draw(Edge0, Edge2); break;
|
||||
case 7: Draw(Edge2, Edge3); break;
|
||||
case 8: Draw(Edge2, Edge3); break;
|
||||
case 9: Draw(Edge0, Edge2); break;
|
||||
case 10:
|
||||
if (CentreValue > 0.0f)
|
||||
{
|
||||
Draw(Edge3, Edge0);
|
||||
Draw(Edge1, Edge2);
|
||||
}
|
||||
else
|
||||
{
|
||||
Draw(Edge0, Edge1);
|
||||
Draw(Edge2, Edge3);
|
||||
}
|
||||
break;
|
||||
case 11: Draw(Edge1, Edge2); break;
|
||||
case 12: Draw(Edge1, Edge3); break;
|
||||
case 13: Draw(Edge0, Edge1); break;
|
||||
case 14: Draw(Edge0, Edge3); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
FContourPoint VF_MapVerticalContourPoint(int32 X, int32 Z,
|
||||
int32 SourceWidth, int32 SourceHeight,
|
||||
int32 OutputWidth, int32 ContentHeight)
|
||||
{
|
||||
return {
|
||||
SourceWidth > 1
|
||||
? static_cast<float>(X) * static_cast<float>(OutputWidth - 1)
|
||||
/ static_cast<float>(SourceWidth - 1)
|
||||
: 0.0f,
|
||||
SourceHeight > 1
|
||||
? static_cast<float>(SourceHeight - 1 - Z)
|
||||
* static_cast<float>(ContentHeight - 1)
|
||||
/ static_cast<float>(SourceHeight - 1)
|
||||
: 0.0f
|
||||
};
|
||||
}
|
||||
|
||||
FContourPoint VF_MapPlanContourPoint(int32 X, int32 Y,
|
||||
int32 SourceWidth, int32 SourceHeight,
|
||||
int32 OutputWidth, int32 ContentHeight)
|
||||
{
|
||||
return {
|
||||
SourceWidth > 1
|
||||
? static_cast<float>(X) * static_cast<float>(OutputWidth - 1)
|
||||
/ static_cast<float>(SourceWidth - 1)
|
||||
: 0.0f,
|
||||
SourceHeight > 1
|
||||
? static_cast<float>(Y) * static_cast<float>(ContentHeight - 1)
|
||||
/ static_cast<float>(SourceHeight - 1)
|
||||
: 0.0f
|
||||
};
|
||||
}
|
||||
|
||||
bool VF_RenderVerticalContour(const FVoxelStrateSampleGrid& Grid, int32 FixedY,
|
||||
TArray<FColor>& OutPixels,
|
||||
int32& OutWidth, int32& OutHeight)
|
||||
{
|
||||
if (!Grid.HasScalarDensity())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const int32 MaxContentDimension = GMaxPreviewImageDimension
|
||||
- GPreviewScaleBarFooterPixels;
|
||||
OutWidth = FMath::Min(Grid.NumX, MaxContentDimension);
|
||||
const int32 ContentHeight = FMath::Min(Grid.NumZ, MaxContentDimension);
|
||||
OutHeight = ContentHeight + GPreviewScaleBarFooterPixels;
|
||||
OutPixels.Init(GContourBackground, OutWidth * OutHeight);
|
||||
if (Grid.NumX > 1 && Grid.NumZ > 1)
|
||||
{
|
||||
for (int32 Z = 0; Z + 1 < Grid.NumZ; ++Z)
|
||||
{
|
||||
for (int32 X = 0; X + 1 < Grid.NumX; ++X)
|
||||
{
|
||||
const FContourPoint A = VF_MapVerticalContourPoint(
|
||||
X, Z, Grid.NumX, Grid.NumZ, OutWidth, ContentHeight);
|
||||
const FContourPoint B = VF_MapVerticalContourPoint(
|
||||
X + 1, Z, Grid.NumX, Grid.NumZ, OutWidth, ContentHeight);
|
||||
const FContourPoint C = VF_MapVerticalContourPoint(
|
||||
X + 1, Z + 1, Grid.NumX, Grid.NumZ, OutWidth, ContentHeight);
|
||||
const FContourPoint D = VF_MapVerticalContourPoint(
|
||||
X, Z + 1, Grid.NumX, Grid.NumZ, OutWidth, ContentHeight);
|
||||
VF_DrawContourQuad(
|
||||
OutPixels, OutWidth, ContentHeight, A, B, C, D,
|
||||
Grid.Density[Grid.Index(X, FixedY, Z)],
|
||||
Grid.Density[Grid.Index(X + 1, FixedY, Z)],
|
||||
Grid.Density[Grid.Index(X + 1, FixedY, Z + 1)],
|
||||
Grid.Density[Grid.Index(X, FixedY, Z + 1)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
VF_DrawScaleBar(OutPixels, OutWidth, ContentHeight, Grid.NumX, Grid.SampleStep);
|
||||
return OutWidth > 0 && OutHeight > 0;
|
||||
}
|
||||
|
||||
bool VF_RenderPlanContour(const FVoxelStrateSampleGrid& Grid, int32 FixedZ,
|
||||
TArray<FColor>& OutPixels,
|
||||
int32& OutWidth, int32& OutHeight)
|
||||
{
|
||||
if (!Grid.HasScalarDensity())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const int32 MaxContentDimension = GMaxPreviewImageDimension
|
||||
- GPreviewScaleBarFooterPixels;
|
||||
OutWidth = FMath::Min(Grid.NumX, MaxContentDimension);
|
||||
const int32 ContentHeight = FMath::Min(Grid.NumY, MaxContentDimension);
|
||||
OutHeight = ContentHeight + GPreviewScaleBarFooterPixels;
|
||||
OutPixels.Init(GContourBackground, OutWidth * OutHeight);
|
||||
if (Grid.NumX > 1 && Grid.NumY > 1)
|
||||
{
|
||||
for (int32 Y = 0; Y + 1 < Grid.NumY; ++Y)
|
||||
{
|
||||
for (int32 X = 0; X + 1 < Grid.NumX; ++X)
|
||||
{
|
||||
const FContourPoint A = VF_MapPlanContourPoint(
|
||||
X, Y, Grid.NumX, Grid.NumY, OutWidth, ContentHeight);
|
||||
const FContourPoint B = VF_MapPlanContourPoint(
|
||||
X + 1, Y, Grid.NumX, Grid.NumY, OutWidth, ContentHeight);
|
||||
const FContourPoint C = VF_MapPlanContourPoint(
|
||||
X + 1, Y + 1, Grid.NumX, Grid.NumY, OutWidth, ContentHeight);
|
||||
const FContourPoint D = VF_MapPlanContourPoint(
|
||||
X, Y + 1, Grid.NumX, Grid.NumY, OutWidth, ContentHeight);
|
||||
VF_DrawContourQuad(
|
||||
OutPixels, OutWidth, ContentHeight, A, B, C, D,
|
||||
Grid.Density[Grid.Index(X, Y, FixedZ)],
|
||||
Grid.Density[Grid.Index(X + 1, Y, FixedZ)],
|
||||
Grid.Density[Grid.Index(X + 1, Y + 1, FixedZ)],
|
||||
Grid.Density[Grid.Index(X, Y + 1, FixedZ)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
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)
|
||||
{
|
||||
@@ -461,6 +719,41 @@ bool VF_WriteStratePreviewCandidate(
|
||||
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.VerticalContourImageFile = Stem + TEXT("_vertical_contour.png");
|
||||
OutCandidate.PlanImageFile = Stem + TEXT("_plan.png");
|
||||
OutCandidate.PlanContourImageFile = Stem + TEXT("_plan_contour.png");
|
||||
const FString VerticalPath = OutputDirectory / OutCandidate.VerticalImageFile;
|
||||
if (!VF_WritePng(VerticalPath, VerticalPixels, VerticalWidth, VerticalHeight, OutError))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool bHasContour = Grid.HasScalarDensity();
|
||||
if (bHasContour)
|
||||
{
|
||||
int32 ContourWidth = 0;
|
||||
int32 ContourHeight = 0;
|
||||
if (!VF_RenderVerticalContour(Grid, FixedY, VerticalPixels,
|
||||
ContourWidth, ContourHeight)
|
||||
|| ContourWidth != VerticalWidth || ContourHeight != VerticalHeight
|
||||
|| !VF_WritePng(OutputDirectory / OutCandidate.VerticalContourImageFile,
|
||||
VerticalPixels, ContourWidth, ContourHeight, OutError))
|
||||
{
|
||||
OutError = FString::Printf(TEXT("failed to write vertical density contour for candidate %d"),
|
||||
CandidateIndex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
TArray<FColor> PlanPixels;
|
||||
int32 PlanWidth = 0;
|
||||
int32 PlanHeight = 0;
|
||||
@@ -471,25 +764,34 @@ bool VF_WriteStratePreviewCandidate(
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!IFileManager::Get().MakeDirectory(*OutputDirectory, true))
|
||||
const FString PlanPath = OutputDirectory / OutCandidate.PlanImageFile;
|
||||
if (!VF_WritePng(PlanPath, PlanPixels, PlanWidth, PlanHeight, OutError))
|
||||
{
|
||||
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))
|
||||
if (bHasContour)
|
||||
{
|
||||
return false;
|
||||
int32 ContourWidth = 0;
|
||||
int32 ContourHeight = 0;
|
||||
if (!VF_RenderPlanContour(Grid, Plan.CellZ, PlanPixels,
|
||||
ContourWidth, ContourHeight)
|
||||
|| ContourWidth != PlanWidth || ContourHeight != PlanHeight
|
||||
|| !VF_WritePng(OutputDirectory / OutCandidate.PlanContourImageFile,
|
||||
PlanPixels, ContourWidth, ContourHeight, OutError))
|
||||
{
|
||||
OutError = FString::Printf(TEXT("failed to write plan density contour for candidate %d"),
|
||||
CandidateIndex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
OutCandidate.bRendered = true;
|
||||
OutCandidate.bContourRendered = bHasContour;
|
||||
if (!bHasContour)
|
||||
{
|
||||
OutCandidate.RenderFailureReason = TEXT(
|
||||
"no scalar density capture was supplied; filled cells were rendered, but no density=0 contour is available");
|
||||
}
|
||||
OutCandidate.VerticalSliceCellY = FixedY;
|
||||
OutCandidate.VerticalSliceWorldY = VF_CellWorldCoordinate(
|
||||
Grid.MinY, Grid.MaxY, Grid.SampleStep, FixedY);
|
||||
@@ -505,6 +807,126 @@ bool VF_WriteStratePreviewCandidate(
|
||||
return true;
|
||||
}
|
||||
|
||||
bool VF_WriteStratePreviewFineCandidate(
|
||||
const FString& OutputDirectory,
|
||||
int32 CandidateIndex,
|
||||
const FVoxelStrateSampleGrid& FineGrid,
|
||||
int32 HeadroomCells,
|
||||
const FString& FineFailureReason,
|
||||
FVoxelStratePreviewCandidate& InOutCandidate,
|
||||
FString& OutError)
|
||||
{
|
||||
InOutCandidate.bFineRequested = true;
|
||||
InOutCandidate.FineRenderFailureReason = FineFailureReason;
|
||||
if (!FineGrid.IsValid())
|
||||
{
|
||||
if (InOutCandidate.FineRenderFailureReason.IsEmpty())
|
||||
{
|
||||
InOutCandidate.FineRenderFailureReason = TEXT("fine ROI measurement was refused");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
InOutCandidate.FineWindow = VF_GetStratePreviewWindow(FineGrid);
|
||||
if (HeadroomCells < 0)
|
||||
{
|
||||
OutError = TEXT("preview HeadroomCells cannot be negative");
|
||||
return false;
|
||||
}
|
||||
|
||||
const FPlanSliceChoice Plan = VF_ChoosePlanSlice(FineGrid, HeadroomCells);
|
||||
const float CentreY = 0.5f * (FineGrid.MinY + FineGrid.MaxY);
|
||||
const float Step = static_cast<float>(FineGrid.SampleStep);
|
||||
const int32 FixedY = FMath::Clamp(
|
||||
FMath::FloorToInt((CentreY - FineGrid.MinY) / Step), 0, FineGrid.NumY - 1);
|
||||
|
||||
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_fine_step_%d"),
|
||||
CandidateIndex, FineGrid.SampleStep);
|
||||
InOutCandidate.FineVerticalImageFile = Stem + TEXT("_vertical_filled.png");
|
||||
InOutCandidate.FineVerticalContourImageFile = Stem + TEXT("_vertical_contour.png");
|
||||
InOutCandidate.FinePlanImageFile = Stem + TEXT("_plan_filled.png");
|
||||
InOutCandidate.FinePlanContourImageFile = Stem + TEXT("_plan_contour.png");
|
||||
|
||||
TArray<FColor> Pixels;
|
||||
int32 Width = 0;
|
||||
int32 Height = 0;
|
||||
if (!VF_RenderVertical(FineGrid, HeadroomCells, FixedY, Pixels, Width, Height)
|
||||
|| !VF_WritePng(OutputDirectory / InOutCandidate.FineVerticalImageFile,
|
||||
Pixels, Width, Height, OutError))
|
||||
{
|
||||
OutError = FString::Printf(TEXT("failed to write fine vertical filled view for candidate %d"),
|
||||
CandidateIndex);
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool bHasContour = FineGrid.HasScalarDensity();
|
||||
if (bHasContour)
|
||||
{
|
||||
int32 ContourWidth = 0;
|
||||
int32 ContourHeight = 0;
|
||||
if (!VF_RenderVerticalContour(FineGrid, FixedY, Pixels,
|
||||
ContourWidth, ContourHeight)
|
||||
|| ContourWidth != Width || ContourHeight != Height
|
||||
|| !VF_WritePng(OutputDirectory / InOutCandidate.FineVerticalContourImageFile,
|
||||
Pixels, ContourWidth, ContourHeight, OutError))
|
||||
{
|
||||
OutError = FString::Printf(TEXT("failed to write fine vertical density contour for candidate %d"),
|
||||
CandidateIndex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
InOutCandidate.FineVerticalSliceCellY = FixedY;
|
||||
InOutCandidate.FineVerticalSliceWorldY = VF_CellWorldCoordinate(
|
||||
FineGrid.MinY, FineGrid.MaxY, FineGrid.SampleStep, FixedY);
|
||||
InOutCandidate.FineVerticalImageWidth = Width;
|
||||
InOutCandidate.FineVerticalImageHeight = Height;
|
||||
|
||||
if (!VF_RenderPlan(FineGrid, HeadroomCells, Plan.CellZ, Pixels, Width, Height)
|
||||
|| !VF_WritePng(OutputDirectory / InOutCandidate.FinePlanImageFile,
|
||||
Pixels, Width, Height, OutError))
|
||||
{
|
||||
OutError = FString::Printf(TEXT("failed to write fine plan filled view for candidate %d"),
|
||||
CandidateIndex);
|
||||
return false;
|
||||
}
|
||||
if (bHasContour)
|
||||
{
|
||||
int32 ContourWidth = 0;
|
||||
int32 ContourHeight = 0;
|
||||
if (!VF_RenderPlanContour(FineGrid, Plan.CellZ, Pixels,
|
||||
ContourWidth, ContourHeight)
|
||||
|| ContourWidth != Width || ContourHeight != Height
|
||||
|| !VF_WritePng(OutputDirectory / InOutCandidate.FinePlanContourImageFile,
|
||||
Pixels, ContourWidth, ContourHeight, OutError))
|
||||
{
|
||||
OutError = FString::Printf(TEXT("failed to write fine plan density contour for candidate %d"),
|
||||
CandidateIndex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
InOutCandidate.bFineRendered = true;
|
||||
InOutCandidate.bFineContourRendered = bHasContour;
|
||||
InOutCandidate.FinePlanSliceCellZ = Plan.CellZ;
|
||||
InOutCandidate.FinePlanSliceWorldZ = VF_CellWorldCoordinate(
|
||||
FineGrid.MinZ, FineGrid.MaxZ, FineGrid.SampleStep, Plan.CellZ);
|
||||
InOutCandidate.FinePlanImageWidth = Width;
|
||||
InOutCandidate.FinePlanImageHeight = Height;
|
||||
if (!bHasContour && InOutCandidate.FineRenderFailureReason.IsEmpty())
|
||||
{
|
||||
InOutCandidate.FineRenderFailureReason = TEXT(
|
||||
"no scalar density capture was supplied; filled cells were rendered, but no density=0 contour is available");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool VF_WriteStratePreviewIndex(
|
||||
const FString& OutputDirectory,
|
||||
const FString& RunTitle,
|
||||
@@ -572,12 +994,17 @@ bool VF_WriteStratePreviewIndex(
|
||||
".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"
|
||||
".window{font-size:11px;color:#8b949e;margin:5px 0 10px}.view-section{margin:12px 0}.view-section h3{font-size:12px;letter-spacing:.03em;color:#e6edf3;margin:10px 0 3px}.pair{display:grid;grid-template-columns:1fr 1fr;gap:8px}.pair figure{margin:0}.pair img{display:block;width:100%;height:auto;background:#090c11;image-rendering:pixelated;border:1px solid #30363d}.pair figcaption{font-size:11px;color:#aab4c0;margin-top:3px}.fine-section{border-top:1px solid #6e4e1f;margin-top:14px;padding-top:4px}.fine-section h3{color:#ffd580}.not-requested{color:#8b949e;font-size:12px;margin:8px 0}.contour-note{color:#8cebd1}\n"
|
||||
".no-image{min-height:40px;border:1px dashed #8b3b42;color:#ffb4b8;padding:12px;font-size:12px}\n"
|
||||
"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\"><strong>How to read coarse images:</strong> At sample step %d, "
|
||||
"hard edges can come from sampling, not necessarily hard world walls. A "
|
||||
"lattice-sourced strate is genuinely rectilinear at step 1; that is expected for a maze.</p>\n"),
|
||||
Window.SampleStep);
|
||||
Html += FString::Printf(
|
||||
TEXT("<p class=\"note\">%s "
|
||||
"Density polarity is explicit: density > 0 is air. One chunk = 32 voxels = 8 m "
|
||||
@@ -592,7 +1019,12 @@ bool VF_WriteStratePreviewIndex(
|
||||
"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"
|
||||
"<span><i class=\"swatch\" style=\"background:#f5a636\"></i>walkable cell</span>"
|
||||
"<span><i class=\"swatch\" style=\"background:#4de2be\"></i>density=0 contour</span></div>\n"
|
||||
"<p class=\"note\"><strong>Fine ROI:</strong> the separate step-1 pass is requested only "
|
||||
"for selected candidates (this run selects coarse survivors). Its exact window is printed "
|
||||
"on the card. If its cell cap is exceeded, no fine allocation is made and the refusal is "
|
||||
"shown on that card.</p>\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, "
|
||||
@@ -622,24 +1054,127 @@ bool VF_WriteStratePreviewIndex(
|
||||
}
|
||||
Html += FString::Printf(TEXT("<p class=\"window\">Numbers and slice choices use: %s</p>\n"),
|
||||
*VF_HtmlEscape(MetricWindow));
|
||||
Html += TEXT("<div class=\"images\">\n");
|
||||
Html += TEXT("<div class=\"view-section\"><h3>COARSE XZ — filled cells and density=0 contour</h3>\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"),
|
||||
TEXT("<p class=\"window\">Exact coarse window: %s; slice Y=%d voxels.</p>\n"
|
||||
"<div class=\"pair\"><figure><img src=\"%s\" alt=\"candidate %d coarse XZ filled cells\"><figcaption>"
|
||||
"COARSE · filled cells · step=%d</figcaption></figure>\n"),
|
||||
*VF_HtmlEscape(MetricWindow), Candidate.VerticalSliceWorldY,
|
||||
*VF_HtmlEscape(Candidate.VerticalImageFile), Candidate.CandidateIndex,
|
||||
Candidate.VerticalSliceWorldY,
|
||||
*VF_HtmlEscape(Candidate.PlanImageFile), Candidate.CandidateIndex,
|
||||
Candidate.PlanSliceWorldZ, Candidate.PlanSliceBoundaryTransitions);
|
||||
Candidate.Window.SampleStep);
|
||||
if (Candidate.bContourRendered)
|
||||
{
|
||||
Html += FString::Printf(
|
||||
TEXT("<figure><img src=\"%s\" alt=\"candidate %d coarse XZ density zero contour\"><figcaption>"
|
||||
"COARSE · density=0 contour · scalar field · step=%d</figcaption></figure></div>\n"),
|
||||
*VF_HtmlEscape(Candidate.VerticalContourImageFile), Candidate.CandidateIndex,
|
||||
Candidate.Window.SampleStep);
|
||||
}
|
||||
else
|
||||
{
|
||||
Html += FString::Printf(
|
||||
TEXT("<div class=\"no-image\"><span class=\"contour-note\">No coarse contour:</span> %s</div></div>\n"),
|
||||
*VF_HtmlEscape(Candidate.RenderFailureReason));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Html += FString::Printf(TEXT("<div class=\"no-image\">No image: %s</div>\n"),
|
||||
Html += FString::Printf(TEXT("<div class=\"no-image\">No coarse image: %s</div>\n"),
|
||||
*VF_HtmlEscape(Candidate.RenderFailureReason));
|
||||
}
|
||||
Html += TEXT("</div>\n<div class=\"view-section\"><h3>COARSE XY — filled cells and density=0 contour</h3>\n");
|
||||
if (Candidate.bRendered)
|
||||
{
|
||||
Html += FString::Printf(
|
||||
TEXT("<p class=\"window\">Exact coarse window: %s; selected Z=%d voxels; %d solid/air boundary transitions.</p>\n"
|
||||
"<div class=\"pair\"><figure><img src=\"%s\" alt=\"candidate %d coarse XY filled cells\"><figcaption>"
|
||||
"COARSE · filled cells · step=%d</figcaption></figure>\n"),
|
||||
*VF_HtmlEscape(MetricWindow), Candidate.PlanSliceWorldZ,
|
||||
Candidate.PlanSliceBoundaryTransitions,
|
||||
*VF_HtmlEscape(Candidate.PlanImageFile), Candidate.CandidateIndex,
|
||||
Candidate.Window.SampleStep);
|
||||
if (Candidate.bContourRendered)
|
||||
{
|
||||
Html += FString::Printf(
|
||||
TEXT("<figure><img src=\"%s\" alt=\"candidate %d coarse XY density zero contour\"><figcaption>"
|
||||
"COARSE · density=0 contour · scalar field · step=%d</figcaption></figure></div>\n"),
|
||||
*VF_HtmlEscape(Candidate.PlanContourImageFile), Candidate.CandidateIndex,
|
||||
Candidate.Window.SampleStep);
|
||||
}
|
||||
else
|
||||
{
|
||||
Html += FString::Printf(
|
||||
TEXT("<div class=\"no-image\"><span class=\"contour-note\">No coarse contour:</span> %s</div></div>\n"),
|
||||
*VF_HtmlEscape(Candidate.RenderFailureReason));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Html += FString::Printf(TEXT("<div class=\"no-image\">No coarse image: %s</div>\n"),
|
||||
*VF_HtmlEscape(Candidate.RenderFailureReason));
|
||||
}
|
||||
Html += TEXT("</div>\n<div class=\"fine-section\"><h3>FINE ROI — filled cells and density=0 contour</h3>\n");
|
||||
if (!Candidate.bFineRequested)
|
||||
{
|
||||
Html += TEXT("<p class=\"not-requested\">Fine ROI not requested for this candidate; it did not pass the coarse survivor screen.</p>\n");
|
||||
}
|
||||
else if (!Candidate.bFineRendered)
|
||||
{
|
||||
const FString FineFailureText = Candidate.FineRenderFailureReason.IsEmpty()
|
||||
? TEXT("unspecified fine preview refusal") : Candidate.FineRenderFailureReason;
|
||||
Html += FString::Printf(
|
||||
TEXT("<div class=\"no-image\">Fine ROI requested but refused/not rendered: %s</div>\n"),
|
||||
*VF_HtmlEscape(FineFailureText));
|
||||
}
|
||||
else
|
||||
{
|
||||
const FString FineWindowDescription = Candidate.FineWindow.Describe();
|
||||
Html += FString::Printf(
|
||||
TEXT("<p class=\"window\">Exact fine ROI window: %s; slice Y=%d voxels.</p>\n"
|
||||
"<div class=\"pair\"><figure><img src=\"%s\" alt=\"candidate %d fine XZ filled cells\"><figcaption>"
|
||||
"FINE ROI · filled cells · step=%d</figcaption></figure>\n"),
|
||||
*VF_HtmlEscape(FineWindowDescription), Candidate.FineVerticalSliceWorldY,
|
||||
*VF_HtmlEscape(Candidate.FineVerticalImageFile), Candidate.CandidateIndex,
|
||||
Candidate.FineWindow.SampleStep);
|
||||
if (Candidate.bFineContourRendered)
|
||||
{
|
||||
Html += FString::Printf(
|
||||
TEXT("<figure><img src=\"%s\" alt=\"candidate %d fine XZ density zero contour\"><figcaption>"
|
||||
"FINE ROI · density=0 contour · scalar field · step=%d</figcaption></figure></div>\n"),
|
||||
*VF_HtmlEscape(Candidate.FineVerticalContourImageFile), Candidate.CandidateIndex,
|
||||
Candidate.FineWindow.SampleStep);
|
||||
}
|
||||
else
|
||||
{
|
||||
Html += FString::Printf(
|
||||
TEXT("<div class=\"no-image\"><span class=\"contour-note\">No fine contour:</span> %s</div></div>\n"),
|
||||
*VF_HtmlEscape(Candidate.FineRenderFailureReason));
|
||||
}
|
||||
|
||||
Html += FString::Printf(
|
||||
TEXT("<p class=\"window\">Exact fine ROI window: %s; selected Z=%d voxels.</p>\n"
|
||||
"<div class=\"pair\"><figure><img src=\"%s\" alt=\"candidate %d fine XY filled cells\"><figcaption>"
|
||||
"FINE ROI · filled cells · step=%d</figcaption></figure>\n"),
|
||||
*VF_HtmlEscape(FineWindowDescription), Candidate.FinePlanSliceWorldZ,
|
||||
*VF_HtmlEscape(Candidate.FinePlanImageFile), Candidate.CandidateIndex,
|
||||
Candidate.FineWindow.SampleStep);
|
||||
if (Candidate.bFineContourRendered)
|
||||
{
|
||||
Html += FString::Printf(
|
||||
TEXT("<figure><img src=\"%s\" alt=\"candidate %d fine XY density zero contour\"><figcaption>"
|
||||
"FINE ROI · density=0 contour · scalar field · step=%d</figcaption></figure></div>\n"),
|
||||
*VF_HtmlEscape(Candidate.FinePlanContourImageFile), Candidate.CandidateIndex,
|
||||
Candidate.FineWindow.SampleStep);
|
||||
}
|
||||
else
|
||||
{
|
||||
Html += FString::Printf(
|
||||
TEXT("<div class=\"no-image\"><span class=\"contour-note\">No fine contour:</span> %s</div></div>\n"),
|
||||
*VF_HtmlEscape(Candidate.FineRenderFailureReason));
|
||||
}
|
||||
}
|
||||
Html += TEXT("</div>\n<table><caption class=\"window\">Grid measurements use the window above; "
|
||||
"centroid distance is roll-space</caption>\n");
|
||||
Html += FString::Printf(
|
||||
@@ -703,6 +1238,25 @@ bool VF_WriteStratePreviewCandidate(
|
||||
return false;
|
||||
}
|
||||
|
||||
bool VF_WriteStratePreviewFineCandidate(
|
||||
const FString& OutputDirectory,
|
||||
int32 CandidateIndex,
|
||||
const FVoxelStrateSampleGrid& FineGrid,
|
||||
int32 HeadroomCells,
|
||||
const FString& FineFailureReason,
|
||||
FVoxelStratePreviewCandidate& InOutCandidate,
|
||||
FString& OutError)
|
||||
{
|
||||
(void)OutputDirectory;
|
||||
(void)CandidateIndex;
|
||||
(void)FineGrid;
|
||||
(void)HeadroomCells;
|
||||
(void)FineFailureReason;
|
||||
(void)InOutCandidate;
|
||||
OutError = TEXT("strate preview rendering is editor-only");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool VF_WriteStratePreviewIndex(
|
||||
const FString& OutputDirectory,
|
||||
const FString& RunTitle,
|
||||
|
||||
@@ -45,10 +45,12 @@ struct VOXELFORGE_API FVoxelStrateMeasureSettings
|
||||
};
|
||||
|
||||
/**
|
||||
* Optional capture of the exact coarse grid used by one measurement.
|
||||
* Optional capture of the exact 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.
|
||||
* Density retains the scalar sample that produced that bit, so an editor preview can draw a
|
||||
* density=0 contour without inventing geometry by blurring the binary view. Both arrays are
|
||||
* opt-in so ordinary metrics callers do not retain a multi-million-cell buffer.
|
||||
* It is an editor/automation hand-off for tools such as the composer preview, not a runtime
|
||||
* generation cache.
|
||||
*/
|
||||
@@ -75,6 +77,10 @@ struct VOXELFORGE_API FVoxelStrateSampleGrid
|
||||
// 1 = air (density > 0), 0 = solid (density <= 0), matching the measurement polarity.
|
||||
TArray<uint8> Air;
|
||||
|
||||
// Exact scalar density at the same cell centre as Air. Present on measurement captures;
|
||||
// empty on legacy/binary-only callers. This is never used by generation.
|
||||
TArray<float> Density;
|
||||
|
||||
FORCEINLINE int32 Index(int32 X, int32 Y, int32 Z) const
|
||||
{
|
||||
return static_cast<int32>(
|
||||
@@ -91,6 +97,11 @@ struct VOXELFORGE_API FVoxelStrateSampleGrid
|
||||
* static_cast<int64>(NumZ)
|
||||
&& Air.Num() == CellCount;
|
||||
}
|
||||
|
||||
bool HasScalarDensity() const
|
||||
{
|
||||
return IsValid() && Density.Num() == CellCount;
|
||||
}
|
||||
};
|
||||
|
||||
/** Metrics derived from one coarse grid and its single air flood fill. */
|
||||
|
||||
@@ -33,6 +33,39 @@ struct VOXELFORGE_API FVoxelStratePreviewWindow
|
||||
FString Describe() const;
|
||||
};
|
||||
|
||||
/** Parameters for the optional selected-candidate fine pass. */
|
||||
struct VOXELFORGE_API FVoxelStrateFinePreviewSettings
|
||||
{
|
||||
// The default is deliberately one voxel: the pass exists to expose detail that step 4 cannot.
|
||||
int32 SampleStep = 1;
|
||||
// radius=64 gives a 128x128 XY ROI and stays below the default two-million-cell cap for the
|
||||
// four-chunk fixture while retaining the same origin-centred frame as the coarse sweep.
|
||||
int32 RadiusInVoxels = 64;
|
||||
int32 MaxCells = 2000000;
|
||||
FVector2D CenterXY = FVector2D::ZeroVector;
|
||||
int32 InteriorMarginVoxels = -1;
|
||||
|
||||
bool IsValid() const
|
||||
{
|
||||
return SampleStep > 0 && RadiusInVoxels > 0 && MaxCells > 0
|
||||
&& FMath::IsFinite(CenterXY.X) && FMath::IsFinite(CenterXY.Y);
|
||||
}
|
||||
|
||||
FVoxelStrateMeasureSettings MakeMeasureSettings(
|
||||
const FVoxelStrateMeasureSettings& BaseSettings) const
|
||||
{
|
||||
FVoxelStrateMeasureSettings Settings = BaseSettings;
|
||||
Settings.SampleStep = SampleStep;
|
||||
Settings.RadiusInVoxels = RadiusInVoxels;
|
||||
Settings.CenterXY = CenterXY;
|
||||
Settings.CoverPointA.Reset();
|
||||
Settings.CoverPointB.Reset();
|
||||
Settings.MaxCells = MaxCells;
|
||||
Settings.InteriorMarginVoxels = InteriorMarginVoxels;
|
||||
return Settings;
|
||||
}
|
||||
};
|
||||
|
||||
/** One lightweight row/card record; sampled cells are rendered immediately and are not retained. */
|
||||
struct VOXELFORGE_API FVoxelStratePreviewCandidate
|
||||
{
|
||||
@@ -43,11 +76,26 @@ struct VOXELFORGE_API FVoxelStratePreviewCandidate
|
||||
FString RejectionReason;
|
||||
FString RenderFailureReason;
|
||||
FString VerticalImageFile;
|
||||
FString VerticalContourImageFile;
|
||||
FString PlanImageFile;
|
||||
FString PlanContourImageFile;
|
||||
|
||||
// Fine files are intentionally separate from the coarse names, even when the ROI happens to
|
||||
// have the same raster dimensions. The exact fine window is repeated on the card.
|
||||
FString FineRenderFailureReason;
|
||||
FString FineVerticalImageFile;
|
||||
FString FineVerticalContourImageFile;
|
||||
FString FinePlanImageFile;
|
||||
FString FinePlanContourImageFile;
|
||||
FVoxelStratePreviewWindow FineWindow;
|
||||
|
||||
bool bRejected = false;
|
||||
bool bMetricsValid = false;
|
||||
bool bRendered = false;
|
||||
bool bContourRendered = false;
|
||||
bool bFineRequested = false;
|
||||
bool bFineRendered = false;
|
||||
bool bFineContourRendered = false;
|
||||
|
||||
float AirFraction = 0.0f;
|
||||
float LargestComponentShare = 0.0f;
|
||||
@@ -66,6 +114,15 @@ struct VOXELFORGE_API FVoxelStratePreviewCandidate
|
||||
int32 PlanImageHeight = 0;
|
||||
int32 PlanSliceBoundaryTransitions = 0;
|
||||
int32 PlanSliceMixedCells = 0;
|
||||
|
||||
int32 FineVerticalSliceCellY = INDEX_NONE;
|
||||
int32 FineVerticalSliceWorldY = 0;
|
||||
int32 FineVerticalImageWidth = 0;
|
||||
int32 FineVerticalImageHeight = 0;
|
||||
int32 FinePlanSliceCellZ = INDEX_NONE;
|
||||
int32 FinePlanSliceWorldZ = 0;
|
||||
int32 FinePlanImageWidth = 0;
|
||||
int32 FinePlanImageHeight = 0;
|
||||
};
|
||||
|
||||
/** Copy the exact measurement-window metadata needed by the index page. */
|
||||
@@ -93,6 +150,23 @@ VOXELFORGE_API bool VF_WriteStratePreviewCandidate(
|
||||
FVoxelStratePreviewCandidate& OutCandidate,
|
||||
FString& OutError);
|
||||
|
||||
/**
|
||||
* Attach a separately measured fine ROI to an existing candidate record.
|
||||
*
|
||||
* The caller chooses which candidates receive this call. An invalid grid is recorded as a
|
||||
* refused/not-rendered fine preview and returns true, so a MaxCells refusal remains visible in
|
||||
* the contact sheet without being mistaken for a PNG failure. The renderer consumes the exact
|
||||
* captured Air and Density arrays and never calls a sampler.
|
||||
*/
|
||||
VOXELFORGE_API bool VF_WriteStratePreviewFineCandidate(
|
||||
const FString& OutputDirectory,
|
||||
int32 CandidateIndex,
|
||||
const FVoxelStrateSampleGrid& FineGrid,
|
||||
int32 HeadroomCells,
|
||||
const FString& FineFailureReason,
|
||||
FVoxelStratePreviewCandidate& InOutCandidate,
|
||||
FString& OutError);
|
||||
|
||||
/** Write one self-contained, offline contact sheet for all candidate records. */
|
||||
VOXELFORGE_API bool VF_WriteStratePreviewIndex(
|
||||
const FString& OutputDirectory,
|
||||
|
||||
Reference in new issue
Block a user