Native FI binds and its own route/clearance/mouth-support checks pass on seeds 0 and 10, but 14/16 seeds have no island arrangement that clears the fixed passage tubes and reaches both mouth heights; seed 10 has one composed collision; ripple 6-7 cm vs the 3.5 cm limit on both upper passages. Third failure of the 'islands adapt to fixed tunnels' premise (L133 x2, L140). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
1280 lines
72 KiB
Diff
1280 lines
72 KiB
Diff
diff --git a/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp b/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp
|
|
index 5e66412..8d21685 100644
|
|
--- a/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp
|
|
+++ b/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp
|
|
@@ -892,6 +892,7 @@ bool BuildSurfaceStratumPlan(const UVoxelStrateManager* StrateManager,
|
|
|
|
bool BuildFloatingIslandStratumPlan(const UVoxelStrateManager* StrateManager,
|
|
const FStrateSlot& Slot, VoxelForgeCore::WorldAssemblyManifest& Manifest,
|
|
+ const VoxelForgeCore::ResolvedLayoutSnapshotV3& Layout,
|
|
double CoreVoxelSizeCm, std::int64_t LayoutChunkWidthSubvoxels, FString& OutError)
|
|
{
|
|
using namespace VoxelForgeCore;
|
|
@@ -903,7 +904,8 @@ bool BuildFloatingIslandStratumPlan(const UVoxelStrateManager* StrateManager,
|
|
IslandPlan.topChunkZ = Slot.TopChunkZ;
|
|
IslandPlan.bottomChunkZ = Slot.BottomChunkZ;
|
|
auto& Recipe = IslandPlan.manifest.recipe;
|
|
- Recipe.geometryVersion = 8;
|
|
+ Recipe.geometryVersion = 10;
|
|
+ Recipe.primaryIslandCount = 9;
|
|
Recipe.terrainFollowingRoute = static_cast<std::uint32_t>(Passage.Style) == 0;
|
|
Recipe.layoutChunkWidthSubvoxels = LayoutChunkWidthSubvoxels;
|
|
Recipe.seed = static_cast<std::uint32_t>(StrateManager->GetWorldSeed());
|
|
@@ -929,13 +931,24 @@ bool BuildFloatingIslandStratumPlan(const UVoxelStrateManager* StrateManager,
|
|
/ Recipe.ownerToCoreScaleDenominator;
|
|
Recipe.spacingSubvoxels = WorldAssemblyFixed(Params.IslandSpacing * OwnerToCoreScale);
|
|
Recipe.minimumRadiusSubvoxels = WorldAssemblyFixed(Params.IslandMinRadius * OwnerToCoreScale);
|
|
- Recipe.maximumRadiusSubvoxels = WorldAssemblyFixed(Params.IslandMaxRadius * OwnerToCoreScale);
|
|
+ Recipe.maximumRadiusSubvoxels = WorldAssemblyFixed(28.0 * OwnerToCoreScale);
|
|
Recipe.thicknessRatioQ16 = WorldAssemblyQ16(Params.ThicknessRatio);
|
|
Recipe.verticalJitterQ16 = WorldAssemblyQ16(Params.VerticalJitter);
|
|
Recipe.topFlattenQ16 = WorldAssemblyQ16(Params.TopFlatten);
|
|
- Recipe.roughnessSubvoxels = WorldAssemblyFixed(Params.SurfaceRoughness * OwnerToCoreScale);
|
|
- Recipe.blendRadiusSubvoxels = WorldAssemblyFixed(Params.SDFBlendRadius * OwnerToCoreScale);
|
|
+ Recipe.roughnessSubvoxels = std::min(WorldAssemblyFixed(
|
|
+ Params.SurfaceRoughness * OwnerToCoreScale), WorldAssemblySubvoxelsPerVoxel / 2);
|
|
+ Recipe.blendRadiusSubvoxels = 2 * WorldAssemblySubvoxelsPerVoxel;
|
|
+ Recipe.contourWarpSubvoxels = WorldAssemblySubvoxelsPerVoxel;
|
|
Recipe.boundarySealSubvoxels = WorldAssemblyFixed(Params.BoundarySealThickness * OwnerToCoreScale);
|
|
+ Recipe.routeConstraints.version = 1;
|
|
+ Recipe.routeConstraints.capsuleRadiusSubvoxels = 348;
|
|
+ Recipe.routeConstraints.capsuleHeightSubvoxels = 1802;
|
|
+ Recipe.routeConstraints.maximumGradeQ16 = 17560;
|
|
+ Recipe.routeConstraints.sweepSpacingSubvoxels = 128;
|
|
+ Recipe.routeConstraints.supportProbeRadiusSubvoxels = 230;
|
|
+ Recipe.routeConstraints.supportSearchDepthSubvoxels = 512;
|
|
+ Recipe.routeConstraints.minimumClearanceSubvoxels = 128;
|
|
+ Recipe.routeConstraints.minimumSupportProbes = 3;
|
|
const auto OwnerVoxelsToCoreQ=[&](double Value)
|
|
{
|
|
return static_cast<std::int64_t>(std::llround(Value*Recipe.ownerVoxelSizeMicrons
|
|
@@ -944,40 +957,21 @@ bool BuildFloatingIslandStratumPlan(const UVoxelStrateManager* StrateManager,
|
|
Recipe.anchorIslandOrdinal=Recipe.primaryIslandCount-1;
|
|
Recipe.anchorPlacementRadiusSubvoxels=WorldAssemblyMicrometresToSubvoxels(
|
|
Slot.PassageDistanceMaximumMicrometres, Recipe.coreVoxelSizeMicrons);
|
|
- const std::int64_t spiralRadius=WorldAssemblyMicrometresToSubvoxels(
|
|
- Slot.PassageSpiralRadiusMicrometres, Recipe.coreVoxelSizeMicrons);
|
|
- // Reserve at least a 1.5 m mid-bore through the anchor root.
|
|
- constexpr double AnchorPassageMinimumBoreOwnerVoxels=12.0;
|
|
- const std::int64_t OwnerVoxelMicrometres = Recipe.ownerVoxelSizeMicrons;
|
|
- const std::int64_t mouthBore=std::max(
|
|
- WorldAssemblyMicrometresToSubvoxels(Slot.PassageMouthRadiusMicrometres,
|
|
- Recipe.coreVoxelSizeMicrons),
|
|
- WorldAssemblyMicrometresToSubvoxels(12 * OwnerVoxelMicrometres,
|
|
- Recipe.coreVoxelSizeMicrons));
|
|
- const std::int64_t bore=std::max(
|
|
- WorldAssemblyMicrometresToSubvoxels(Slot.PassageMidRadiusMicrometres,
|
|
- Recipe.coreVoxelSizeMicrons),
|
|
- WorldAssemblyMicrometresToSubvoxels(
|
|
- static_cast<std::int64_t>(AnchorPassageMinimumBoreOwnerVoxels * OwnerVoxelMicrometres),
|
|
- Recipe.coreVoxelSizeMicrons));
|
|
- const std::int64_t wallReserve=WorldAssemblyMicrometresToSubvoxels(
|
|
- 12 * OwnerVoxelMicrometres, Recipe.coreVoxelSizeMicrons);
|
|
- const std::int64_t FourOwnerVoxelsSubvoxels=WorldAssemblyMicrometresToSubvoxels(
|
|
- 4 * OwnerVoxelMicrometres, Recipe.coreVoxelSizeMicrons);
|
|
- Recipe.anchorMouthOffsetSubvoxels=spiralRadius+mouthBore+FourOwnerVoxelsSubvoxels;
|
|
- const std::int64_t excursion=std::max(
|
|
- WorldAssemblyMicrometresToSubvoxels(Slot.PassageWanderMicrometres,
|
|
- Recipe.coreVoxelSizeMicrons), spiralRadius);
|
|
- Recipe.anchorRootRadiusSubvoxels=excursion+bore+wallReserve;
|
|
- Recipe.anchorIslandMinimumRadiusSubvoxels=std::max(Recipe.minimumRadiusSubvoxels,
|
|
- Recipe.anchorMouthOffsetSubvoxels+mouthBore+FourOwnerVoxelsSubvoxels);
|
|
+ Recipe.anchorMouthOffsetSubvoxels = OwnerVoxelsToCoreQ(4.0);
|
|
+ Recipe.anchorRootRadiusSubvoxels = std::max(OwnerVoxelsToCoreQ(16.0),
|
|
+ Recipe.anchorMouthOffsetSubvoxels + Recipe.routeConstraints.capsuleRadiusSubvoxels
|
|
+ + WorldAssemblySubvoxelsPerVoxel);
|
|
+ Recipe.anchorIslandMinimumRadiusSubvoxels = std::max(Recipe.maximumRadiusSubvoxels,
|
|
+ Recipe.anchorMouthOffsetSubvoxels + Recipe.routeConstraints.capsuleRadiusSubvoxels
|
|
+ + WorldAssemblySubvoxelsPerVoxel);
|
|
Recipe.anchorRootBottomZSubvoxels=Recipe.layoutMinimumZSubvoxels
|
|
+Recipe.boundarySealSubvoxels;
|
|
std::string PlanError;
|
|
- const std::optional<FloatingIslandManifestV1> Planned = PlanFloatingIslandCluster(Recipe, PlanError);
|
|
+ const std::optional<FloatingIslandManifestV1> Planned =
|
|
+ PlanFloatingIslandClusterAroundPassages(Recipe, Layout, Slot.StrateIndex, PlanError);
|
|
if (!Planned)
|
|
{
|
|
- OutError = FString::Printf(TEXT("FloatingIslands automatic plan for strate %d failed: %s"),
|
|
+ OutError = FString::Printf(TEXT("FloatingIslands passage-aware plan for strate %d failed: %s"),
|
|
Slot.StrateIndex, UTF8_TO_TCHAR(PlanError.c_str()));
|
|
return false;
|
|
}
|
|
@@ -1671,18 +1665,6 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
|
|
Generated.plan.strates.push_back(std::move(Stratum));
|
|
++BuiltSurfaceCount;
|
|
}
|
|
- // Native floating-island evaluation remains an explicit imported-plan opt-in.
|
|
- // The default owner path uses the legacy generator for this slot; emitting an
|
|
- // automatic FI plan here silently switches density dispatch and changes the
|
|
- // shared passage mouth before the owner has opted into that field.
|
|
- else if (Type == ECaveGeneratorType::FloatingIslands && bImportedOverride)
|
|
- {
|
|
- if (!BuildFloatingIslandStratumPlan(StrateManager, Slot, Generated,
|
|
- CoreVoxelSizeCm, LayoutChunkWidthSubvoxels, OutError))
|
|
- {
|
|
- return false;
|
|
- }
|
|
- }
|
|
}
|
|
if (BuiltSurfaceCount > 1)
|
|
{
|
|
@@ -2031,6 +2013,98 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
|
|
{
|
|
return false;
|
|
}
|
|
+ bool bAddedFloatingIslandPlan = false;
|
|
+ for (const FStrateSlot& Slot : StrateManager->GetLayout())
|
|
+ {
|
|
+ if (Slot.Definition == nullptr
|
|
+ || StrateManager->GetGeneratorTypeForChunk(FIntVector(0, 0, Slot.TopChunkZ))
|
|
+ != ECaveGeneratorType::FloatingIslands)
|
|
+ {
|
|
+ continue;
|
|
+ }
|
|
+ const auto Existing = std::find_if(Manifest->floatingIslands.begin(),
|
|
+ Manifest->floatingIslands.end(), [&Slot](const auto& Plan)
|
|
+ { return Plan.strateIndex == Slot.StrateIndex; });
|
|
+ if (Existing != Manifest->floatingIslands.end()) continue;
|
|
+ if (!BuildFloatingIslandStratumPlan(StrateManager, Slot, *Manifest, Layout,
|
|
+ CoreVoxelSizeCm, LayoutChunkWidthSubvoxels, OutError))
|
|
+ {
|
|
+ return false;
|
|
+ }
|
|
+ bAddedFloatingIslandPlan = true;
|
|
+ }
|
|
+ if (bAddedFloatingIslandPlan)
|
|
+ {
|
|
+ if (!Manifest->finalize(CoreError))
|
|
+ {
|
|
+ OutError = FString::Printf(TEXT("Core passage-aware FloatingIslands planning failed: %s"),
|
|
+ UTF8_TO_TCHAR(CoreError.c_str()));
|
|
+ return false;
|
|
+ }
|
|
+ ManifestHash = FString::Printf(TEXT("%016llx"),
|
|
+ static_cast<unsigned long long>(Manifest->contentHash));
|
|
+ for (const auto& Plan : Manifest->floatingIslands)
|
|
+ {
|
|
+ auto Stratum = std::find_if(Layout.strates.begin(), Layout.strates.end(),
|
|
+ [&Plan](const auto& Item) { return Item.strateIndex == Plan.strateIndex; });
|
|
+ if (Stratum == Layout.strates.end())
|
|
+ {
|
|
+ OutError = FString::Printf(TEXT("FloatingIslands strate %d disappeared before anchor export."),
|
|
+ Plan.strateIndex);
|
|
+ return false;
|
|
+ }
|
|
+ Stratum->recipeVersion = Plan.manifest.recipe.geometryVersion;
|
|
+ std::erase_if(Stratum->anchors, [](const auto& Anchor)
|
|
+ {
|
|
+ return Anchor.kind == "island_anchor" || Anchor.kind == "passage_anchor"
|
|
+ || Anchor.kind == "route_point";
|
|
+ });
|
|
+ const auto& NativePlan = Plan.manifest.plan;
|
|
+ const auto& NativeRecipe = Plan.manifest.recipe;
|
|
+ for (std::size_t IslandIndex = 0; IslandIndex < NativePlan.islands.size(); ++IslandIndex)
|
|
+ {
|
|
+ const auto& Island = NativePlan.islands[IslandIndex];
|
|
+ VoxelForgeCore::ResolvedLayoutAnchorV2 Anchor;
|
|
+ Anchor.kind = "island_anchor";
|
|
+ Anchor.stableId = "island/" + std::to_string(IslandIndex);
|
|
+ Anchor.units = "1/256 Core voxel";
|
|
+ Anchor.values = { Island.centerSubvoxels[0], Island.centerSubvoxels[1],
|
|
+ Island.centerSubvoxels[2], Island.radiusSubvoxels, Island.topHalfSubvoxels };
|
|
+ Stratum->anchors.push_back(std::move(Anchor));
|
|
+ if (IslandIndex == NativeRecipe.anchorIslandOrdinal)
|
|
+ {
|
|
+ VoxelForgeCore::ResolvedLayoutAnchorV2 PassageAnchor;
|
|
+ PassageAnchor.kind = "passage_anchor";
|
|
+ PassageAnchor.stableId = "passage-anchor/island/"
|
|
+ + std::to_string(IslandIndex);
|
|
+ PassageAnchor.units = "1/256 Core voxel";
|
|
+ PassageAnchor.values = { Island.centerSubvoxels[0], Island.centerSubvoxels[1],
|
|
+ NativeRecipe.anchorRootRadiusSubvoxels,
|
|
+ NativeRecipe.anchorMouthOffsetSubvoxels,
|
|
+ NativeRecipe.anchorPlacementRadiusSubvoxels,
|
|
+ NativeRecipe.anchorRootBottomZSubvoxels, Island.radiusSubvoxels };
|
|
+ if (!NativePlan.route.empty())
|
|
+ {
|
|
+ const auto& Mouth = NativePlan.route.back().positionSubvoxels;
|
|
+ PassageAnchor.values.insert(PassageAnchor.values.end(),
|
|
+ { Mouth[0], Mouth[1], Mouth[2] });
|
|
+ }
|
|
+ Stratum->anchors.push_back(std::move(PassageAnchor));
|
|
+ }
|
|
+ }
|
|
+ for (std::size_t RouteIndex = 0; RouteIndex < NativePlan.route.size(); ++RouteIndex)
|
|
+ {
|
|
+ const auto& Point = NativePlan.route[RouteIndex];
|
|
+ VoxelForgeCore::ResolvedLayoutAnchorV2 Anchor;
|
|
+ Anchor.kind = "route_point";
|
|
+ Anchor.stableId = "route/" + std::to_string(RouteIndex);
|
|
+ Anchor.units = "1/256 Core voxel";
|
|
+ Anchor.values = { Point.positionSubvoxels[0], Point.positionSubvoxels[1],
|
|
+ Point.positionSubvoxels[2], Point.radiusSubvoxels };
|
|
+ Stratum->anchors.push_back(std::move(Anchor));
|
|
+ }
|
|
+ }
|
|
+ }
|
|
std::string LayoutError;
|
|
if (!Layout.finalize(LayoutError))
|
|
{
|
|
diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h
|
|
index 8c44461..ec7cfe3 100644
|
|
--- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h
|
|
+++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h
|
|
@@ -511,7 +511,7 @@ namespace VoxelForgeCore
|
|
// the planned graph route, preserves its walk corridor through shell overlaps, and keeps
|
|
// boundary-blended host rock from widening the authored passage wall.
|
|
// v237 adds the shared Core cave-graph shaft primitive and basal terminal route.
|
|
- inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 237;
|
|
+ inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 239;
|
|
|
|
enum class FieldChannel : std::uint32_t
|
|
{
|
|
@@ -1417,6 +1417,19 @@ namespace VoxelForgeCore
|
|
bool passed = false;
|
|
};
|
|
|
|
+ struct FloatingIslandPassageClearanceAuditResult
|
|
+ {
|
|
+ std::uint64_t passageSamples = 0;
|
|
+ std::uint64_t blockedIslandSamples = 0;
|
|
+ std::uint64_t routeSamples = 0;
|
|
+ std::uint64_t blockedRouteSamples = 0;
|
|
+ std::uint32_t arrivalMouthSupportProbes = 0;
|
|
+ std::uint32_t departureMouthSupportProbes = 0;
|
|
+ std::int64_t minimumExcessClearanceSubvoxels = 0;
|
|
+ double portalExclusionMetres = 0.0;
|
|
+ bool passed = false;
|
|
+ };
|
|
+
|
|
struct FloatingIslandInfluenceBounds
|
|
{
|
|
bool finite = false;
|
|
@@ -1461,11 +1474,17 @@ namespace VoxelForgeCore
|
|
// Planner output is ordinary reviewed data; clients can load it without re-running the planner.
|
|
VFCORE_API std::optional<FloatingIslandManifestV1> PlanFloatingIslandCluster(
|
|
const FloatingIslandRecipeV1& recipe, std::string& error);
|
|
+ VFCORE_API std::optional<FloatingIslandManifestV1> PlanFloatingIslandClusterAroundPassages(
|
|
+ const FloatingIslandRecipeV1& recipe, const ResolvedLayoutSnapshotV3& layout,
|
|
+ std::int32_t strateIndex, std::string& error);
|
|
VFCORE_API FloatingIslandBoundsAuditResult AuditFloatingIslandBounds(
|
|
const FloatingIslandFieldV1& field, std::uint32_t boxCount = 256,
|
|
std::uint64_t seed = 0x49534c414e444231ull);
|
|
VFCORE_API FloatingIslandRouteAuditResult AuditFloatingIslandRoute(
|
|
const FloatingIslandFieldV1& field, std::int64_t routeOffsetSubvoxels = 0);
|
|
+ VFCORE_API FloatingIslandPassageClearanceAuditResult AuditFloatingIslandPassageClearance(
|
|
+ const FloatingIslandFieldV1& field, const ResolvedLayoutSnapshotV3& layout,
|
|
+ std::int32_t strateIndex, double portalExclusionMetres = 10.0);
|
|
VFCORE_API FloatingIslandThicknessAuditResult AuditFloatingIslandThickness(
|
|
const FloatingIslandPlanV1& plan, double minimumDepthToDiameterRatio = 0.70);
|
|
|
|
diff --git a/Standalone/Core/src/FloatingIslandsInternal.h b/Standalone/Core/src/FloatingIslandsInternal.h
|
|
index 33d97d6..58f0959 100644
|
|
--- a/Standalone/Core/src/FloatingIslandsInternal.h
|
|
+++ b/Standalone/Core/src/FloatingIslandsInternal.h
|
|
@@ -4,6 +4,7 @@
|
|
#include "VoxelForgeCore/CpuCycleCounter.h"
|
|
#include "VoxelForgeCore/FloatingIslandInstrumentation.h"
|
|
#include "VoxelForgeCore/SharedColumnCache.h"
|
|
+#include "VoxelForgeCore/PassageSpline.h"
|
|
#include "Json.h"
|
|
#include "VoxelForgeCore/VoxelForgeCore.h"
|
|
|
|
@@ -285,7 +286,7 @@ namespace floating_island_detail
|
|
? recipe.ownerVoxelSizeMicrons / ScaleGcd : 0;
|
|
const std::uint32_t ExpectedDenominator = ScaleGcd > 0
|
|
? recipe.coreVoxelSizeMicrons / ScaleGcd : 0;
|
|
- if (recipe.schemaVersion != 1 || recipe.geometryVersion < 1 || recipe.geometryVersion > 8
|
|
+ if (recipe.schemaVersion != 1 || recipe.geometryVersion < 1 || recipe.geometryVersion > 10
|
|
|| (recipe.geometryVersion == 1
|
|
&& (recipe.crustDepthBonusQ16 != 0 || recipe.rootDepthRatioQ16 != 0))
|
|
|| recipe.unitsPerVoxel != Q
|
|
@@ -1988,6 +1989,229 @@ std::string FloatingIslandManifestV1::ToJson() const
|
|
return out.str();
|
|
}
|
|
|
|
+namespace floating_island_detail
|
|
+{
|
|
+ struct FIslandPassageSpan
|
|
+ {
|
|
+ PassageSpline::FSegment segment;
|
|
+ double alongMetres = 0.0;
|
|
+ double totalMetres = 0.0;
|
|
+ double radiusSubvoxels = 0.0;
|
|
+ bool portalAtStart = false;
|
|
+ };
|
|
+
|
|
+ inline double SegmentDistanceSquared3D(const PassageSpline::FPoint& p1,
|
|
+ const PassageSpline::FPoint& q1, const PassageSpline::FPoint& p2,
|
|
+ const PassageSpline::FPoint& q2)
|
|
+ {
|
|
+ const std::array<double,3> d1={q1[0]-p1[0],q1[1]-p1[1],q1[2]-p1[2]};
|
|
+ const std::array<double,3> d2={q2[0]-p2[0],q2[1]-p2[1],q2[2]-p2[2]};
|
|
+ const std::array<double,3> r={p1[0]-p2[0],p1[1]-p2[1],p1[2]-p2[2]};
|
|
+ const auto dot=[](const auto& a,const auto& b)
|
|
+ {return a[0]*b[0]+a[1]*b[1]+a[2]*b[2];};
|
|
+ const double a=dot(d1,d1),e=dot(d2,d2),f=dot(d2,r);
|
|
+ double s=0.0,t=0.0;
|
|
+ if(a<=1.0e-18&&e<=1.0e-18)return dot(r,r);
|
|
+ if(a<=1.0e-18)t=std::clamp(f/e,0.0,1.0);
|
|
+ else
|
|
+ {
|
|
+ const double c=dot(d1,r);
|
|
+ if(e<=1.0e-18)s=std::clamp(-c/a,0.0,1.0);
|
|
+ else
|
|
+ {
|
|
+ const double b=dot(d1,d2);
|
|
+ const double denominator=a*e-b*b;
|
|
+ if(std::abs(denominator)>1.0e-18)
|
|
+ s=std::clamp((b*f-c*e)/denominator,0.0,1.0);
|
|
+ t=(b*s+f)/e;
|
|
+ if(t<0.0){t=0.0;s=std::clamp(-c/a,0.0,1.0);}
|
|
+ else if(t>1.0){t=1.0;s=std::clamp((b-c)/a,0.0,1.0);}
|
|
+ }
|
|
+ }
|
|
+ std::array<double,3> delta{};
|
|
+ for(std::size_t axis=0;axis<3;++axis)
|
|
+ delta[axis]=r[axis]+s*d1[axis]-t*d2[axis];
|
|
+ return dot(delta,delta);
|
|
+ }
|
|
+
|
|
+ inline std::vector<FIslandPassageSpan> BuildIslandPassageSpans(
|
|
+ const ResolvedLayoutSnapshotV3& layout,std::int32_t strateIndex,
|
|
+ const FloatingIslandRecipeV1& recipe,double portalExclusionMetres)
|
|
+ {
|
|
+ std::vector<FIslandPassageSpan> spans;
|
|
+ const double qPerMetre=static_cast<double>(WorldAssemblySubvoxelsPerVoxel)
|
|
+ *1000000.0/std::max<std::uint32_t>(1,recipe.coreVoxelSizeMicrons);
|
|
+ for(const auto& passage:layout.passages)
|
|
+ {
|
|
+ const bool upper=passage.upperStrateIndex==strateIndex;
|
|
+ const bool lower=passage.lowerStrateIndex==strateIndex;
|
|
+ if((!upper&&!lower)||passage.controlPoints.size()<2)continue;
|
|
+ std::vector<PassageSpline::FPoint> controls;
|
|
+ controls.reserve(passage.controlPoints.size());
|
|
+ for(const auto& point:passage.controlPoints)
|
|
+ controls.push_back({static_cast<double>(point.positionSubvoxels[0]),
|
|
+ static_cast<double>(point.positionSubvoxels[1]),
|
|
+ static_cast<double>(point.positionSubvoxels[2]),
|
|
+ static_cast<double>(point.radiusSubvoxels)});
|
|
+ const auto built=PassageSpline::BuildSegments(controls,controls.size()-1,
|
|
+ 0.25,qPerMetre,{},0.04);
|
|
+ if(built.empty())continue;
|
|
+ const double total=built.back().alongMetres+built.back().lengthMetres;
|
|
+ const auto& first=controls.front();
|
|
+ const auto& last=controls.back();
|
|
+ const auto distanceSquaredTo=[&](const PassageSpline::FPoint& point,
|
|
+ const std::array<std::int64_t,3>& mouth)
|
|
+ {
|
|
+ double result=0.0;
|
|
+ for(std::size_t axis=0;axis<3;++axis)
|
|
+ {const double delta=point[axis]-mouth[axis];result+=delta*delta;}
|
|
+ return result;
|
|
+ };
|
|
+ const bool upperAtStart=distanceSquaredTo(first,passage.upperMouthSubvoxels)
|
|
+ <=distanceSquaredTo(last,passage.upperMouthSubvoxels);
|
|
+ const bool portalAtStart=upper?upperAtStart:!upperAtStart;
|
|
+ for(const auto& segment:built)
|
|
+ {
|
|
+ const double along=segment.alongMetres+0.5*segment.lengthMetres;
|
|
+ const double portalDistance=portalAtStart?along:total-along;
|
|
+ if(portalDistance<=portalExclusionMetres)continue;
|
|
+ const PassageSpline::FPoint midpoint={
|
|
+ 0.5*(segment.a[0]+segment.b[0]),
|
|
+ 0.5*(segment.a[1]+segment.b[1]),
|
|
+ 0.5*(segment.a[2]+segment.b[2]),
|
|
+ 0.5*(segment.a[3]+segment.b[3])};
|
|
+ const double minZ=std::min(segment.a[2],segment.b[2]);
|
|
+ const double maxZ=std::max(segment.a[2],segment.b[2]);
|
|
+ if(maxZ<static_cast<double>(recipe.layoutMinimumZSubvoxels)
|
|
+ ||minZ>static_cast<double>(recipe.layoutMaximumZSubvoxels))continue;
|
|
+ spans.push_back({segment,along,total,midpoint[3],portalAtStart});
|
|
+ }
|
|
+ }
|
|
+ return spans;
|
|
+ }
|
|
+
|
|
+ inline std::array<double,4> EvaluateIslandRouteBezier(
|
|
+ const std::array<double,3>& a,const std::array<double,3>& b,
|
|
+ const std::array<double,3>& waypoint,double t)
|
|
+ {
|
|
+ const double u=1.0-t;
|
|
+ const std::array<double,4> result={
|
|
+ u*u*u*a[0]+3.0*u*u*t*waypoint[0]+3.0*u*t*t*waypoint[0]+t*t*t*b[0],
|
|
+ u*u*u*a[1]+3.0*u*u*t*waypoint[1]+3.0*u*t*t*waypoint[1]+t*t*t*b[1],
|
|
+ std::lerp(a[2],b[2],t),t};
|
|
+ return result;
|
|
+ }
|
|
+
|
|
+ struct FIslandDetourCandidate
|
|
+ {
|
|
+ std::array<std::array<std::int64_t,3>,9> centers{};
|
|
+ double horizontalLengthMetres=0.0;
|
|
+ double conservativeClearanceMetres=-std::numeric_limits<double>::infinity();
|
|
+ double waypointAngle=0.0;
|
|
+ double waypointRadiusMetres=0.0;
|
|
+ };
|
|
+
|
|
+ inline std::vector<FIslandDetourCandidate> BuildIslandDetourCandidates(
|
|
+ const FloatingIslandRecipeV1& recipe,const ResolvedLayoutPassageV1& arrival,
|
|
+ const ResolvedLayoutPassageV1& departure,
|
|
+ const std::vector<FIslandPassageSpan>& spans)
|
|
+ {
|
|
+ std::vector<FIslandDetourCandidate> candidates;
|
|
+ const double qPerMetre=static_cast<double>(WorldAssemblySubvoxelsPerVoxel)
|
|
+ *1000000.0/std::max<std::uint32_t>(1,recipe.coreVoxelSizeMicrons);
|
|
+ const std::array<double,3> a={static_cast<double>(arrival.lowerMouthSubvoxels[0]),
|
|
+ static_cast<double>(arrival.lowerMouthSubvoxels[1]),
|
|
+ static_cast<double>(arrival.lowerMouthSubvoxels[2])};
|
|
+ const std::array<double,3> b={static_cast<double>(departure.upperMouthSubvoxels[0]),
|
|
+ static_cast<double>(departure.upperMouthSubvoxels[1]),
|
|
+ static_cast<double>(departure.upperMouthSubvoxels[2])};
|
|
+ for(int angleIndex=0;angleIndex<16;++angleIndex)
|
|
+ for(const double radiusMetres:{60.0,72.0,84.0})
|
|
+ {
|
|
+ FIslandDetourCandidate candidate;
|
|
+ candidate.waypointAngle=2.0*Pi*angleIndex/16.0;
|
|
+ candidate.waypointRadiusMetres=radiusMetres;
|
|
+ const std::array<double,3> waypoint={
|
|
+ static_cast<double>(recipe.layoutOriginXSubvoxels)
|
|
+ +std::cos(candidate.waypointAngle)*radiusMetres*qPerMetre,
|
|
+ static_cast<double>(recipe.layoutOriginYSubvoxels)
|
|
+ +std::sin(candidate.waypointAngle)*radiusMetres*qPerMetre,
|
|
+ 0.5*(a[2]+b[2])};
|
|
+ constexpr std::size_t CurveSteps=512;
|
|
+ std::array<double,CurveSteps+1> arc{};
|
|
+ std::array<std::array<double,3>,CurveSteps+1> curve{};
|
|
+ auto prior=EvaluateIslandRouteBezier(a,b,waypoint,0.0);
|
|
+ curve[0]={prior[0],prior[1],prior[2]};
|
|
+ for(std::size_t n=1;n<=CurveSteps;++n)
|
|
+ {
|
|
+ const auto value=EvaluateIslandRouteBezier(a,b,waypoint,
|
|
+ static_cast<double>(n)/CurveSteps);
|
|
+ curve[n]={value[0],value[1],value[2]};
|
|
+ const double dx=(value[0]-prior[0])/qPerMetre;
|
|
+ const double dy=(value[1]-prior[1])/qPerMetre;
|
|
+ arc[n]=arc[n-1]+std::hypot(dx,dy);
|
|
+ prior=value;
|
|
+ }
|
|
+ candidate.horizontalLengthMetres=arc.back();
|
|
+ if(!(candidate.horizontalLengthMetres>1.0))continue;
|
|
+ for(std::size_t island=0;island<candidate.centers.size();++island)
|
|
+ {
|
|
+ const double target=arc.back()*island/(candidate.centers.size()-1);
|
|
+ const auto upper=std::lower_bound(arc.begin(),arc.end(),target);
|
|
+ const std::size_t upperIndex=std::min<std::size_t>(
|
|
+ CurveSteps,static_cast<std::size_t>(upper-arc.begin()));
|
|
+ const std::size_t lowerIndex=upperIndex?upperIndex-1:0;
|
|
+ const double span=arc[upperIndex]-arc[lowerIndex];
|
|
+ const double t=span>1.0e-9?(target-arc[lowerIndex])/span:0.0;
|
|
+ auto point=std::array<double,3>{
|
|
+ std::lerp(curve[lowerIndex][0],curve[upperIndex][0],t),
|
|
+ std::lerp(curve[lowerIndex][1],curve[upperIndex][1],t),
|
|
+ std::lerp(a[2],b[2],target/arc.back())};
|
|
+ if(island==0)point=a;
|
|
+ if(island+1==candidate.centers.size())point=b;
|
|
+ for(std::size_t axis=0;axis<3;++axis)
|
|
+ candidate.centers[island][axis]=RoundQ(point[axis]);
|
|
+ }
|
|
+ double minimumClearance=std::numeric_limits<double>::infinity();
|
|
+ for(std::size_t routeIndex=1;routeIndex<candidate.centers.size();++routeIndex)
|
|
+ {
|
|
+ PassageSpline::FPoint routeA{},routeB{};
|
|
+ for(std::size_t axis=0;axis<3;++axis)
|
|
+ {
|
|
+ routeA[axis]=static_cast<double>(candidate.centers[routeIndex-1][axis]);
|
|
+ routeB[axis]=static_cast<double>(candidate.centers[routeIndex][axis]);
|
|
+ }
|
|
+ for(const auto& span:spans)
|
|
+ {
|
|
+ const double distance=std::sqrt(SegmentDistanceSquared3D(
|
|
+ routeA,routeB,span.segment.a,span.segment.b))/qPerMetre;
|
|
+ const double passageRadius=span.radiusSubvoxels/qPerMetre;
|
|
+ const double capsuleRadius=0.5*recipe.routeConstraints.capsuleHeightSubvoxels/qPerMetre;
|
|
+ const double featureReach=recipe.maximumRadiusSubvoxels/qPerMetre
|
|
+ +(recipe.contourWarpSubvoxels+IslandBlendMargin(recipe)
|
|
+ +recipe.roughnessSubvoxels)/qPerMetre;
|
|
+ minimumClearance=std::min(minimumClearance,distance-passageRadius
|
|
+ -1.0533-capsuleRadius-featureReach-0.15);
|
|
+ }
|
|
+ }
|
|
+ candidate.conservativeClearanceMetres=std::isfinite(minimumClearance)
|
|
+ ?minimumClearance:1.0e6;
|
|
+ candidates.push_back(candidate);
|
|
+ }
|
|
+ std::stable_sort(candidates.begin(),candidates.end(),
|
|
+ [](const auto& left,const auto& right)
|
|
+ {
|
|
+ const bool leftSafe=left.conservativeClearanceMetres>=0.0;
|
|
+ const bool rightSafe=right.conservativeClearanceMetres>=0.0;
|
|
+ if(leftSafe!=rightSafe)return leftSafe;
|
|
+ if(left.horizontalLengthMetres!=right.horizontalLengthMetres)
|
|
+ return left.horizontalLengthMetres<right.horizontalLengthMetres;
|
|
+ return left.conservativeClearanceMetres>right.conservativeClearanceMetres;
|
|
+ });
|
|
+ return candidates;
|
|
+ }
|
|
+}
|
|
+
|
|
std::optional<FloatingIslandManifestV1> PlanFloatingIslandCluster(
|
|
const FloatingIslandRecipeV1& inputRecipe, std::string& error)
|
|
{
|
|
@@ -2264,6 +2488,210 @@ std::optional<FloatingIslandManifestV1> PlanFloatingIslandCluster(
|
|
return manifest;
|
|
}
|
|
|
|
+std::optional<FloatingIslandManifestV1> PlanFloatingIslandClusterAroundPassages(
|
|
+ const FloatingIslandRecipeV1& inputRecipe,const ResolvedLayoutSnapshotV3& layout,
|
|
+ std::int32_t strateIndex,std::string& error)
|
|
+{
|
|
+ using namespace floating_island_detail;
|
|
+ FloatingIslandRecipeV1 recipe=inputRecipe;
|
|
+ if(recipe.routeConstraints.version==0)
|
|
+ {
|
|
+ recipe.routeConstraints.version=1;
|
|
+ recipe.routeConstraints.capsuleRadiusSubvoxels=348;
|
|
+ recipe.routeConstraints.capsuleHeightSubvoxels=1802;
|
|
+ recipe.routeConstraints.maximumGradeQ16=17560;
|
|
+ recipe.routeConstraints.sweepSpacingSubvoxels=128;
|
|
+ recipe.routeConstraints.supportProbeRadiusSubvoxels=230;
|
|
+ recipe.routeConstraints.supportSearchDepthSubvoxels=512;
|
|
+ recipe.routeConstraints.minimumClearanceSubvoxels=128;
|
|
+ recipe.routeConstraints.minimumSupportProbes=3;
|
|
+ }
|
|
+ if(recipe.geometryVersion<10||recipe.primaryIslandCount!=9)
|
|
+ {error="corridor-aware FloatingIslands requires geometry version 10 and nine route islands";return std::nullopt;}
|
|
+ if(!ValidateRecipe(recipe,error))return std::nullopt;
|
|
+
|
|
+ const ResolvedLayoutPassageV1* arrival=nullptr;
|
|
+ const ResolvedLayoutPassageV1* departure=nullptr;
|
|
+ for(const auto& passage:layout.passages)
|
|
+ {
|
|
+ if(passage.lowerStrateIndex==strateIndex
|
|
+ &&passage.upperStrateIndex<strateIndex)arrival=&passage;
|
|
+ if(passage.upperStrateIndex==strateIndex
|
|
+ &&passage.lowerStrateIndex>strateIndex)departure=&passage;
|
|
+ }
|
|
+ if(!arrival||!departure)
|
|
+ {error="corridor-aware FloatingIslands needs resolved arrival and departure mouths";return std::nullopt;}
|
|
+
|
|
+ constexpr double PortalExclusionMetres=13.0;
|
|
+ const auto passageSpans=BuildIslandPassageSpans(layout,strateIndex,recipe,
|
|
+ PortalExclusionMetres);
|
|
+ if(passageSpans.empty())
|
|
+ {error="corridor-aware FloatingIslands found no resolved passage spans inside its strate";return std::nullopt;}
|
|
+ auto candidates=BuildIslandDetourCandidates(recipe,*arrival,*departure,passageSpans);
|
|
+ const double qPerMetre=static_cast<double>(WorldAssemblySubvoxelsPerVoxel)
|
|
+ *1000000.0/std::max<std::uint32_t>(1,recipe.coreVoxelSizeMicrons);
|
|
+ const double capsuleHalf=0.5*recipe.routeConstraints.capsuleHeightSubvoxels;
|
|
+ const double routeLift=capsuleHalf+recipe.routeConstraints.minimumClearanceSubvoxels+Q/8.0;
|
|
+ std::uint32_t tried=0;
|
|
+ std::string lastFailure="no outer detour has conservative tube clearance";
|
|
+ for(const auto& candidate:candidates)
|
|
+ {
|
|
+ if(candidate.conservativeClearanceMetres<0.0)break;
|
|
+ ++tried;
|
|
+ FloatingIslandManifestV1 manifest;
|
|
+ manifest.recipe=recipe;
|
|
+ auto& plan=manifest.plan;
|
|
+ plan.islands.reserve(recipe.primaryIslandCount);
|
|
+ bool candidateValid=true;
|
|
+ for(std::uint32_t ordinal=0;ordinal<recipe.primaryIslandCount;++ordinal)
|
|
+ {
|
|
+ const auto& center=candidate.centers[ordinal];
|
|
+ const double t=static_cast<double>(ordinal)/(recipe.primaryIslandCount-1);
|
|
+ const std::int64_t topZ=RoundQ(std::lerp(
|
|
+ static_cast<double>(arrival->lowerMouthSubvoxels[2])-routeLift,
|
|
+ static_cast<double>(departure->upperMouthSubvoxels[2])-routeLift,t));
|
|
+ auto island=MakeIsland(recipe,ordinal,center[0],center[1],topZ,
|
|
+ 0x100000000ull+ordinal);
|
|
+ if(ordinal==recipe.anchorIslandOrdinal)
|
|
+ island.radiusSubvoxels=std::max(island.radiusSubvoxels,
|
|
+ recipe.anchorIslandMinimumRadiusSubvoxels);
|
|
+ island.topHalfSubvoxels=std::max<std::int64_t>(1,island.radiusSubvoxels/5);
|
|
+ island.centerSubvoxels[2]=island.topZSubvoxels-island.topHalfSubvoxels;
|
|
+ island.bottomZSubvoxels=IslandBottom(recipe,island.topZSubvoxels,
|
|
+ island.radiusSubvoxels);
|
|
+ SetIslandBounds(recipe,island);
|
|
+ if(island.bottomZSubvoxels<recipe.layoutMinimumZSubvoxels
|
|
+ +recipe.boundarySealSubvoxels
|
|
+ ||island.topZSubvoxels>recipe.layoutMaximumZSubvoxels
|
|
+ -recipe.boundarySealSubvoxels)
|
|
+ {candidateValid=false;lastFailure="detour island escaped the sealed vertical layout";break;}
|
|
+ plan.islands.push_back(std::move(island));
|
|
+ }
|
|
+ if(!candidateValid)continue;
|
|
+ for(std::uint32_t ordinal=0;ordinal+1<recipe.primaryIslandCount;++ordinal)
|
|
+ plan.clusterEdges.push_back(MakeConnector(recipe,plan.islands[ordinal],
|
|
+ plan.islands[ordinal+1],0x200000000ull+ordinal));
|
|
+ FloatingIslandFieldV1 planningField(recipe,plan);
|
|
+ if(!planningField.isValid())
|
|
+ {lastFailure="detour island features did not pass the Core field contract";continue;}
|
|
+
|
|
+ std::uint32_t routeOrdinal=0;
|
|
+ const double routeStep=0.75*qPerMetre;
|
|
+ for(std::size_t segment=0;segment+1<plan.islands.size();++segment)
|
|
+ {
|
|
+ const auto& a=plan.islands[segment].centerSubvoxels;
|
|
+ const auto& b=plan.islands[segment+1].centerSubvoxels;
|
|
+ const double dx=static_cast<double>(b[0]-a[0]);
|
|
+ const double dy=static_cast<double>(b[1]-a[1]);
|
|
+ const std::uint32_t pieces=static_cast<std::uint32_t>(std::max(1.0,
|
|
+ std::ceil(std::hypot(dx,dy)/routeStep)));
|
|
+ for(std::uint32_t piece=segment==0?0:1;piece<=pieces;++piece)
|
|
+ {
|
|
+ const double t=static_cast<double>(piece)/pieces;
|
|
+ const std::int64_t x=RoundQ(static_cast<double>(a[0])+dx*t);
|
|
+ const std::int64_t y=RoundQ(static_cast<double>(a[1])+dy*t);
|
|
+ const auto surface=FindCapsuleFootingTopZ(recipe,plan,x,y);
|
|
+ if(!surface)
|
|
+ {candidateValid=false;lastFailure="detour route has no full capsule-width island support";break;}
|
|
+ WorldAssemblyRoutePointV1 point;
|
|
+ point.stableFeatureId=StableId(recipe.seed,0x500000000ull+routeOrdinal,
|
|
+ 0x524f555445ull);
|
|
+ point.positionSubvoxels={x,y,*surface+static_cast<std::int64_t>(std::llround(routeLift))};
|
|
+ point.radiusSubvoxels=4*Q;
|
|
+ if(segment==0&&piece==0)
|
|
+ point.positionSubvoxels=arrival->lowerMouthSubvoxels;
|
|
+ if(segment+2==plan.islands.size()&&piece==pieces)
|
|
+ point.positionSubvoxels=departure->upperMouthSubvoxels;
|
|
+ if(!plan.route.empty()&&plan.route.back().positionSubvoxels==point.positionSubvoxels)
|
|
+ continue;
|
|
+ plan.route.push_back(point);
|
|
+ ++routeOrdinal;
|
|
+ }
|
|
+ if(!candidateValid)break;
|
|
+ }
|
|
+ if(!candidateValid||plan.route.size()<2)continue;
|
|
+
|
|
+ const double maximumGrade=Q16ToDouble(recipe.routeConstraints.maximumGradeQ16)*0.95;
|
|
+ std::vector<std::int64_t> baseHeights;
|
|
+ baseHeights.reserve(plan.route.size());
|
|
+ for(const auto& point:plan.route)baseHeights.push_back(point.positionSubvoxels[2]);
|
|
+ std::vector<std::int64_t> profile=baseHeights;
|
|
+ for(std::size_t i=0;i<plan.route.size();++i)
|
|
+ {
|
|
+ double adjusted=static_cast<double>(baseHeights[i]);
|
|
+ for(std::size_t j=0;j<plan.route.size();++j)
|
|
+ {
|
|
+ const double dx=static_cast<double>(plan.route[i].positionSubvoxels[0]
|
|
+ -plan.route[j].positionSubvoxels[0]);
|
|
+ const double dy=static_cast<double>(plan.route[i].positionSubvoxels[1]
|
|
+ -plan.route[j].positionSubvoxels[1]);
|
|
+ adjusted=std::max(adjusted,static_cast<double>(baseHeights[j])
|
|
+ -maximumGrade*std::hypot(dx,dy));
|
|
+ }
|
|
+ profile[i]=RoundQ(adjusted);
|
|
+ }
|
|
+ const auto arrivalZ=arrival->lowerMouthSubvoxels[2];
|
|
+ const auto departureZ=departure->upperMouthSubvoxels[2];
|
|
+ if(profile.front()>arrivalZ+Q/2||profile.back()>departureZ+Q/2)
|
|
+ {lastFailure="island support profile cannot meet both fixed mouth elevations";continue;}
|
|
+ for(std::size_t i=1;i+1<plan.route.size();++i)
|
|
+ plan.route[i].positionSubvoxels[2]=profile[i];
|
|
+ plan.route.front().positionSubvoxels=arrival->lowerMouthSubvoxels;
|
|
+ plan.route.back().positionSubvoxels=departure->upperMouthSubvoxels;
|
|
+
|
|
+ if(!manifest.finalize(error))
|
|
+ {lastFailure=error;continue;}
|
|
+ const FloatingIslandFieldV1 field(manifest.recipe,manifest.plan);
|
|
+ const auto routeAudit=AuditFloatingIslandRoute(field);
|
|
+ if(!routeAudit.passed)
|
|
+ {
|
|
+ lastFailure="detour route sweep failed: blocked="
|
|
+ +std::to_string(routeAudit.blockedCapsuleSamples)+" unsupported="
|
|
+ +std::to_string(routeAudit.unsupportedCenterSamples)+" grade="
|
|
+ +std::to_string(routeAudit.gradeFailures)+" detour_angle_deg="
|
|
+ +std::to_string(candidate.waypointAngle*180.0/Pi)+" detour_radius_m="
|
|
+ +std::to_string(candidate.waypointRadiusMetres)+" first_unsupported_q=("
|
|
+ +std::to_string(routeAudit.firstUnsupportedCenterSubvoxels[0])+","+
|
|
+ std::to_string(routeAudit.firstUnsupportedCenterSubvoxels[1])+","+
|
|
+ std::to_string(routeAudit.firstUnsupportedCenterSubvoxels[2])+") segment="
|
|
+ +std::to_string(routeAudit.firstUnsupportedSegment);
|
|
+ continue;
|
|
+ }
|
|
+ const auto clearance=AuditFloatingIslandPassageClearance(field,layout,
|
|
+ strateIndex,PortalExclusionMetres);
|
|
+ if(!clearance.passed)
|
|
+ {
|
|
+ lastFailure="passage clearance failed: island="
|
|
+ +std::to_string(clearance.blockedIslandSamples)+" route="
|
|
+ +std::to_string(clearance.blockedRouteSamples)+" mouth_support="
|
|
+ +std::to_string(clearance.arrivalMouthSupportProbes)+"/"
|
|
+ +std::to_string(clearance.departureMouthSupportProbes);
|
|
+ continue;
|
|
+ }
|
|
+ const std::string encoded=manifest.ToJson();
|
|
+ const auto roundTrip=FloatingIslandManifestV1::FromJson(encoded,error);
|
|
+ if(!roundTrip||roundTrip->ToJson()!=encoded
|
|
+ ||roundTrip->contentHash!=manifest.contentHash)
|
|
+ {if(error.empty())error="corridor-aware floating-island plan did not canonical-round-trip";return std::nullopt;}
|
|
+ std::printf("[FloatingIslandCorridorPlan] strate=%d detours_tried=%u waypoint_angle_deg=%.1f waypoint_radius_m=%.1f route_m=%.1f conservative_clearance_m=%.2f portal_exclusion_m=%.1f islands=%zu arrival_binding=PASS departure_binding=PASS status=PASS\n",
|
|
+ strateIndex,tried,candidate.waypointAngle*180.0/Pi,
|
|
+ candidate.waypointRadiusMetres,candidate.horizontalLengthMetres,
|
|
+ candidate.conservativeClearanceMetres,PortalExclusionMetres,
|
|
+ manifest.plan.islands.size());
|
|
+ error.clear();
|
|
+ return manifest;
|
|
+ }
|
|
+ const auto bestCandidate=std::max_element(candidates.begin(),candidates.end(),
|
|
+ [](const auto& left,const auto& right)
|
|
+ {return left.conservativeClearanceMetres<right.conservativeClearanceMetres;});
|
|
+ error="no fixed-mouth island detour passed route support and passage clearance after "
|
|
+ +std::to_string(tried)+" candidates; best_clearance_m="
|
|
+ +(bestCandidate==candidates.end()?std::string("none")
|
|
+ :std::to_string(bestCandidate->conservativeClearanceMetres))
|
|
+ +" last="+lastFailure;
|
|
+ return std::nullopt;
|
|
+}
|
|
+
|
|
FloatingIslandFieldV1::FloatingIslandFieldV1(const FloatingIslandRecipeV1& recipe,
|
|
const FloatingIslandPlanV1& plan) noexcept : recipe_(&recipe), plan_(&plan)
|
|
{}
|
|
@@ -2848,6 +3276,107 @@ FloatingIslandRouteAuditResult AuditFloatingIslandRoute(const FloatingIslandFiel
|
|
return result;
|
|
}
|
|
|
|
+FloatingIslandPassageClearanceAuditResult AuditFloatingIslandPassageClearance(
|
|
+ const FloatingIslandFieldV1& field,const ResolvedLayoutSnapshotV3& layout,
|
|
+ std::int32_t strateIndex,double portalExclusionMetres)
|
|
+{
|
|
+ using namespace floating_island_detail;
|
|
+ FloatingIslandPassageClearanceAuditResult result;
|
|
+ result.portalExclusionMetres=portalExclusionMetres;
|
|
+ if(!field.isValid()||!field.recipe()||!field.plan()
|
|
+ ||field.plan()->route.size()<2||!std::isfinite(portalExclusionMetres)
|
|
+ ||portalExclusionMetres<0.0)return result;
|
|
+ const auto& recipe=*field.recipe();
|
|
+ const double qPerMetre=static_cast<double>(WorldAssemblySubvoxelsPerVoxel)
|
|
+ *1000000.0/std::max<std::uint32_t>(1,recipe.coreVoxelSizeMicrons);
|
|
+ const std::int64_t influenceReach=RoundQ(1.0533*qPerMetre);
|
|
+ const std::int64_t capsuleReach=recipe.routeConstraints.capsuleHeightSubvoxels/2;
|
|
+ const std::int64_t sampleGuard=RoundQ(0.15*qPerMetre);
|
|
+ const auto spans=BuildIslandPassageSpans(layout,strateIndex,recipe,
|
|
+ portalExclusionMetres);
|
|
+ bool haveClearance=false;
|
|
+ std::int64_t minimumExcess=std::numeric_limits<std::int64_t>::max();
|
|
+ for(const auto& span:spans)
|
|
+ {
|
|
+ const PassageSpline::FPoint center={
|
|
+ 0.5*(span.segment.a[0]+span.segment.b[0]),
|
|
+ 0.5*(span.segment.a[1]+span.segment.b[1]),
|
|
+ 0.5*(span.segment.a[2]+span.segment.b[2]),
|
|
+ 0.5*(span.segment.a[3]+span.segment.b[3])};
|
|
+ const std::int64_t density=field.densitySubvoxels(RoundQ(center[0]),
|
|
+ RoundQ(center[1]),RoundQ(center[2]));
|
|
+ const std::int64_t required=RoundQ(center[3])+influenceReach
|
|
+ +capsuleReach+sampleGuard;
|
|
+ const std::int64_t excess=density-required;
|
|
+ minimumExcess=std::min(minimumExcess,excess);
|
|
+ haveClearance=true;
|
|
+ ++result.passageSamples;
|
|
+ if(excess<0)++result.blockedIslandSamples;
|
|
+
|
|
+ for(std::size_t routeSegment=0;routeSegment+1<field.plan()->route.size();++routeSegment)
|
|
+ {
|
|
+ const auto& routeA=field.plan()->route[routeSegment].positionSubvoxels;
|
|
+ const auto& routeB=field.plan()->route[routeSegment+1].positionSubvoxels;
|
|
+ PassageSpline::FPoint a{},b{};
|
|
+ for(std::size_t axis=0;axis<3;++axis)
|
|
+ {a[axis]=static_cast<double>(routeA[axis]);b[axis]=static_cast<double>(routeB[axis]);}
|
|
+ const double distance=std::sqrt(SegmentDistanceSquared3D(a,b,
|
|
+ span.segment.a,span.segment.b));
|
|
+ const double requiredDistance=center[3]+influenceReach
|
|
+ +capsuleReach+sampleGuard;
|
|
+ ++result.routeSamples;
|
|
+ if(distance<requiredDistance)++result.blockedRouteSamples;
|
|
+ }
|
|
+ }
|
|
+ result.minimumExcessClearanceSubvoxels=haveClearance?minimumExcess:0;
|
|
+
|
|
+ const ResolvedLayoutPassageV1* arrival=nullptr;
|
|
+ const ResolvedLayoutPassageV1* departure=nullptr;
|
|
+ for(const auto& passage:layout.passages)
|
|
+ {
|
|
+ if(passage.lowerStrateIndex==strateIndex&&passage.upperStrateIndex<strateIndex)
|
|
+ arrival=&passage;
|
|
+ if(passage.upperStrateIndex==strateIndex&&passage.lowerStrateIndex>strateIndex)
|
|
+ departure=&passage;
|
|
+ }
|
|
+ const auto mouthSupport=[&](const std::array<std::int64_t,3>& mouth)
|
|
+ {
|
|
+ constexpr std::array<std::array<double,2>,5> probes={{{0.0,0.0},
|
|
+ {1.0,0.0},{-1.0,0.0},{0.0,1.0},{0.0,-1.0}}};
|
|
+ const double radius=static_cast<double>(recipe.routeConstraints.supportProbeRadiusSubvoxels);
|
|
+ const std::int64_t foot=mouth[2]-recipe.routeConstraints.capsuleHeightSubvoxels/2;
|
|
+ const std::int64_t sampleZ=foot-recipe.routeConstraints.minimumClearanceSubvoxels-Q/2;
|
|
+ std::uint32_t supported=0;
|
|
+ for(const auto& probe:probes)
|
|
+ {
|
|
+ const auto x=RoundQ(static_cast<double>(mouth[0])+probe[0]*radius);
|
|
+ const auto y=RoundQ(static_cast<double>(mouth[1])+probe[1]*radius);
|
|
+ if(field.densitySubvoxels(x,y,sampleZ)<0)++supported;
|
|
+ }
|
|
+ return supported;
|
|
+ };
|
|
+ bool bound=false;
|
|
+ if(arrival&&departure)
|
|
+ {
|
|
+ const auto& route=field.plan()->route;
|
|
+ const auto close=[](const auto& a,const auto& b)
|
|
+ {
|
|
+ for(std::size_t axis=0;axis<3;++axis)
|
|
+ if(std::abs(a[axis]-b[axis])>2)return false;
|
|
+ return true;
|
|
+ };
|
|
+ bound=close(route.front().positionSubvoxels,arrival->lowerMouthSubvoxels)
|
|
+ &&close(route.back().positionSubvoxels,departure->upperMouthSubvoxels);
|
|
+ result.arrivalMouthSupportProbes=mouthSupport(arrival->lowerMouthSubvoxels);
|
|
+ result.departureMouthSupportProbes=mouthSupport(departure->upperMouthSubvoxels);
|
|
+ }
|
|
+ result.passed=bound&&haveClearance&&result.blockedIslandSamples==0
|
|
+ &&result.blockedRouteSamples==0
|
|
+ &&result.arrivalMouthSupportProbes>=recipe.routeConstraints.minimumSupportProbes
|
|
+ &&result.departureMouthSupportProbes>=recipe.routeConstraints.minimumSupportProbes;
|
|
+ return result;
|
|
+}
|
|
+
|
|
FloatingIslandThicknessAuditResult AuditFloatingIslandThickness(
|
|
const FloatingIslandPlanV1& plan, double minimumDepthToDiameterRatio)
|
|
{
|
|
diff --git a/Standalone/Core/src/ShapeWorldInternal.h b/Standalone/Core/src/ShapeWorldInternal.h
|
|
index 1a57f85..98dabf5 100644
|
|
--- a/Standalone/Core/src/ShapeWorldInternal.h
|
|
+++ b/Standalone/Core/src/ShapeWorldInternal.h
|
|
@@ -1754,9 +1754,81 @@ public:
|
|
?passageAirDistance:density);
|
|
if(nativeTunnelFieldDensity!=std::numeric_limits<float>::lowest())
|
|
return std::max(density,passageAirDistance);
|
|
+ const VoxelForgeCore::WorldAssemblyFloatingIslandPlanV1* landingPlan=nullptr;
|
|
+ if(WorldAssembly&&queryStratum&&queryStratum->Resolved
|
|
+ &&queryStratum->Resolved->generatorKind=="FloatingIslands")
|
|
+ {
|
|
+ const auto island=std::find_if(WorldAssembly->floatingIslands.begin(),
|
|
+ WorldAssembly->floatingIslands.end(),[&](const auto& candidate)
|
|
+ {return candidate.strateIndex==queryStratum->StrateIndex;});
|
|
+ if(island!=WorldAssembly->floatingIslands.end())landingPlan=&*island;
|
|
+ }
|
|
+ const auto preserveFloatingIslandLandingSupport=[&](float composedDensity)
|
|
+ {
|
|
+ if(!landingPlan||landingPlan->manifest.recipe.geometryVersion<10)
|
|
+ return composedDensity;
|
|
+ const auto& constraints=landingPlan->manifest.recipe.routeConstraints;
|
|
+ if(constraints.version!=1)return composedDensity;
|
|
+ const auto* routeField=std::get_if<FFloatingIslandFieldAdapter>(
|
|
+ &queryStratum->Evaluator);
|
|
+ const auto& route=landingPlan->manifest.plan.route;
|
|
+ if(!routeField||route.size()<2)return composedDensity;
|
|
+
|
|
+ constexpr std::size_t LandingRouteSegments=18;
|
|
+ const double supportRadiusMetres=std::min(0.45,
|
|
+ constraints.supportProbeRadiusSubvoxels/qPerMetre+0.10);
|
|
+ const double queryX=qx,queryY=qy,queryZ=qz;
|
|
+ for(std::size_t segment=0;segment+1<route.size();++segment)
|
|
+ {
|
|
+ if(segment>=LandingRouteSegments
|
|
+ &&segment+LandingRouteSegments<route.size())continue;
|
|
+ const auto& a=route[segment].positionSubvoxels;
|
|
+ const auto& b=route[segment+1].positionSubvoxels;
|
|
+ const double dx=static_cast<double>(b[0]-a[0]);
|
|
+ const double dy=static_cast<double>(b[1]-a[1]);
|
|
+ const double dz=static_cast<double>(b[2]-a[2]);
|
|
+ const double horizontalSquared=dx*dx+dy*dy;
|
|
+ if(horizontalSquared<=1.0e-12)continue;
|
|
+ const double t=std::clamp(((queryX-a[0])*dx+(queryY-a[1])*dy)
|
|
+ /horizontalSquared,0.0,1.0);
|
|
+ const double routeX=a[0]+t*dx,routeY=a[1]+t*dy;
|
|
+ if(std::hypot(queryX-routeX,queryY-routeY)
|
|
+ >supportRadiusMetres*qPerMetre)
|
|
+ continue;
|
|
+ const double routeCenterZ=a[2]+t*dz;
|
|
+ const double footZ=routeCenterZ
|
|
+ -0.5*constraints.capsuleHeightSubvoxels;
|
|
+ const double shallowDepth=constraints.minimumClearanceSubvoxels+Q/2.0;
|
|
+ const double deepDepth=constraints.minimumClearanceSubvoxels
|
|
+ +constraints.supportSearchDepthSubvoxels;
|
|
+ if(queryZ<=footZ-shallowDepth&&queryZ>=footZ-deepDepth)
|
|
+ {
|
|
+ const std::int64_t nativeDensity=routeField->densitySubvoxelsAt(
|
|
+ static_cast<std::int64_t>(std::llround(queryX)),
|
|
+ static_cast<std::int64_t>(std::llround(queryY)),
|
|
+ static_cast<std::int64_t>(std::llround(queryZ)));
|
|
+ if(nativeDensity<0)
|
|
+ return std::min(composedDensity,
|
|
+ static_cast<float>(static_cast<double>(nativeDensity)/Q));
|
|
+ }
|
|
+ }
|
|
+ return composedDensity;
|
|
+ };
|
|
if(unionDistance>0.0&&unionShellDistance<0.0&&density>=0.0f)
|
|
return static_cast<float>(unionShellDistance/Q);
|
|
- return std::max(density,passageAirDistance);
|
|
+ float composedDensity=std::max(density,passageAirDistance);
|
|
+ if(landingPlan&&landingPlan->manifest.recipe.geometryVersion>=10
|
|
+ &&density>=0.0f&&unionDistance<0.0&&unionShellDistance>0.0)
|
|
+ {
|
|
+ // In open-air parts of the island stratum, the boolean air union saturates
|
|
+ // the passage's positive interior distance to the farther island-air
|
|
+ // distance. Keep the nearest positive bore/shell distance so the explicit
|
|
+ // passage shell remains measurable without changing any field signs.
|
|
+ composedDensity=std::min(composedDensity,
|
|
+ std::min(passageAirDistance,
|
|
+ static_cast<float>(unionShellDistance/Q)));
|
|
+ }
|
|
+ return preserveFloatingIslandLandingSupport(composedDensity);
|
|
}
|
|
|
|
float ApplyResolvedStrateComposition(float density,float X,float Y,float Z) const
|
|
@@ -7217,9 +7289,11 @@ private:
|
|
Density=filteredDensity;
|
|
if(routeNativeBlend>0.0&&NativeDensity<0)
|
|
{
|
|
+ const double routePreservation=Plan->manifest.recipe.geometryVersion>=10
|
|
+ ?routeNativeBlend:routeNativeBlend*0.60;
|
|
const double routePreserved=std::lerp(static_cast<double>(Density),
|
|
static_cast<double>(std::min(Density,NativeDensity)),
|
|
- routeNativeBlend*0.60);
|
|
+ routePreservation);
|
|
Density=static_cast<std::int64_t>(std::llround(routePreserved));
|
|
}
|
|
// Marching Cubes can retain an isolated solid with no two-metre axis
|
|
diff --git a/Standalone/Source/StandaloneViewer.cpp b/Standalone/Source/StandaloneViewer.cpp
|
|
index 1c0ed4c..80ad688 100644
|
|
--- a/Standalone/Source/StandaloneViewer.cpp
|
|
+++ b/Standalone/Source/StandaloneViewer.cpp
|
|
@@ -9208,11 +9208,15 @@ int main(int Argc,char** Argv)
|
|
{
|
|
const auto PlanStart=std::chrono::steady_clock::now();
|
|
VoxelForgeCore::WorldAssemblyManifest WorldAssembly;
|
|
+ VoxelForgeCore::ResolvedLayoutSnapshotV3 ResolvedLayout;
|
|
const bool bPlanReady=StandaloneWorldAssemblyPrototype::AttachRound1WorldAssemblyPrototype(*App.World,App.OwnerJsonRoot,
|
|
Error,WorldAssemblyManifestOutputPath,&WorldAssembly)
|
|
&&(!bFloatingIslandsRecipe
|
|
||StandaloneWorldAssemblyPrototype::AttachFloatingIslandWorldAssembly(
|
|
- *App.World,App.OwnerJsonRoot,WorldAssembly,Error));
|
|
+ *App.World,App.OwnerJsonRoot,WorldAssembly,Error))
|
|
+ &&(!bFloatingIslandsRecipe
|
|
+ ||StandaloneWorldAssemblyPrototype::BuildResolvedLayoutSnapshot(
|
|
+ *App.World,WorldAssembly,"floating-islands",ResolvedLayout,Error));
|
|
const double PlanMilliseconds=std::chrono::duration<double,std::milli>(
|
|
std::chrono::steady_clock::now()-PlanStart).count();
|
|
std::printf("[WorldAssemblerPlanTiming] total_ms=%.3f status=%s\n",
|
|
diff --git a/Standalone/Source/StreamBenchmarkHeadless.cpp b/Standalone/Source/StreamBenchmarkHeadless.cpp
|
|
index 1afcff5..f22afe9 100644
|
|
--- a/Standalone/Source/StreamBenchmarkHeadless.cpp
|
|
+++ b/Standalone/Source/StreamBenchmarkHeadless.cpp
|
|
@@ -1830,31 +1830,11 @@ int main(int argc,char** argv)
|
|
else if(!StandaloneWorldAssemblyPrototype::AttachRound1WorldAssemblyPrototype(
|
|
*ownerWorld,ownerJsonRoot,error,{},&worldAssembly))
|
|
{std::fprintf(stderr,"World assembler prototype setup failed: %s\n",error.c_str());return 2;}
|
|
- if(((!useExportedCorePlan&&options.FloatingIslandsRecipe)
|
|
- ||(options.NativeOwnerFloatingIslandsRecipe
|
|
- &&worldAssembly.floatingIslands.empty()))
|
|
+ if(!useExportedCorePlan&&options.FloatingIslandsRecipe
|
|
+ &&!options.NativeOwnerFloatingIslandsRecipe
|
|
&&!StandaloneWorldAssemblyPrototype::AttachFloatingIslandWorldAssembly(
|
|
*ownerWorld,ownerJsonRoot,worldAssembly,error))
|
|
{std::fprintf(stderr,"Floating-island assembly setup failed: %s\n",error.c_str());return 2;}
|
|
- if(!options.WorldAssemblyManifestOutputPath.empty())
|
|
- {
|
|
- const std::string manifestText=worldAssembly.ToJson();
|
|
- const auto& outputPath=options.WorldAssemblyManifestOutputPath;
|
|
- std::error_code directoryError;
|
|
- if(!outputPath.parent_path().empty())
|
|
- std::filesystem::create_directories(outputPath.parent_path(),directoryError);
|
|
- if(directoryError)
|
|
- {std::fprintf(stderr,"Could not create the complete world assembly manifest output directory.\n");return 2;}
|
|
- std::ofstream output(outputPath,std::ios::binary);
|
|
- if(!output)
|
|
- {std::fprintf(stderr,"Could not open the complete world assembly manifest output path.\n");return 2;}
|
|
- output.write(manifestText.data(),static_cast<std::streamsize>(manifestText.size()));
|
|
- if(!output)
|
|
- {std::fprintf(stderr,"Could not write the complete world assembly manifest payload.\n");return 2;}
|
|
- std::printf("[WorldAssemblerFullManifest] bytes=%zu surface_strates=%zu floating_islands=%zu path=%s status=PASS\n",
|
|
- manifestText.size(),worldAssembly.plan.strates.size(),worldAssembly.floatingIslands.size(),
|
|
- outputPath.string().c_str());
|
|
- }
|
|
if (options.AuditHeightfieldProfile && !RunHeightfieldProfileAudit(worldAssembly))
|
|
{
|
|
std::fprintf(stderr,"Profiled SurfaceWorld interval, classification or route audit failed.\n");
|
|
@@ -1886,6 +1866,31 @@ int main(int argc,char** argv)
|
|
&&!StandaloneWorldAssemblyPrototype::ValidateExportedCorePlanMatchesOwner(
|
|
*ownerWorld,worldAssembly,resolvedLayout,error))
|
|
{std::fprintf(stderr,"Unreal-resolved Core plan/owner slot validation failed: %s\n",error.c_str());return 2;}
|
|
+ if(options.NativeOwnerFloatingIslandsRecipe
|
|
+ &&worldAssembly.floatingIslands.empty()
|
|
+ &&!StandaloneWorldAssemblyPrototype::AttachFloatingIslandWorldAssembly(
|
|
+ *ownerWorld,ownerJsonRoot,worldAssembly,resolvedLayout,error,
|
|
+ true))
|
|
+ {std::fprintf(stderr,"Floating-island assembly setup failed: %s\n",error.c_str());return 2;}
|
|
+ if(!options.WorldAssemblyManifestOutputPath.empty())
|
|
+ {
|
|
+ const std::string manifestText=worldAssembly.ToJson();
|
|
+ const auto& outputPath=options.WorldAssemblyManifestOutputPath;
|
|
+ std::error_code directoryError;
|
|
+ if(!outputPath.parent_path().empty())
|
|
+ std::filesystem::create_directories(outputPath.parent_path(),directoryError);
|
|
+ if(directoryError)
|
|
+ {std::fprintf(stderr,"Could not create the complete world assembly manifest output directory.\n");return 2;}
|
|
+ std::ofstream output(outputPath,std::ios::binary);
|
|
+ if(!output)
|
|
+ {std::fprintf(stderr,"Could not open the complete world assembly manifest output path.\n");return 2;}
|
|
+ output.write(manifestText.data(),static_cast<std::streamsize>(manifestText.size()));
|
|
+ if(!output)
|
|
+ {std::fprintf(stderr,"Could not write the complete world assembly manifest payload.\n");return 2;}
|
|
+ std::printf("[WorldAssemblerFullManifest] bytes=%zu surface_strates=%zu floating_islands=%zu path=%s status=PASS\n",
|
|
+ manifestText.size(),worldAssembly.plan.strates.size(),worldAssembly.floatingIslands.size(),
|
|
+ outputPath.string().c_str());
|
|
+ }
|
|
if(!std::holds_alternative<std::nullptr_t>(ownerJsonRoot.Get("motif_family_override").Data))
|
|
{
|
|
std::vector<VoxelForgeCore::ResolvedLayoutStampRequestV1> requests;
|
|
diff --git a/Standalone/Source/WorldAssemblyPrototype.h b/Standalone/Source/WorldAssemblyPrototype.h
|
|
index 4e20068..70c4c61 100644
|
|
--- a/Standalone/Source/WorldAssemblyPrototype.h
|
|
+++ b/Standalone/Source/WorldAssemblyPrototype.h
|
|
@@ -1366,7 +1366,9 @@ namespace StandaloneWorldAssemblyPrototype
|
|
}
|
|
|
|
inline bool AttachFloatingIslandWorldAssembly(FStandaloneOwnerWorld& owner,
|
|
- VFJson::Value& root,VoxelForgeCore::WorldAssemblyManifest& assembly,std::string& error)
|
|
+ VFJson::Value& root,VoxelForgeCore::WorldAssemblyManifest& assembly,
|
|
+ VoxelForgeCore::ResolvedLayoutSnapshotV3& resolvedLayout,
|
|
+ std::string& error,bool planAroundPassages=false)
|
|
{
|
|
if(!owner.Manager)
|
|
{error="floating-island assembly needs the resolved standalone strate manager";return false;}
|
|
@@ -1386,7 +1388,8 @@ namespace StandaloneWorldAssemblyPrototype
|
|
FIntVector(0,0,islandSlot->TopChunkZ));
|
|
const FStratePassageConfig& passage=islandSlot->Definition->PassageConfig;
|
|
recipe.seed=static_cast<std::uint32_t>(owner.Seed);
|
|
- recipe.geometryVersion=8;
|
|
+ recipe.geometryVersion=planAroundPassages?10:8;
|
|
+ if(planAroundPassages)recipe.primaryIslandCount=9;
|
|
recipe.terrainFollowingRoute=static_cast<std::uint32_t>(passage.Style)==0;
|
|
recipe.ownerVoxelSizeMicrons=owner.OwnerVoxelSizeMicrons;
|
|
recipe.coreVoxelSizeMicrons=owner.CoreVoxelSizeMicrons;
|
|
@@ -1406,43 +1409,79 @@ namespace StandaloneWorldAssemblyPrototype
|
|
recipe.islandDensityQ16=WorldAssemblyQ16(std::clamp<double>(params.IslandDensity,0.0,1.0));
|
|
recipe.spacingSubvoxels=WorldAssemblyFixed(params.IslandSpacing*ownerToCoreScale);
|
|
recipe.minimumRadiusSubvoxels=WorldAssemblyFixed(params.IslandMinRadius*ownerToCoreScale);
|
|
- recipe.maximumRadiusSubvoxels=WorldAssemblyFixed(params.IslandMaxRadius*ownerToCoreScale);
|
|
+ recipe.maximumRadiusSubvoxels=WorldAssemblyFixed((planAroundPassages
|
|
+ ?28.0:params.IslandMaxRadius)*ownerToCoreScale);
|
|
recipe.thicknessRatioQ16=WorldAssemblyQ16(params.ThicknessRatio);
|
|
recipe.verticalJitterQ16=WorldAssemblyQ16(params.VerticalJitter);
|
|
recipe.topFlattenQ16=WorldAssemblyQ16(params.TopFlatten);
|
|
- recipe.blendRadiusSubvoxels=WorldAssemblyFixed(params.SDFBlendRadius*ownerToCoreScale);
|
|
+ recipe.blendRadiusSubvoxels=planAroundPassages
|
|
+ ?2*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel
|
|
+ :WorldAssemblyFixed(params.SDFBlendRadius*ownerToCoreScale);
|
|
+ if(planAroundPassages)
|
|
+ recipe.contourWarpSubvoxels=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
|
recipe.roughnessSubvoxels=WorldAssemblyFixed(params.SurfaceRoughness*ownerToCoreScale);
|
|
+ if(planAroundPassages)
|
|
+ recipe.roughnessSubvoxels=std::min(recipe.roughnessSubvoxels,
|
|
+ VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel/2);
|
|
recipe.boundarySealSubvoxels=WorldAssemblyFixed(params.BoundarySealThickness*ownerToCoreScale);
|
|
+ if(planAroundPassages)
|
|
+ {
|
|
+ recipe.routeConstraints.version=1;
|
|
+ recipe.routeConstraints.capsuleRadiusSubvoxels=348;
|
|
+ recipe.routeConstraints.capsuleHeightSubvoxels=1802;
|
|
+ recipe.routeConstraints.maximumGradeQ16=17560;
|
|
+ recipe.routeConstraints.sweepSpacingSubvoxels=128;
|
|
+ recipe.routeConstraints.supportProbeRadiusSubvoxels=230;
|
|
+ recipe.routeConstraints.supportSearchDepthSubvoxels=512;
|
|
+ recipe.routeConstraints.minimumClearanceSubvoxels=128;
|
|
+ recipe.routeConstraints.minimumSupportProbes=3;
|
|
+ }
|
|
const auto ownerVoxelsToCoreQ=[&](double value)
|
|
{return static_cast<std::int64_t>(std::llround(value*owner.OwnerVoxelSizeMicrons
|
|
*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel/owner.CoreVoxelSizeMicrons));};
|
|
recipe.anchorIslandOrdinal=recipe.primaryIslandCount-1;
|
|
recipe.anchorPlacementRadiusSubvoxels=ownerVoxelsToCoreQ(passage.DistanceMax);
|
|
- const std::int64_t spiralRadius=ownerVoxelsToCoreQ(passage.SpiralRadius);
|
|
- const std::int64_t mouthBore=std::max(ownerVoxelsToCoreQ(passage.MouthRadius),
|
|
- ownerVoxelsToCoreQ(36.0));
|
|
- const std::int64_t bore=std::max(ownerVoxelsToCoreQ(passage.MidRadius),
|
|
- ownerVoxelsToCoreQ(24.0));
|
|
- const std::int64_t wallReserve=ownerVoxelsToCoreQ(20.0);
|
|
- // Retain a 4.5-metre mouth for capsule clearance while keeping the long
|
|
- // ramp at three metres to leave more rock around the first coil.
|
|
- recipe.anchorMouthOffsetSubvoxels=ownerVoxelsToCoreQ(40.0);
|
|
- const std::int64_t supportOverhang=ownerVoxelsToCoreQ(16.0);
|
|
- const std::int64_t rootTaperReserve=ownerVoxelsToCoreQ(10.0);
|
|
- const std::int64_t mouthEnvelope=recipe.anchorMouthOffsetSubvoxels+mouthBore;
|
|
- const std::int64_t treadEnvelope=spiralRadius+bore+supportOverhang;
|
|
- recipe.anchorRootRadiusSubvoxels=std::max(mouthEnvelope,treadEnvelope)
|
|
- +wallReserve+rootTaperReserve;
|
|
- recipe.anchorIslandMinimumRadiusSubvoxels=std::max(recipe.minimumRadiusSubvoxels,
|
|
- std::max(recipe.anchorMouthOffsetSubvoxels+mouthBore+ownerVoxelsToCoreQ(4.0),
|
|
- recipe.anchorRootRadiusSubvoxels+wallReserve));
|
|
+ if(planAroundPassages)
|
|
+ {
|
|
+ recipe.anchorMouthOffsetSubvoxels=ownerVoxelsToCoreQ(4.0);
|
|
+ recipe.anchorRootRadiusSubvoxels=std::max(ownerVoxelsToCoreQ(16.0),
|
|
+ recipe.anchorMouthOffsetSubvoxels+recipe.routeConstraints.capsuleRadiusSubvoxels
|
|
+ +VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
|
|
+ recipe.anchorIslandMinimumRadiusSubvoxels=std::max(recipe.maximumRadiusSubvoxels,
|
|
+ recipe.anchorMouthOffsetSubvoxels+recipe.routeConstraints.capsuleRadiusSubvoxels
|
|
+ +VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
|
|
+ }
|
|
+ else
|
|
+ {
|
|
+ const std::int64_t spiralRadius=ownerVoxelsToCoreQ(passage.SpiralRadius);
|
|
+ const std::int64_t mouthBore=std::max(ownerVoxelsToCoreQ(passage.MouthRadius),
|
|
+ ownerVoxelsToCoreQ(36.0));
|
|
+ const std::int64_t bore=std::max(ownerVoxelsToCoreQ(passage.MidRadius),
|
|
+ ownerVoxelsToCoreQ(24.0));
|
|
+ const std::int64_t wallReserve=ownerVoxelsToCoreQ(20.0);
|
|
+ recipe.anchorMouthOffsetSubvoxels=ownerVoxelsToCoreQ(40.0);
|
|
+ const std::int64_t supportOverhang=ownerVoxelsToCoreQ(16.0);
|
|
+ const std::int64_t rootTaperReserve=ownerVoxelsToCoreQ(10.0);
|
|
+ const std::int64_t mouthEnvelope=recipe.anchorMouthOffsetSubvoxels+mouthBore;
|
|
+ const std::int64_t treadEnvelope=spiralRadius+bore+supportOverhang;
|
|
+ recipe.anchorRootRadiusSubvoxels=std::max(mouthEnvelope,treadEnvelope)
|
|
+ +wallReserve+rootTaperReserve;
|
|
+ recipe.anchorIslandMinimumRadiusSubvoxels=std::max(recipe.minimumRadiusSubvoxels,
|
|
+ std::max(recipe.anchorMouthOffsetSubvoxels+mouthBore+ownerVoxelsToCoreQ(4.0),
|
|
+ recipe.anchorRootRadiusSubvoxels+wallReserve));
|
|
+ }
|
|
recipe.anchorRootBottomZSubvoxels=recipe.layoutMinimumZSubvoxels
|
|
+recipe.boundarySealSubvoxels;
|
|
|
|
- auto planned=VoxelForgeCore::PlanFloatingIslandCluster(recipe,error);
|
|
+ auto planned=planAroundPassages
|
|
+ ?VoxelForgeCore::PlanFloatingIslandClusterAroundPassages(
|
|
+ recipe,resolvedLayout,islandSlot->StrateIndex,error)
|
|
+ :VoxelForgeCore::PlanFloatingIslandCluster(recipe,error);
|
|
if(!planned)return false;
|
|
const VoxelForgeCore::FloatingIslandFieldV1 routeField(
|
|
planned->recipe,planned->plan);
|
|
+ const double metresPerSubvoxel=static_cast<double>(owner.CoreVoxelSizeMicrons)
|
|
+ /(1000000.0*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
|
|
const auto routeAudit=VoxelForgeCore::AuditFloatingIslandRoute(routeField);
|
|
std::printf("[FloatingIslandRouteAudit] points=%zu centers=%llu capsule_samples=%llu blocked=%llu support_samples=%llu unsupported=%llu grade_failures=%llu status=%s\n",
|
|
planned->plan.route.size(),
|
|
@@ -1453,8 +1492,27 @@ namespace StandaloneWorldAssemblyPrototype
|
|
static_cast<unsigned long long>(routeAudit.unsupportedCenterSamples),
|
|
static_cast<unsigned long long>(routeAudit.gradeFailures),
|
|
routeAudit.passed?"PASS":"FAIL");
|
|
- const double metresPerSubvoxel=static_cast<double>(owner.CoreVoxelSizeMicrons)
|
|
- /(1000000.0*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
|
|
+ VoxelForgeCore::FloatingIslandPassageClearanceAuditResult passageAudit;
|
|
+ if(planAroundPassages)
|
|
+ {
|
|
+ constexpr double PortalExclusionMetres=13.0;
|
|
+ passageAudit=VoxelForgeCore::AuditFloatingIslandPassageClearance(
|
|
+ routeField,resolvedLayout,islandSlot->StrateIndex,PortalExclusionMetres);
|
|
+ std::printf("[FloatingIslandPassageClearance] strate=%d tube_samples=%llu island_blocked=%llu route_checks=%llu route_blocked=%llu arrival_support=%u/5 departure_support=%u/5 clearance_excess_m=%.3f portal_exclusion_m=%.1f influence_reach_m=1.053 capsule_bound_m=%.3f status=%s\n",
|
|
+ islandSlot->StrateIndex,
|
|
+ static_cast<unsigned long long>(passageAudit.passageSamples),
|
|
+ static_cast<unsigned long long>(passageAudit.blockedIslandSamples),
|
|
+ static_cast<unsigned long long>(passageAudit.routeSamples),
|
|
+ static_cast<unsigned long long>(passageAudit.blockedRouteSamples),
|
|
+ passageAudit.arrivalMouthSupportProbes,
|
|
+ passageAudit.departureMouthSupportProbes,
|
|
+ passageAudit.minimumExcessClearanceSubvoxels*metresPerSubvoxel,
|
|
+ passageAudit.portalExclusionMetres,
|
|
+ planned->recipe.routeConstraints.capsuleHeightSubvoxels*metresPerSubvoxel*0.5,
|
|
+ passageAudit.passed?"PASS":"FAIL");
|
|
+ if(!routeAudit.passed||!passageAudit.passed)
|
|
+ {error="corridor-aware floating-island route or passage audit failed";return false;}
|
|
+ }
|
|
const double bedrockTopMetres=(minimumZ+recipe.boundarySealSubvoxels)*metresPerSubvoxel;
|
|
const double ceilingBottomMetres=(maximumZ-recipe.boundarySealSubvoxels)*metresPerSubvoxel;
|
|
for(const auto& island:planned->plan.islands)
|
|
@@ -1473,18 +1531,82 @@ namespace StandaloneWorldAssemblyPrototype
|
|
assembly.floatingIslands.push_back(std::move(islands));
|
|
if(!assembly.finalize(error))return false;
|
|
|
|
+ if(planAroundPassages)
|
|
+ {
|
|
+ auto resolvedStratum=std::find_if(resolvedLayout.strates.begin(),
|
|
+ resolvedLayout.strates.end(),[&](const auto& item)
|
|
+ {return item.strateIndex==islandSlot->StrateIndex;});
|
|
+ if(resolvedStratum==resolvedLayout.strates.end())
|
|
+ {error="resolved FloatingIslands stratum disappeared before native anchor attachment";return false;}
|
|
+ resolvedStratum->recipeVersion=assembly.floatingIslands.back().manifest.recipe.geometryVersion;
|
|
+ resolvedStratum->anchors.erase(std::remove_if(resolvedStratum->anchors.begin(),
|
|
+ resolvedStratum->anchors.end(),[](const auto& anchor)
|
|
+ {return anchor.kind=="island_anchor"||anchor.kind=="passage_anchor"
|
|
+ ||anchor.kind=="route_point";}),resolvedStratum->anchors.end());
|
|
+ const auto& nativePlan=assembly.floatingIslands.back().manifest.plan;
|
|
+ const auto& nativeRecipe=assembly.floatingIslands.back().manifest.recipe;
|
|
+ for(std::size_t n=0;n<nativePlan.islands.size();++n)
|
|
+ {
|
|
+ const auto& island=nativePlan.islands[n];
|
|
+ VoxelForgeCore::ResolvedLayoutAnchorV2 anchor;
|
|
+ anchor.kind="island_anchor";anchor.stableId="island/"+std::to_string(n);
|
|
+ anchor.units="1/256 Core voxel";
|
|
+ anchor.values={island.centerSubvoxels[0],island.centerSubvoxels[1],
|
|
+ island.centerSubvoxels[2],island.radiusSubvoxels,island.topHalfSubvoxels};
|
|
+ resolvedStratum->anchors.push_back(std::move(anchor));
|
|
+ if(n==nativeRecipe.anchorIslandOrdinal)
|
|
+ {
|
|
+ VoxelForgeCore::ResolvedLayoutAnchorV2 passageAnchor;
|
|
+ passageAnchor.kind="passage_anchor";
|
|
+ passageAnchor.stableId="passage-anchor/island/"+std::to_string(n);
|
|
+ passageAnchor.units="1/256 Core voxel";
|
|
+ passageAnchor.values={island.centerSubvoxels[0],island.centerSubvoxels[1],
|
|
+ nativeRecipe.anchorRootRadiusSubvoxels,
|
|
+ nativeRecipe.anchorMouthOffsetSubvoxels,
|
|
+ nativeRecipe.anchorPlacementRadiusSubvoxels,
|
|
+ nativeRecipe.anchorRootBottomZSubvoxels,island.radiusSubvoxels};
|
|
+ if(!nativePlan.route.empty())
|
|
+ {
|
|
+ const auto& mouth=nativePlan.route.back().positionSubvoxels;
|
|
+ passageAnchor.values.insert(passageAnchor.values.end(),
|
|
+ {mouth[0],mouth[1],mouth[2]});
|
|
+ }
|
|
+ resolvedStratum->anchors.push_back(std::move(passageAnchor));
|
|
+ }
|
|
+ }
|
|
+ for(std::size_t n=0;n<nativePlan.route.size();++n)
|
|
+ {
|
|
+ const auto& point=nativePlan.route[n];
|
|
+ VoxelForgeCore::ResolvedLayoutAnchorV2 anchor;
|
|
+ anchor.kind="route_point";anchor.stableId="route/"+std::to_string(n);
|
|
+ anchor.units="1/256 Core voxel";
|
|
+ anchor.values={point.positionSubvoxels[0],point.positionSubvoxels[1],
|
|
+ point.positionSubvoxels[2],point.radiusSubvoxels};
|
|
+ resolvedStratum->anchors.push_back(std::move(anchor));
|
|
+ }
|
|
+ if(!resolvedLayout.finalize(error))return false;
|
|
+ }
|
|
+
|
|
VFJson::Value assemblyJson;
|
|
if(!VFJson::Parser(assembly.ToJson()).Parse(assemblyJson,error))return false;
|
|
auto* rootObject=std::get_if<VFJson::Value::Object>(&root.Data);
|
|
if(!rootObject){error="owner snapshot root is not a JSON object";return false;}
|
|
(*rootObject)["world_assembly"]=std::move(assemblyJson);
|
|
- std::printf("[FloatingIslandWorldAssembly] seed=%d strate_index=%d chunks=%d..%d island_count=%zu recipe_hash=%016llx status=PASS\n",
|
|
+ std::printf("[FloatingIslandWorldAssembly] seed=%d strate_index=%d chunks=%d..%d island_count=%zu dispatch=FloatingIslands-native recipe_hash=%016llx status=PASS\n",
|
|
owner.Seed,islandSlot->StrateIndex,islandSlot->TopChunkZ,islandSlot->BottomChunkZ,
|
|
assembly.floatingIslands.back().manifest.plan.islands.size(),
|
|
static_cast<unsigned long long>(assembly.floatingIslands.back().manifest.recipe.contentHash));
|
|
return true;
|
|
}
|
|
|
|
+ inline bool AttachFloatingIslandWorldAssembly(FStandaloneOwnerWorld& owner,
|
|
+ VFJson::Value& root,VoxelForgeCore::WorldAssemblyManifest& assembly,
|
|
+ std::string& error)
|
|
+ {
|
|
+ VoxelForgeCore::ResolvedLayoutSnapshotV3 unusedLayout;
|
|
+ return AttachFloatingIslandWorldAssembly(owner,root,assembly,unusedLayout,error,false);
|
|
+ }
|
|
+
|
|
inline const char* GeneratorKind(ECaveGeneratorType type)
|
|
{
|
|
switch(type)
|
|
@@ -1964,6 +2086,7 @@ namespace StandaloneWorldAssemblyPrototype
|
|
:it->manifest.plan.route.size()-1;
|
|
std::size_t resolvedRoutePointCount=it->manifest.plan.route.size();
|
|
if(it->manifest.recipe.geometryVersion>=5
|
|
+ &&it->manifest.recipe.geometryVersion<10
|
|
&&!it->manifest.plan.route.empty()
|
|
&&it->manifest.recipe.anchorIslandOrdinal<it->manifest.plan.islands.size())
|
|
{
|