CultLib/src/GameCult.Geometry/CultGeometryQuadMesh.cs
Meta Vens 967142e413 Fix surface-nets mutation findings F1/F3/F4/F7/F8 (production code)
F1: Extract's locals (position, v0..v3) were assigned through pattern-match
and short-circuited out-var chains whose definite-assignment analysis breaks
under a single mutated operator, so Stryker's Safe Mode dropped all 98
mutants in Extract as compile errors. Restructured to plain null checks and
a for-loop over a uint[4] so every local is unconditionally assigned; no
mutation of a boolean operator can make it fail to compile.

F3: deleted the `<= 1e-20f` fallback in Interpolate. A straddling edge always
has delta > 0 (.NET preserves subnormals), so the guard only ever replaced a
correct interpolated value with a wrong 0.5 midpoint.

F4: deleted the per-quad geometric cross-product winding test. Orientation
now comes only from the topological inside/outside flag plus a fixed
per-axis handedness sign (OrientationSign; axis 1's (X, Z) perpendicular
pairing is the one left-handed case, X x Z == -Y). This is now determined
purely by which endpoint of the crossing edge is inside and the axis, never
by quad geometry, so degenerate (zero-area) quads orient consistently with
their neighbours instead of falling back to an unstable dot-product sign.

F7: Extract now rejects any non-finite sample (NaN or +-Infinity) with
ArgumentException, in the same style as the existing shape/cellSize checks.

F8: collapsed three duplicated (edge, axis) mappings - TryGetOtherEndpoint's
per-axis switch, the three Axis*EdgeStarts tables plus the axis==0?1:0
ternaries in AccumulateAxisEdges, and TryGetQuadCells' three axis-specific
corner blocks - into one PerpAxes/PerimeterLoop table pair, used by all
three. Replaced the three duplicated `<= isoValue` inside tests with one
IsInside predicate. Deleted the per-axis trailing `InRange(x, cellsX)` /
`InRange(y, cellsY)` / `InRange(z, cellsZ)` checks (provably always true,
since TryGetOtherEndpoint already guarantees the edge's own-axis coordinate
indexes a valid cell) and the explicit TryGetValue+throw invariant guard
(the four cells around a crossing edge are proven to always have a vertex;
a direct dictionary index now fails loudly via KeyNotFoundException if that
invariant is ever actually violated, with no reachable branch to maintain).

F6: deleted CultGeometryGridEdge's <, >, <=, >= operators (CultGeometryQuadMesh.cs).
Nothing in production or tests used them; ordering goes through CompareTo.
!= is kept only because C# requires it whenever == is defined (CS0216).

Builds clean; does not touch CultGeometryIsoSurface.cs, CultGeometryDocuments.cs,
or ISOSURFACE-PROVENANCE.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 19:29:45 +02:00

89 lines
3.6 KiB
C#

using System;
namespace GameCult.Geometry
{
/// <summary>
/// The grid edge from sample (X, Y, Z) to (X, Y, Z) + e_Axis, where Axis 0/1/2 is X/Y/Z.
/// </summary>
public readonly struct CultGeometryGridEdge : IEquatable<CultGeometryGridEdge>, IComparable<CultGeometryGridEdge>
{
/// <summary>Creates a grid edge.</summary>
public CultGeometryGridEdge(int x, int y, int z, byte axis)
{
X = x;
Y = y;
Z = z;
Axis = axis;
}
/// <summary>Gets the starting sample's x coordinate.</summary>
public int X { get; }
/// <summary>Gets the starting sample's y coordinate.</summary>
public int Y { get; }
/// <summary>Gets the starting sample's z coordinate.</summary>
public int Z { get; }
/// <summary>Gets the axis the edge runs along: 0 for x, 1 for y, 2 for z.</summary>
public byte Axis { get; }
/// <summary>Returns whether this edge is equal to another edge.</summary>
public bool Equals(CultGeometryGridEdge other) =>
X == other.X && Y == other.Y && Z == other.Z && Axis == other.Axis;
/// <inheritdoc />
public override bool Equals(object? obj) => obj is CultGeometryGridEdge other && Equals(other);
/// <inheritdoc />
public override int GetHashCode() => HashCode.Combine(X, Y, Z, Axis);
/// <summary>Compares edges by (X, Y, Z, Axis) in that order.</summary>
public int CompareTo(CultGeometryGridEdge other)
{
var byX = X.CompareTo(other.X);
if (byX != 0) return byX;
var byY = Y.CompareTo(other.Y);
if (byY != 0) return byY;
var byZ = Z.CompareTo(other.Z);
if (byZ != 0) return byZ;
return Axis.CompareTo(other.Axis);
}
/// <inheritdoc />
public override string ToString() => $"({X}, {Y}, {Z})+e{Axis}";
// The relational operators (<, >, <=, >=) had no caller and no test anywhere in the repo:
// ordering goes through CompareTo (see
// Extraction_is_deterministic_and_quad_edges_are_strictly_ascending), so they were deleted
// rather than carried as untested surface. != is kept only because C# requires it whenever
// == is defined (CS0216); it is exercised directly by a test.
public static bool operator ==(CultGeometryGridEdge left, CultGeometryGridEdge right) => left.Equals(right);
public static bool operator !=(CultGeometryGridEdge left, CultGeometryGridEdge right) => !left.Equals(right);
}
/// <summary>
/// An engine-neutral quad mesh with welded vertices, produced by surface nets extraction.
/// Not a CultCache document: this is an in-process algorithm result, not a persisted artifact.
/// </summary>
public sealed class CultGeometryQuadMesh
{
/// <summary>Gets or sets the welded vertex positions, xyz per vertex.</summary>
public float[] Positions { get; set; } = Array.Empty<float>();
/// <summary>Gets or sets the quad vertex indices, 4 per quad.</summary>
public uint[] Quads { get; set; } = Array.Empty<uint>();
/// <summary>Gets or sets the grid edge each quad was emitted for, one per quad, strictly ascending.</summary>
public CultGeometryGridEdge[] QuadEdges { get; set; } = Array.Empty<CultGeometryGridEdge>();
/// <summary>Gets the number of quads.</summary>
public int QuadCount => Quads.Length / 4;
/// <summary>Gets the number of welded vertices.</summary>
public int VertexCount => Positions.Length / 3;
}
}