feat(explore): a commandlet -- 91x faster, and the first real PICTURE

Every experiment in this project cost a build + full-suite cycle. The
landing task took 52 of them over ~9 hours. And nobody could SEE the world:
every visual judgement all session was made on density SLICES -- diagrams,
not pictures. That is how a floor heaving 1.4 m every two metres got
described as "roughness visible but modest" while Jahni spotted it in
thirty seconds by walking it.

-run=VoxelForgeExplore, editor-only module, three modes, one build many runs:

  render  raymarched PNGs, fixed 0.25-voxel step + 10 bisection iterations
          (NOT sphere tracing -- the op stack yields density, not a signed
          distance, so a long step tunnels through walls). Three views, each
          with a 1.76 m human marker in frame, because every scale mistake
          here came from numbers with no body beside them.
  walk    an agent using the EXISTING VF player-fit stencil, never its own
          movement model -- two definitions of "can a player stand here"
          would drift and we could not tell which was lying.
  export  OBJ through the canonical UVoxelMarchingCubesMesher. No second
          mesher; a forked meshing invariant is the bug this project keeps
          paying for. 23.2 MB, 106,762 verts, 200,842 tris for 128^3.

⭐ THE NUMBER THAT JUSTIFIES IT: the full automation run takes 4,396 s
(73m16s). The commandlet does all three modes in 48.1 s wall, 5.9 s of
which is engine startup. ~91x faster. And I had guessed the suite cost
~90 s -- it is 73 MINUTES. The iteration loop was far more expensive than
I knew.

First real walk metrics on the Maze (seed 0, slot 4):
  distance 214 m · tortuosity 4.98 · dead ends 0.935/100 m
  narrow-gap events 0 · reachable fit fraction 3.01%
  result NotConnectedAtThisResolution
Tortuosity ~5 is a genuinely winding route, which is right for a maze.
3% reachable is not.

Editor Development build clean. SHIPPING build clean with the receipt
containing VoxelForge and NOT VoxelForgeEditor -- the instrument cannot
reach a packaged game. Automation 32/32, 0 failed.

⚠️ Codex initially shipped this UNBUILT, and correctly: I omitted the
## Validation section from that spec, so it fell back to CLAUDE.md's
"NEVER build or compile". 100+ compile errors from three causes -- bare
TCondensedJsonPrintPolicy and TJsonWriter needing their template
arguments, and a missing UObject/Package.h. That include is the SECOND
miss this session, so CODEMAP §6 now carries the gotcha.

⚠️ Unrelated, found in passing: VoxelForge.uplugin does not declare
RealtimeMeshComponent although the runtime module uses it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K8V9my1PL68pmQb2gutZZV
This commit is contained in:
Fr0zkaandClaude Opus 5 committed 2026-09-07 04:07:07 +02:00
1 parent 413c15de24
commit bd547c35c4
9 files changed
+2327 -24

No files matched your search

+431 -23
View File
@@ -1338,6 +1338,37 @@ namespace VoxelStrateMeasurePrivate
}
}
void BuildPlayerFitConnectivityMetrics(
const FSampleGrid& Grid,
const FVoxelStrateMeasureSettings& Settings,
FVoxelStrateMetrics& OutMetrics,
TArray<uint8>& OutPlayerFitMask)
{
OutMetrics = FVoxelStrateMetrics();
OutMetrics.ResolvedMarginVoxels = Grid.ResolvedMarginVoxels;
OutMetrics.SampledMinZ = Grid.SampledMinZ;
OutMetrics.SampledMaxZ = Grid.SampledMaxZ;
OutMetrics.SampledNumX = Grid.NumX;
OutMetrics.SampledNumY = Grid.NumY;
OutMetrics.SampledNumZ = Grid.NumZ;
OutMetrics.SampledMinX = Grid.MinX;
OutMetrics.SampledMaxX = Grid.MaxX;
OutMetrics.SampledMinY = Grid.MinY;
OutMetrics.SampledMaxY = Grid.MaxY;
OutMetrics.NumSampled = Grid.CellCount;
for (const uint8 bAir : Grid.Air)
{
OutMetrics.NumAir += bAir != 0u ? 1 : 0;
}
OutMetrics.NumSolid = static_cast<int64>(Grid.CellCount) - OutMetrics.NumAir;
OutMetrics.AirFraction = OutMetrics.NumSampled > 0
? static_cast<float>(static_cast<double>(OutMetrics.NumAir)
/ static_cast<double>(OutMetrics.NumSampled))
: 0.0f;
BuildPlayerFitMetrics(Grid, Settings, OutMetrics, &OutPlayerFitMask);
OutMetrics.bValid = OutMetrics.NumSampled > 0;
}
float MedianFloat(TArray<float>& Values)
{
if (Values.Num() == 0) return 0.0f;
@@ -1948,7 +1979,6 @@ namespace VoxelStrateMeasurePrivate
bOutStartSnapped = false;
bOutGoalSnapped = false;
OutNumRouteRetries = 0;
const EEndpointCellResult StartCell = ResolveEndpointCell(
Grid, AVoxel, OutStart, bOutStartSnapped, EligibilityMask);
const EEndpointCellResult GoalCell = ResolveEndpointCell(
@@ -2243,28 +2273,7 @@ namespace VoxelStrateMeasurePrivate
// Manhattan feature transform here: those values belong to the separate legacy
// measurement pass, and recomputing them for every fine candidate made the gate
// needlessly expensive without changing its answer.
PlayerMetrics.ResolvedMarginVoxels = Grid.ResolvedMarginVoxels;
PlayerMetrics.SampledMinZ = Grid.SampledMinZ;
PlayerMetrics.SampledMaxZ = Grid.SampledMaxZ;
PlayerMetrics.SampledNumX = Grid.NumX;
PlayerMetrics.SampledNumY = Grid.NumY;
PlayerMetrics.SampledNumZ = Grid.NumZ;
PlayerMetrics.SampledMinX = Grid.MinX;
PlayerMetrics.SampledMaxX = Grid.MaxX;
PlayerMetrics.SampledMinY = Grid.MinY;
PlayerMetrics.SampledMaxY = Grid.MaxY;
PlayerMetrics.NumSampled = Grid.CellCount;
for (const uint8 bAir : Grid.Air)
{
PlayerMetrics.NumAir += bAir != 0u ? 1 : 0;
}
PlayerMetrics.NumSolid = static_cast<int64>(Grid.CellCount) - PlayerMetrics.NumAir;
PlayerMetrics.AirFraction = PlayerMetrics.NumSampled > 0
? static_cast<float>(static_cast<double>(PlayerMetrics.NumAir)
/ static_cast<double>(PlayerMetrics.NumSampled))
: 0.0f;
BuildPlayerFitMetrics(Grid, Settings, PlayerMetrics, &PlayerFitMask);
PlayerMetrics.bValid = PlayerMetrics.NumSampled > 0;
BuildPlayerFitConnectivityMetrics(Grid, Settings, PlayerMetrics, PlayerFitMask);
if (OutPlayerMetrics != nullptr)
{
*OutPlayerMetrics = PlayerMetrics;
@@ -2314,6 +2323,227 @@ namespace VoxelStrateMeasurePrivate
}
return Result;
}
int32 GetSixNeighbour(const FSampleGrid& Grid, int32 Cell, int32 Direction)
{
int32 X = 0;
int32 Y = 0;
int32 Z = 0;
DecodeIndex(Grid, Cell, X, Y, Z);
switch (Direction)
{
case 0: return X + 1 < Grid.NumX ? Grid.Index(X + 1, Y, Z) : INDEX_NONE;
case 1: return X > 0 ? Grid.Index(X - 1, Y, Z) : INDEX_NONE;
case 2: return Y + 1 < Grid.NumY ? Grid.Index(X, Y + 1, Z) : INDEX_NONE;
case 3: return Y > 0 ? Grid.Index(X, Y - 1, Z) : INDEX_NONE;
case 4: return Z + 1 < Grid.NumZ ? Grid.Index(X, Y, Z + 1) : INDEX_NONE;
case 5: return Z > 0 ? Grid.Index(X, Y, Z - 1) : INDEX_NONE;
default: return INDEX_NONE;
}
}
bool IsEligiblePlayerFitCell(
const FSampleGrid& Grid,
const TArray<uint8>& PlayerFitMask,
int32 Cell)
{
return Cell >= 0
&& Cell < Grid.CellCount
&& Grid.Air[Cell] != 0u
&& PlayerFitMask[Cell] != 0u;
}
int32 CountPlayerFitNeighbours(
const FSampleGrid& Grid,
const TArray<uint8>& PlayerFitMask,
int32 Cell)
{
int32 Count = 0;
for (int32 Direction = 0; Direction < 6; ++Direction)
{
if (IsEligiblePlayerFitCell(
Grid, PlayerFitMask, GetSixNeighbour(Grid, Cell, Direction)))
{
++Count;
}
}
return Count;
}
int32 LocalAirSpanVoxels(
const FSampleGrid& Grid,
int32 Cell,
int32 Axis,
int32 MaxRelevantSpanVoxels)
{
if (Cell < 0 || Cell >= Grid.CellCount || Grid.Air[Cell] == 0u)
{
return 0;
}
int32 CentreX = 0;
int32 CentreY = 0;
int32 CentreZ = 0;
DecodeIndex(Grid, Cell, CentreX, CentreY, CentreZ);
int32 Span = 1;
bool bTouchesWindowBoundary = false;
for (const int32 Sign : { -1, 1 })
{
int32 Offset = 1;
for (;; ++Offset)
{
if (Span >= MaxRelevantSpanVoxels)
{
return Span;
}
const int32 X = Axis == 0 ? CentreX + Sign * Offset : CentreX;
const int32 Y = Axis == 1 ? CentreY + Sign * Offset : CentreY;
const int32 Z = CentreZ;
if (X < 0 || X >= Grid.NumX || Y < 0 || Y >= Grid.NumY)
{
bTouchesWindowBoundary = true;
break;
}
if (Grid.Air[Grid.Index(X, Y, Z)] == 0u)
{
break;
}
++Span;
}
}
// A window edge is a lower bound, not a solid wall. Do not label a corridor narrow merely
// because the fitted diagnostic box clipped the air span before reaching rock.
return bTouchesWindowBoundary ? MAX_int32 : Span * Grid.SampleStep;
}
float LocalWidestHorizontalAirSpanVoxels(
const FSampleGrid& Grid,
int32 Cell,
float NarrowGapThresholdVoxels)
{
const int32 MaxRelevantSpanVoxels = FMath::Max(
1,
FMath::CeilToInt(NarrowGapThresholdVoxels));
return static_cast<float>(FMath::Max(
LocalAirSpanVoxels(Grid, Cell, 0, MaxRelevantSpanVoxels),
LocalAirSpanVoxels(Grid, Cell, 1, MaxRelevantSpanVoxels)));
}
void WalkPlayerFitGraph(
const FSampleGrid& Grid,
const TArray<uint8>& PlayerFitMask,
int32 Start,
int32 Goal,
float NarrowGapThresholdVoxels,
FVoxelPlayerFitWalkReport& InOutReport)
{
if (!IsEligiblePlayerFitCell(Grid, PlayerFitMask, Start)
|| !IsEligiblePlayerFitCell(Grid, PlayerFitMask, Goal))
{
return;
}
TArray<uint8> Visited;
Visited.Init(0u, Grid.CellCount);
TArray<uint8> NarrowCell;
NarrowCell.Init(255u, Grid.CellCount);
TArray<int32> Stack;
TArray<int32> NextDirection;
// Reserve the full bounded capacity up front. TArray growth slack is otherwise allowed
// to exceed the walker's stated memory budget while the DFS discovers a large component.
Stack.Reserve(Grid.CellCount);
NextDirection.Reserve(Grid.CellCount);
Stack.Add(Start);
NextDirection.Add(0);
Visited[Start] = 1u;
auto RecordTraversal = [&](int32 From, int32 To)
{
++InOutReport.AgentGraphTraversals;
auto IsNarrowCell = [&](int32 Cell)
{
if (NarrowCell[Cell] == 255u)
{
NarrowCell[Cell] = LocalWidestHorizontalAirSpanVoxels(
Grid, Cell, NarrowGapThresholdVoxels)
< NarrowGapThresholdVoxels ? 1u : 0u;
}
return NarrowCell[Cell] != 0u;
};
const bool bNarrow = IsNarrowCell(From) || IsNarrowCell(To);
if (bNarrow)
{
++InOutReport.NarrowGapTraversals;
}
};
while (Stack.Num() > 0)
{
const int32 Current = Stack.Last();
if (Current == Goal)
{
InOutReport.bCanReachDeparture = true;
break;
}
bool bAdvanced = false;
while (NextDirection.Last() < 6)
{
const int32 Direction = NextDirection.Last()++;
const int32 Neighbour = GetSixNeighbour(Grid, Current, Direction);
if (!IsEligiblePlayerFitCell(Grid, PlayerFitMask, Neighbour)
|| Visited[Neighbour] != 0u)
{
continue;
}
Visited[Neighbour] = 1u;
RecordTraversal(Current, Neighbour);
Stack.Add(Neighbour);
NextDirection.Add(0);
bAdvanced = true;
break;
}
if (bAdvanced)
{
continue;
}
// A degree-one pose is an actual topological dead end. A higher-degree pose with all
// branches already visited is merely a DFS backtrack through a loop/junction.
if (Current != Start && Current != Goal
&& CountPlayerFitNeighbours(Grid, PlayerFitMask, Current) <= 1)
{
++InOutReport.DeadEndsEncountered;
}
Stack.Pop();
NextDirection.Pop();
if (Stack.Num() > 0)
{
RecordTraversal(Current, Stack.Last());
}
}
const float DistanceMeters = static_cast<float>(InOutReport.AgentGraphTraversals)
* FVoxelPlayerCapsuleConstants::VoxelSizeMeters;
InOutReport.DistanceTravelledMeters = DistanceMeters;
InOutReport.NarrowGapFraction = InOutReport.AgentGraphTraversals > 0
? static_cast<float>(static_cast<double>(InOutReport.NarrowGapTraversals)
/ static_cast<double>(InOutReport.AgentGraphTraversals))
: 0.0f;
InOutReport.NarrowGapEventsPer100m = DistanceMeters > KINDA_SMALL_NUMBER
? static_cast<float>(static_cast<double>(InOutReport.NarrowGapTraversals)
* 100.0 / static_cast<double>(DistanceMeters))
: 0.0f;
InOutReport.DeadEndsPer100m = DistanceMeters > KINDA_SMALL_NUMBER
? static_cast<float>(static_cast<double>(InOutReport.DeadEndsEncountered)
* 100.0 / static_cast<double>(DistanceMeters))
: 0.0f;
InOutReport.DeadEndsEncountered = FMath::Max<int64>(
InOutReport.DeadEndsEncountered, 0);
}
}
FVoxelStrateMetrics VF_MeasureStrate(
@@ -2587,4 +2817,182 @@ FVoxelConnectivityDiagnostics VF_DiagnosePlayerFitConnectivityWithSampler(
return Result;
}
bool VF_MeasurePlayerFitWalkWithSampler(
const IVoxelStrateDensitySampler& Sampler,
int32 StrateBottomWorldZ,
int32 StrateTopWorldZ,
float BoundarySealThickness,
const FVector& AVoxel,
const FVector& BVoxel,
const FVoxelStrateMeasureSettings& Settings,
FVoxelPlayerFitWalkReport& OutReport)
{
using namespace VoxelStrateMeasurePrivate;
OutReport = FVoxelPlayerFitWalkReport();
OutReport.CapsuleRadiusMeters = Settings.PlayerCapsuleRadiusVoxels
* FVoxelPlayerCapsuleConstants::VoxelSizeMeters;
OutReport.CapsuleWidthMeters = 2.0f * OutReport.CapsuleRadiusMeters;
OutReport.CapsuleHeightMeters = 2.0f * Settings.PlayerCapsuleHalfHeightVoxels
* FVoxelPlayerCapsuleConstants::VoxelSizeMeters;
OutReport.NarrowGapThresholdMeters = 1.5f * OutReport.CapsuleWidthMeters;
OutReport.NarrowGapDefinition = TEXT(
"widest axis-aligned horizontal air span at each traversed fit-graph cell");
const bool bFiniteEndpoints = FMath::IsFinite(AVoxel.X)
&& FMath::IsFinite(AVoxel.Y)
&& FMath::IsFinite(AVoxel.Z)
&& FMath::IsFinite(BVoxel.X)
&& FMath::IsFinite(BVoxel.Y)
&& FMath::IsFinite(BVoxel.Z);
if (!bFiniteEndpoints)
{
OutReport.RefusalReason = TEXT("Arrival and departure points must be finite.");
return false;
}
if (Settings.MaxRouteRetries < 0)
{
OutReport.RefusalReason = TEXT("MaxRouteRetries cannot be negative.");
return false;
}
if (Settings.SampleStep != 1)
{
OutReport.RefusalReason = TEXT(
"Player-fit walk refused: exact capsule fit requires SampleStep=1.");
return false;
}
FSampleGrid Grid;
FString RefusalReason;
if (!BuildSampleGrid(
nullptr,
nullptr,
&Sampler,
INDEX_NONE,
StrateBottomWorldZ,
StrateTopWorldZ,
BoundarySealThickness,
true,
Settings,
true,
Grid,
RefusalReason))
{
OutReport.RefusalReason = RefusalReason;
return false;
}
TArray<uint8> PlayerFitMask;
int64 NumPlayerFitCells = 0;
if (!BuildPlayerFitMask(
Grid,
Settings,
PlayerFitMask,
NumPlayerFitCells,
RefusalReason))
{
OutReport.RefusalReason = RefusalReason;
return false;
}
int32 Start = INDEX_NONE;
int32 Goal = INDEX_NONE;
int32 NumRouteRetries = 0;
const EVoxelConnectivityResult Result = EvaluateConnectivityOnGrid(
nullptr,
&Sampler,
Grid,
AVoxel,
BVoxel,
Start,
Goal,
OutReport.bStartSnapped,
OutReport.bGoalSnapped,
NumRouteRetries,
Settings.MaxRouteRetries,
&PlayerFitMask);
FVoxelConnectivityDiagnostics Diagnostics;
PopulateConnectivityDiagnostics(
Grid,
Start,
Goal,
Result,
OutReport.bStartSnapped,
OutReport.bGoalSnapped,
NumRouteRetries,
Diagnostics,
&PlayerFitMask);
OutReport.bValid = true;
OutReport.Result = Result;
OutReport.NumRouteRetries = NumRouteRetries;
OutReport.SampledNumX = Grid.NumX;
OutReport.SampledNumY = Grid.NumY;
OutReport.SampledNumZ = Grid.NumZ;
OutReport.SampledMinZ = Grid.SampledMinZ;
OutReport.SampledMaxZ = Grid.SampledMaxZ;
OutReport.SampledMinX = Grid.MinX;
OutReport.SampledMaxX = Grid.MaxX;
OutReport.SampledMinY = Grid.MinY;
OutReport.SampledMaxY = Grid.MaxY;
OutReport.PlayerFitVolumeCells = NumPlayerFitCells;
OutReport.ReachablePlayerFitCells = FMath::Max<int64>(
Diagnostics.StartComponentCells, 0);
OutReport.ReachablePlayerFitFraction = OutReport.PlayerFitVolumeCells > 0
? static_cast<float>(static_cast<double>(OutReport.ReachablePlayerFitCells)
/ static_cast<double>(OutReport.PlayerFitVolumeCells))
: 0.0f;
OutReport.StraightLineMeters = FVector::Dist(AVoxel, BVoxel)
* FVoxelPlayerCapsuleConstants::VoxelSizeMeters;
if (Start >= 0 && Goal >= 0)
{
const float NarrowGapThresholdVoxels = OutReport.NarrowGapThresholdMeters
/ FVoxelPlayerCapsuleConstants::VoxelSizeMeters;
WalkPlayerFitGraph(
Grid,
PlayerFitMask,
Start,
Goal,
NarrowGapThresholdVoxels,
OutReport);
}
OutReport.bCanReachDeparture = Result == EVoxelConnectivityResult::Connected;
OutReport.Tortuosity = OutReport.StraightLineMeters > KINDA_SMALL_NUMBER
? OutReport.DistanceTravelledMeters / OutReport.StraightLineMeters
: 0.0f;
OutReport.DeadEndsEncountered = FMath::Max<int64>(
OutReport.DeadEndsEncountered, 0);
OutReport.NarrowGapEventsPer100m = OutReport.DistanceTravelledMeters
> KINDA_SMALL_NUMBER
? static_cast<float>(static_cast<double>(OutReport.NarrowGapTraversals)
* 100.0 / static_cast<double>(OutReport.DistanceTravelledMeters))
: 0.0f;
OutReport.DeadEndsPer100m = OutReport.DistanceTravelledMeters
> KINDA_SMALL_NUMBER
? static_cast<float>(static_cast<double>(OutReport.DeadEndsEncountered)
* 100.0 / static_cast<double>(OutReport.DistanceTravelledMeters))
: 0.0f;
if (Result == EVoxelConnectivityResult::StartCellNotPlayerFit)
{
OutReport.RefusalReason = TEXT("arrival endpoint is not a player-fitting floor cell");
}
else if (Result == EVoxelConnectivityResult::GoalCellNotPlayerFit)
{
OutReport.RefusalReason = TEXT("departure endpoint is not a player-fitting floor cell");
}
else if (Result == EVoxelConnectivityResult::OutOfWindow)
{
OutReport.RefusalReason = TEXT("arrival or departure point is outside the fitted walk window");
}
else
{
OutReport.RefusalReason = Diagnostics.RefusalReason;
}
return true;
}
#endif // WITH_EDITOR — measurement never ships
@@ -255,6 +255,61 @@ struct VOXELFORGE_API FVoxelConnectivityDiagnostics
float GoalToStartComponentDistanceCells = -1.0f;
};
/**
* Experiential traversal facts over the exact VF player-fit mask.
*
* This is intentionally a report, not a new movement implementation. The mask is built by the
* same direct capsule stencil used by VF_DiagnosePlayerFitConnectivityWithSampler; the explorer's
* deterministic graph walk only instruments that already-resolved six-neighbour pose graph.
* Distances are metres so the result can be read beside a human-scale screenshot.
*/
struct VOXELFORGE_API FVoxelPlayerFitWalkReport
{
bool bValid = false;
FString RefusalReason;
EVoxelConnectivityResult Result = EVoxelConnectivityResult::OutOfWindow;
bool bCanReachDeparture = false;
bool bStartSnapped = false;
bool bGoalSnapped = false;
int32 NumRouteRetries = 0;
int64 PlayerFitVolumeCells = 0;
int64 ReachablePlayerFitCells = 0;
float ReachablePlayerFitFraction = 0.0f;
int32 SampledNumX = 0;
int32 SampledNumY = 0;
int32 SampledNumZ = 0;
int32 SampledMinZ = 0;
int32 SampledMaxZ = 0;
float SampledMinX = 0.0f;
float SampledMaxX = 0.0f;
float SampledMinY = 0.0f;
float SampledMaxY = 0.0f;
// The agent follows a fixed-order depth-first walk over the already-resolved fit graph. A
// branch edge is counted twice when the agent backtracks, which makes this an instrument of
// traversal experience rather than a shortest-path length.
int64 AgentGraphTraversals = 0;
int64 DeadEndsEncountered = 0;
float DeadEndsPer100m = 0.0f;
float DistanceTravelledMeters = 0.0f;
float StraightLineMeters = 0.0f;
float Tortuosity = 0.0f;
// A narrow-gap event is a traversed graph edge touching a route cell whose widest
// axis-aligned horizontal air span is below the threshold. This deliberately reports the
// operational proxy instead of pretending the voxel mask contains an oriented corridor width.
int64 NarrowGapTraversals = 0;
float NarrowGapFraction = 0.0f;
float NarrowGapThresholdMeters = 0.0f;
float NarrowGapEventsPer100m = 0.0f;
float CapsuleRadiusMeters = 0.0f;
float CapsuleWidthMeters = 0.0f;
float CapsuleHeightMeters = 0.0f;
FString NarrowGapDefinition;
};
/**
* Measure one strate in actor-space voxel coordinates.
*
@@ -395,3 +450,18 @@ VOXELFORGE_API FVoxelConnectivityDiagnostics VF_DiagnosePlayerFitConnectivityWit
const FVector& BVoxel,
const FVoxelStrateMeasureSettings& Settings,
FVoxelStrateMetrics* OutPlayerMetrics = nullptr);
/**
* Instrument a deterministic agent over the same exact player-fit graph used by the route gate.
* The return value is false only when the bounded fine grid/stencil could not be built; a valid
* report may still say NotConnectedAtThisResolution or another explicit connectivity result.
*/
VOXELFORGE_API bool VF_MeasurePlayerFitWalkWithSampler(
const IVoxelStrateDensitySampler& Sampler,
int32 StrateBottomWorldZ,
int32 StrateTopWorldZ,
float BoundarySealThickness,
const FVector& AVoxel,
const FVector& BVoxel,
const FVoxelStrateMeasureSettings& Settings,
FVoxelPlayerFitWalkReport& OutReport);
@@ -0,0 +1,11 @@
#include "CoreMinimal.h"
#include "Modules/ModuleManager.h"
class FVoxelForgeEditorModule final : public IModuleInterface
{
public:
virtual void StartupModule() override {}
virtual void ShutdownModule() override {}
};
IMPLEMENT_MODULE(FVoxelForgeEditorModule, VoxelForgeEditor)
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,16 @@
#pragma once
#include "Commandlets/Commandlet.h"
#include "VoxelForgeExploreCommandlet.generated.h"
/** Headless, editor-only explorer for authoritative density, traversal, and mesh output. */
UCLASS()
class VOXELFORGEEDITOR_API UVoxelForgeExploreCommandlet : public UCommandlet
{
GENERATED_BODY()
public:
UVoxelForgeExploreCommandlet();
virtual int32 Main(const FString& Params) override;
};
@@ -0,0 +1,29 @@
// Editor-only commandlets and offline explorers. This module is not linked by Game/Shipping.
using UnrealBuildTool;
public class VoxelForgeEditor : ModuleRules
{
public VoxelForgeEditor(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
// The commandlet's PNG shading, metre conversions, and JSON fields are part of its
// deterministic artifact contract, so keep the editor-side arithmetic on the same precise
// floating-point model as the authoritative Runtime module.
FPSemantics = FPSemanticsMode.Precise;
PublicDependencyModuleNames.AddRange(new string[]
{
"Core",
"CoreUObject",
"Engine",
});
PrivateDependencyModuleNames.AddRange(new string[]
{
"Json",
"ImageWrapper",
"VoxelForge",
});
}
}