feat(opstack): the box fold carries NUMBERS -- amplitude bounds alongside EVoxelOpEffect (DECOMPOSITION 0.2)
Its own commit, nothing else in it, because it changes the contract all thirteen tests rest on. VoxelForge.OpStack.BoxVerdictFold is extended in the same commit, as required. THE PROBLEM, restated because the fix only makes sense against it Direction alone can never recover a FIELDED carve. A noise-threshold carve (TunnelNetwork's worms, wall roughness) has no spatial bound: it answers CarveOnly on every box of every strate that enables it, and that answer is TRUE. So it kills AllSolid everywhere and the archetype skips zero tiles. No refinement of EffectOverBox can fix that. But the AMPLITUDE is bounded, and for the worm it is trivial: the block only runs below the threshold, so t = 1 - WormValue/WormThreshold is in [0,1] and NetworkMask is in [0,1], hence the carve is at most WormStrength. "The rock is solid by more than the sum of every remaining carve" is therefore provable. THE CONTRACT - IVoxelDensityOp gains MaxCarveOverBox / MaxFillOverBox (density units, FLT_MAX = unknown) and ForcedMarginOverBox (how far the density is guaranteed from zero, 0 = unknown). - FVoxelBoxHypotheses gains SolidMargin / AirMargin. A forcing op sets one; each carve subtracts its amplitude; the hypothesis dies when the margin is no longer strictly positive. - VF_ForceHypotheses and VF_FoldEffect take the new values as DEFAULTED parameters. BACKWARDS COMPATIBILITY IS THE POINT, and it is structural rather than promised: the defaults are FLT_MAX and 0, so an op that overrides nothing subtracts FLT_MAX from a margin of 0 and kills the hypothesis exactly as the purely directional fold did. Not one existing verdict moves. The float arithmetic is deliberately unguarded: 0 - FLT_MAX is -FLT_MAX, -FLT_MAX - FLT_MAX saturates to -inf, -inf > 0 is false, and no NaN is reachable because both terms share a sign. BOUNDS DECLARED (each proved from the code, not estimated -- over-estimating costs CPU, under-estimating is a hole) - FConstantFieldSource::ForcedMarginOverBox = |Value|. This is the missing FIRST TERM: without a source that states how solid the rock is, there is nothing for a bounded carve to be subtracted from, and every bound would still kill the hypothesis. - FWormFieldSource: MaxCarve = WormStrength (the bound that had been written and unused since stage A), MaxFill = 0. - FCaveRoughnessMod: 1.4 * SurfaceRoughness * VOXEL_NOISE_SCALE both ways. - FLayerLineMod (LayerLineDepth), FRibbingMod (RibbingDepth), FScallopMod (ScallopStrength), FCaveOverhangMod (SCALE * Depth * Strength). WHAT THIS DOES *NOT* DO YET, said plainly rather than implied TunnelNetwork still proves ZERO tiles. The chain dies at FRoomGraphSource, which answers Both with unknown amplitude, before any of the bounded ops are reached. Making it answer spatially means building the SDF cache for the queried box, which only pays once ClassifyTile actually consumes ClassifyBox -- so it belongs with that work, not here. The one case that changes today is a tunnel strate with RoomDensity <= 0: the room source returns Identity and the bounded worm + roughness can now leave AllSolid standing. Seven ops keep the FLT_MAX default (terrace, cliff, arch, column, dome, pinch, floor bias). Their amplitudes depend on room-relative data, and bounding them would change no verdict while the room source is unbounded. Writing bounds nobody can consume is how a bound goes stale unnoticed. ONE PRE-EXISTING DEBT MADE SHARPER, noted at every site: these bounds are computed from STRATE params, and a per-room terrain op can write a LARGER amplitude (ApplyTo overwrites even where the strate had 0). So a bound can be too small -- the dangerous direction. Same root as the too-optimistic EffectOverBox flagged in C1, same fix, and it MUST land before ClassifyTile consumes ClassifyBox. TEST: eight new blocks in BoxVerdictFold. The first two are the ones to read -- they assert that the defaults reproduce the old fold exactly, and that "unknown" is not "zero". Also asserted: carve amplitudes accumulate; equality loses the tile (the > is strict on purpose, since zero counts as air at the mesher); a bounded Both no longer kills a margin it cannot cross; and nothing bounded can resurrect an unprovable box. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
64d0e11821
commit
353d504169
3 files changed
+304
-17
No files matched your search
@@ -134,6 +134,16 @@ namespace
|
||||
return EVoxelOpEffect::Both; // jamais atteint sauf Value == 0 : ClassifyBox répond avant
|
||||
}
|
||||
|
||||
/** ⚠️ LA MOITIÉ MANQUANTE DU PLIAGE NUMÉRIQUE (`OPSTACK-DECOMPOSITION §0.2`).
|
||||
* `MaxCarveOverBox` dit ce qu'un opérateur peut RETIRER ; ceci dit ce qu'il y avait à
|
||||
* retirer. Un champ constant est le seul opérateur du plugin qui connaisse cette marge
|
||||
* EXACTEMENT : la densité vaut `Value` partout, donc la marge est `|Value|`. Sans elle la
|
||||
* soustraction n'a pas de premier terme et tout carve borné tue quand même l'hypothèse. */
|
||||
float ForcedMarginOverBox(const FBox&, const FVoxelOpContext&) const override
|
||||
{
|
||||
return FMath::Abs(Value);
|
||||
}
|
||||
|
||||
private:
|
||||
float Value;
|
||||
};
|
||||
@@ -2305,7 +2315,17 @@ namespace
|
||||
return (P.SurfaceRoughness > 0.0f) ? EVoxelOpEffect::Both : EVoxelOpEffect::Identity;
|
||||
}
|
||||
|
||||
/** La borne d'amplitude, en unités de densité. Pas encore consommée par le pliage. */
|
||||
/**
|
||||
* ✅ Borne consommée par le pliage numérique. `RoughNoise` et `FineNoise` respectent tous
|
||||
* deux le contrat `[-1, 1]` de fBM/Ridged/Cellular, mis à l'échelle par `VOXEL_NOISE_SCALE`,
|
||||
* et `TotalRough = Rough·S + Fine·S·0.4` ⇒ `|TotalRough| ≤ 1.4 · S · SCALE`. Le fade est
|
||||
* dans `[0,1]`. Le clamp anti-remplissage ne fait que RÉDUIRE côté fill ; on ne s'appuie pas
|
||||
* dessus (il ne s'applique que dans l'air certain), donc la borne fill reste la même.
|
||||
*/
|
||||
float MaxCarveOverBox(const FBox&, const FVoxelOpContext&) const override { return MaxAmplitude(); }
|
||||
float MaxFillOverBox (const FBox&, const FVoxelOpContext&) const override { return MaxAmplitude(); }
|
||||
|
||||
/** La borne d'amplitude, en unités de densité. */
|
||||
float MaxAmplitude() const
|
||||
{
|
||||
return (P.SurfaceRoughness > 0.0f)
|
||||
@@ -2476,6 +2496,17 @@ namespace
|
||||
? EVoxelOpEffect::CarveOnly : EVoxelOpEffect::Identity;
|
||||
}
|
||||
|
||||
/** `LineValue = max(sin,0)³ ∈ [0,1]`, `Fade ∈ [0,1]` ⇒ retrait ≤ `LayerLineDepth`.
|
||||
* ⚠️ Borne calculée sur les params de la STRATE : un op `LayerLines` par salle peut écrire
|
||||
* une profondeur PLUS GRANDE, ce qui rendrait cette borne fausse dans le sens dangereux.
|
||||
* C'est la même dette que la note d'`EffectOverBox` juste au-dessus, et elle doit être
|
||||
* réglée par le même correctif — AVANT que `ClassifyTile` ne consomme `ClassifyBox`. */
|
||||
float MaxCarveOverBox(const FBox&, const FVoxelOpContext&) const override
|
||||
{
|
||||
return (P.LayerLineSpacing > 0.0f) ? FMath::Max(P.LayerLineDepth, 0.0f) : 0.0f;
|
||||
}
|
||||
float MaxFillOverBox(const FBox&, const FVoxelOpContext&) const override { return 0.0f; }
|
||||
|
||||
private:
|
||||
FStrateGenerationParams P;
|
||||
const FRoomGraphSource* Rooms; // NON possédant
|
||||
@@ -2532,6 +2563,14 @@ namespace
|
||||
? EVoxelOpEffect::FillOnly : EVoxelOpEffect::Identity;
|
||||
}
|
||||
|
||||
/** `RibValue = max(sin,0)² ∈ [0,1]`, `Fade ∈ [0,1]` ⇒ ajout ≤ `RibbingDepth`.
|
||||
* Même réserve « params de strate » que `FLayerLineMod::MaxCarveOverBox`. */
|
||||
float MaxCarveOverBox(const FBox&, const FVoxelOpContext&) const override { return 0.0f; }
|
||||
float MaxFillOverBox(const FBox&, const FVoxelOpContext&) const override
|
||||
{
|
||||
return (P.RibbingSpacing > 0.0f) ? FMath::Max(P.RibbingDepth, 0.0f) : 0.0f;
|
||||
}
|
||||
|
||||
private:
|
||||
FStrateGenerationParams P;
|
||||
const FRoomGraphSource* Rooms; // NON possédant
|
||||
@@ -2594,6 +2633,15 @@ namespace
|
||||
? EVoxelOpEffect::FillOnly : EVoxelOpEffect::Identity;
|
||||
}
|
||||
|
||||
/** fBM ∈ [-1,1] × `VOXEL_NOISE_SCALE`, lobe positif seulement, `Fade ∈ [0,1]` ⇒ ajout
|
||||
* ≤ `SCALE · Depth · Strength`. Même réserve « params de strate ». */
|
||||
float MaxCarveOverBox(const FBox&, const FVoxelOpContext&) const override { return 0.0f; }
|
||||
float MaxFillOverBox(const FBox&, const FVoxelOpContext&) const override
|
||||
{
|
||||
return VOXEL_NOISE_SCALE * FMath::Max(P.OverhangDepth, 0.0f)
|
||||
* FMath::Max(P.OverhangStrength, 0.0f);
|
||||
}
|
||||
|
||||
private:
|
||||
FStrateGenerationParams P;
|
||||
uint32 SeedU;
|
||||
@@ -2725,6 +2773,14 @@ namespace
|
||||
return (P.ScallopStrength > 0.0f) ? EVoxelOpEffect::CarveOnly : EVoxelOpEffect::Identity;
|
||||
}
|
||||
|
||||
/** `Cellular3D ∈ [-1,1]`, lobe positif seulement, `Fade ∈ [0,1]` ⇒ retrait ≤
|
||||
* `ScallopStrength`. Même réserve « params de strate ». */
|
||||
float MaxCarveOverBox(const FBox&, const FVoxelOpContext&) const override
|
||||
{
|
||||
return FMath::Max(P.ScallopStrength, 0.0f);
|
||||
}
|
||||
float MaxFillOverBox(const FBox&, const FVoxelOpContext&) const override { return 0.0f; }
|
||||
|
||||
private:
|
||||
FStrateGenerationParams P;
|
||||
uint32 SeedU;
|
||||
@@ -3223,9 +3279,28 @@ namespace
|
||||
: EVoxelOpEffect::Identity;
|
||||
}
|
||||
|
||||
/** L'amplitude max de carve, en unités de densité. Pas encore consommée par le pliage —
|
||||
* posée ici pour que la borne de `§0.2` ait déjà un domicile quand les intervalles
|
||||
* arriveront. / The bound §0.2 needs, given a home before it has a consumer. */
|
||||
/**
|
||||
* ✅ **LA BORNE DE `§0.2`, MAINTENANT CONSOMMÉE.** Elle a passé plusieurs entrées de journal
|
||||
* écrite mais inutilisée, faute d'un pliage capable de porter un nombre ; ce pliage existe.
|
||||
*
|
||||
* La preuve tient en une ligne : `t = 1 − WormValue/WormThreshold ∈ [0,1]` (le bloc ne
|
||||
* s'exécute que sous le seuil) et `NetworkMask ∈ [0,1]` par construction, donc
|
||||
* `t · WormStrength · NetworkMask ≤ WormStrength`. C'est une borne PROUVÉE, pas prudente —
|
||||
* la seule sorte qui ait le droit d'être ici : sur-estimer coûte du CPU, sous-estimer fait
|
||||
* un trou.
|
||||
*/
|
||||
float MaxCarveOverBox(const FBox&, const FVoxelOpContext&) const override
|
||||
{
|
||||
return MaxCarveAmplitude();
|
||||
}
|
||||
|
||||
/** Le ver ne REMPLIT jamais : `InOut.Density -= …` avec un terme positif. */
|
||||
float MaxFillOverBox(const FBox&, const FVoxelOpContext&) const override
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
/** L'amplitude max de carve, en unités de densité. */
|
||||
float MaxCarveAmplitude() const
|
||||
{
|
||||
return (P.WormStrength > 0.0f && P.WormThreshold > 0.0f) ? P.WormStrength : 0.0f;
|
||||
|
||||
Reference in new issue
Block a user