Test surface nets naming, orientation, welding, and normals; scope Stryker to it
Adds SurfaceNetsTests.cs and QuadNormalsTests.cs covering the sign
convention, edge naming, outward winding, vertex placement (midpoint and
unequal-crossing means, origin/cellSize), closed-sphere welding and
Euler characteristic, an ambiguous checkerboard cell's even-but-not-2 edge
usage, open-boundary quad suppression, determinism/ordering, and
face-weighted normals (sliver-vs-large-quad area weighting, unit cube
normals, zero-area/untouched-vertex degenerate case). Adds
stryker-config.json (vstest runner; this project is NUnit 4, not xunit.v3)
and records GameCult.Geometry as scoped, diff-only, in
docs/mutation-testing.md's follow-ups.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 18:06:54 +02:00
|
|
|
using System;
|
|
|
|
|
using FluentAssertions;
|
|
|
|
|
using NUnit.Framework;
|
|
|
|
|
|
|
|
|
|
namespace GameCult.Geometry.Tests
|
|
|
|
|
{
|
|
|
|
|
[TestFixture]
|
|
|
|
|
public sealed class QuadNormalsTests
|
|
|
|
|
{
|
|
|
|
|
[Test]
|
|
|
|
|
public void A_sliver_sharing_a_vertex_does_not_move_the_large_quads_normal()
|
|
|
|
|
{
|
|
|
|
|
// A 10x10 quad in the XY plane (facing +Z) sharing vertex 0 with a near-degenerate
|
2026-09-25 18:38:15 +02:00
|
|
|
// sliver tilted into the XZ plane. The sliver's area is ~1e6 times smaller. Offset away
|
|
|
|
|
// from the origin so a diagonal computed as (p2 + p0) instead of (p2 - p0) is not
|
|
|
|
|
// coincidentally the same value (p0 = 0 would erase that distinction).
|
Test surface nets naming, orientation, welding, and normals; scope Stryker to it
Adds SurfaceNetsTests.cs and QuadNormalsTests.cs covering the sign
convention, edge naming, outward winding, vertex placement (midpoint and
unequal-crossing means, origin/cellSize), closed-sphere welding and
Euler characteristic, an ambiguous checkerboard cell's even-but-not-2 edge
usage, open-boundary quad suppression, determinism/ordering, and
face-weighted normals (sliver-vs-large-quad area weighting, unit cube
normals, zero-area/untouched-vertex degenerate case). Adds
stryker-config.json (vstest runner; this project is NUnit 4, not xunit.v3)
and records GameCult.Geometry as scoped, diff-only, in
docs/mutation-testing.md's follow-ups.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 18:06:54 +02:00
|
|
|
var mesh = new CultGeometryQuadMesh
|
|
|
|
|
{
|
|
|
|
|
Positions = new[]
|
|
|
|
|
{
|
|
|
|
|
// Large quad: 0,1,2,3
|
2026-09-25 18:38:15 +02:00
|
|
|
5f, 7f, 9f,
|
|
|
|
|
15f, 7f, 9f,
|
|
|
|
|
15f, 17f, 9f,
|
|
|
|
|
5f, 17f, 9f,
|
Test surface nets naming, orientation, welding, and normals; scope Stryker to it
Adds SurfaceNetsTests.cs and QuadNormalsTests.cs covering the sign
convention, edge naming, outward winding, vertex placement (midpoint and
unequal-crossing means, origin/cellSize), closed-sphere welding and
Euler characteristic, an ambiguous checkerboard cell's even-but-not-2 edge
usage, open-boundary quad suppression, determinism/ordering, and
face-weighted normals (sliver-vs-large-quad area weighting, unit cube
normals, zero-area/untouched-vertex degenerate case). Adds
stryker-config.json (vstest runner; this project is NUnit 4, not xunit.v3)
and records GameCult.Geometry as scoped, diff-only, in
docs/mutation-testing.md's follow-ups.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 18:06:54 +02:00
|
|
|
// Sliver quad: 0 (shared), 4, 5, 6
|
2026-09-25 18:38:15 +02:00
|
|
|
5.001f, 7f, 9f,
|
|
|
|
|
5.001f, 7f, 9.001f,
|
|
|
|
|
5f, 7f, 9.001f,
|
Test surface nets naming, orientation, welding, and normals; scope Stryker to it
Adds SurfaceNetsTests.cs and QuadNormalsTests.cs covering the sign
convention, edge naming, outward winding, vertex placement (midpoint and
unequal-crossing means, origin/cellSize), closed-sphere welding and
Euler characteristic, an ambiguous checkerboard cell's even-but-not-2 edge
usage, open-boundary quad suppression, determinism/ordering, and
face-weighted normals (sliver-vs-large-quad area weighting, unit cube
normals, zero-area/untouched-vertex degenerate case). Adds
stryker-config.json (vstest runner; this project is NUnit 4, not xunit.v3)
and records GameCult.Geometry as scoped, diff-only, in
docs/mutation-testing.md's follow-ups.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 18:06:54 +02:00
|
|
|
},
|
|
|
|
|
Quads = new[]
|
|
|
|
|
{
|
|
|
|
|
0u, 1u, 2u, 3u,
|
|
|
|
|
0u, 4u, 5u, 6u,
|
|
|
|
|
},
|
|
|
|
|
QuadEdges = new[]
|
|
|
|
|
{
|
|
|
|
|
new CultGeometryGridEdge(0, 0, 0, 2),
|
|
|
|
|
new CultGeometryGridEdge(0, 0, 0, 1),
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var normals = CultGeometryQuadNormals.FaceWeighted(mesh);
|
|
|
|
|
|
|
|
|
|
normals[0].Should().BeApproximately(0f, 1e-4f);
|
|
|
|
|
normals[1].Should().BeApproximately(0f, 1e-4f);
|
|
|
|
|
normals[2].Should().BeApproximately(1f, 1e-4f);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Test]
|
2026-09-25 18:38:15 +02:00
|
|
|
public void A_closed_cube_gets_unit_length_normals_pointing_away_from_center()
|
Test surface nets naming, orientation, welding, and normals; scope Stryker to it
Adds SurfaceNetsTests.cs and QuadNormalsTests.cs covering the sign
convention, edge naming, outward winding, vertex placement (midpoint and
unequal-crossing means, origin/cellSize), closed-sphere welding and
Euler characteristic, an ambiguous checkerboard cell's even-but-not-2 edge
usage, open-boundary quad suppression, determinism/ordering, and
face-weighted normals (sliver-vs-large-quad area weighting, unit cube
normals, zero-area/untouched-vertex degenerate case). Adds
stryker-config.json (vstest runner; this project is NUnit 4, not xunit.v3)
and records GameCult.Geometry as scoped, diff-only, in
docs/mutation-testing.md's follow-ups.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 18:06:54 +02:00
|
|
|
{
|
|
|
|
|
var mesh = Cube();
|
|
|
|
|
|
|
|
|
|
var normals = CultGeometryQuadNormals.FaceWeighted(mesh);
|
|
|
|
|
|
2026-09-25 18:38:15 +02:00
|
|
|
// Each vertex's normal must point away from the cube's center (0.5,0.5,0.5) along every
|
|
|
|
|
// axis, i.e. its sign must match the vertex's own offset from the center. Checking the
|
|
|
|
|
// exact direction (not just the length) catches a mutant that flips the sign of one of a
|
|
|
|
|
// vertex's three contributing faces: three orthogonal +-2-length vectors summed have the
|
|
|
|
|
// same length no matter which one is negated, so a length-only check cannot tell them apart.
|
|
|
|
|
var oneOverRoot3 = 1f / MathF.Sqrt(3f);
|
Test surface nets naming, orientation, welding, and normals; scope Stryker to it
Adds SurfaceNetsTests.cs and QuadNormalsTests.cs covering the sign
convention, edge naming, outward winding, vertex placement (midpoint and
unequal-crossing means, origin/cellSize), closed-sphere welding and
Euler characteristic, an ambiguous checkerboard cell's even-but-not-2 edge
usage, open-boundary quad suppression, determinism/ordering, and
face-weighted normals (sliver-vs-large-quad area weighting, unit cube
normals, zero-area/untouched-vertex degenerate case). Adds
stryker-config.json (vstest runner; this project is NUnit 4, not xunit.v3)
and records GameCult.Geometry as scoped, diff-only, in
docs/mutation-testing.md's follow-ups.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 18:06:54 +02:00
|
|
|
for (var vertex = 0; vertex < mesh.VertexCount; vertex++)
|
|
|
|
|
{
|
2026-09-25 18:38:15 +02:00
|
|
|
var px = mesh.Positions[vertex * 3];
|
|
|
|
|
var py = mesh.Positions[(vertex * 3) + 1];
|
|
|
|
|
var pz = mesh.Positions[(vertex * 3) + 2];
|
|
|
|
|
var expected = new[]
|
|
|
|
|
{
|
|
|
|
|
px > 0.5f ? oneOverRoot3 : -oneOverRoot3,
|
|
|
|
|
py > 0.5f ? oneOverRoot3 : -oneOverRoot3,
|
|
|
|
|
pz > 0.5f ? oneOverRoot3 : -oneOverRoot3,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
normals[vertex * 3].Should().BeApproximately(expected[0], 1e-5f);
|
|
|
|
|
normals[(vertex * 3) + 1].Should().BeApproximately(expected[1], 1e-5f);
|
|
|
|
|
normals[(vertex * 3) + 2].Should().BeApproximately(expected[2], 1e-5f);
|
Test surface nets naming, orientation, welding, and normals; scope Stryker to it
Adds SurfaceNetsTests.cs and QuadNormalsTests.cs covering the sign
convention, edge naming, outward winding, vertex placement (midpoint and
unequal-crossing means, origin/cellSize), closed-sphere welding and
Euler characteristic, an ambiguous checkerboard cell's even-but-not-2 edge
usage, open-boundary quad suppression, determinism/ordering, and
face-weighted normals (sliver-vs-large-quad area weighting, unit cube
normals, zero-area/untouched-vertex degenerate case). Adds
stryker-config.json (vstest runner; this project is NUnit 4, not xunit.v3)
and records GameCult.Geometry as scoped, diff-only, in
docs/mutation-testing.md's follow-ups.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 18:06:54 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
public void A_zero_area_quad_contributes_nothing_and_an_untouched_vertex_is_a_finite_zero()
|
|
|
|
|
{
|
|
|
|
|
var mesh = new CultGeometryQuadMesh
|
|
|
|
|
{
|
|
|
|
|
// Vertex 4 is never referenced by any quad.
|
|
|
|
|
Positions = new[]
|
|
|
|
|
{
|
|
|
|
|
0f, 0f, 0f,
|
|
|
|
|
1f, 0f, 0f,
|
|
|
|
|
2f, 0f, 0f,
|
|
|
|
|
3f, 0f, 0f,
|
|
|
|
|
9f, 9f, 9f,
|
|
|
|
|
},
|
|
|
|
|
Quads = new[] { 0u, 1u, 2u, 3u },
|
|
|
|
|
QuadEdges = new[] { new CultGeometryGridEdge(0, 0, 0, 0) },
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var normals = CultGeometryQuadNormals.FaceWeighted(mesh);
|
|
|
|
|
|
|
|
|
|
normals.Should().HaveCount(15);
|
|
|
|
|
normals.Should().OnlyContain(value => value == 0f);
|
2026-09-25 18:10:19 +02:00
|
|
|
normals.Should().OnlyContain(value => float.IsFinite(value));
|
Test surface nets naming, orientation, welding, and normals; scope Stryker to it
Adds SurfaceNetsTests.cs and QuadNormalsTests.cs covering the sign
convention, edge naming, outward winding, vertex placement (midpoint and
unequal-crossing means, origin/cellSize), closed-sphere welding and
Euler characteristic, an ambiguous checkerboard cell's even-but-not-2 edge
usage, open-boundary quad suppression, determinism/ordering, and
face-weighted normals (sliver-vs-large-quad area weighting, unit cube
normals, zero-area/untouched-vertex degenerate case). Adds
stryker-config.json (vstest runner; this project is NUnit 4, not xunit.v3)
and records GameCult.Geometry as scoped, diff-only, in
docs/mutation-testing.md's follow-ups.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 18:06:54 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
public void Null_mesh_is_rejected()
|
|
|
|
|
{
|
|
|
|
|
Action act = () => CultGeometryQuadNormals.FaceWeighted(null!);
|
|
|
|
|
|
|
|
|
|
act.Should().Throw<ArgumentNullException>();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static CultGeometryQuadMesh Cube()
|
|
|
|
|
{
|
|
|
|
|
// 0..7: (0,0,0) (1,0,0) (1,1,0) (0,1,0) (0,0,1) (1,0,1) (1,1,1) (0,1,1)
|
|
|
|
|
var positions = new[]
|
|
|
|
|
{
|
|
|
|
|
0f, 0f, 0f,
|
|
|
|
|
1f, 0f, 0f,
|
|
|
|
|
1f, 1f, 0f,
|
|
|
|
|
0f, 1f, 0f,
|
|
|
|
|
0f, 0f, 1f,
|
|
|
|
|
1f, 0f, 1f,
|
|
|
|
|
1f, 1f, 1f,
|
|
|
|
|
0f, 1f, 1f,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Each face ordered so d1 x d2 points outward.
|
|
|
|
|
var quads = new[]
|
|
|
|
|
{
|
|
|
|
|
0u, 3u, 2u, 1u, // -Z
|
|
|
|
|
4u, 5u, 6u, 7u, // +Z
|
|
|
|
|
0u, 1u, 5u, 4u, // -Y
|
|
|
|
|
3u, 7u, 6u, 2u, // +Y
|
|
|
|
|
0u, 4u, 7u, 3u, // -X
|
|
|
|
|
1u, 2u, 6u, 5u, // +X
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var quadEdges = new[]
|
|
|
|
|
{
|
|
|
|
|
new CultGeometryGridEdge(0, 0, 0, 0),
|
|
|
|
|
new CultGeometryGridEdge(0, 0, 0, 1),
|
|
|
|
|
new CultGeometryGridEdge(0, 0, 0, 2),
|
|
|
|
|
new CultGeometryGridEdge(0, 1, 0, 0),
|
|
|
|
|
new CultGeometryGridEdge(1, 0, 0, 1),
|
|
|
|
|
new CultGeometryGridEdge(0, 0, 1, 2),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return new CultGeometryQuadMesh
|
|
|
|
|
{
|
|
|
|
|
Positions = positions,
|
|
|
|
|
Quads = quads,
|
|
|
|
|
QuadEdges = quadEdges,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|