CultLib/tests/GameCult.Geometry.Tests/IsoSurfaceTests.cs
Meta Vens 20ac3b0b6c Collapse CultVec2, CultVec3 and CultRect into CultMath float2, float3 and rect
GameCult.Geometry no longer carries vector or rectangle values. CultCircle and
CultSphere hold float2/float3, the isosurface and surface-nets extractors take
float3 origin, and the per-call conversion to float3 is gone. Their MessagePack
bytes are unchanged (pinned against the pre-collapse encodings), now produced
by CultMathResolver through the default CultCache options.

This is a breaking change to the GameCult.Geometry public API.

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

107 lines
3.5 KiB
C#

using System;
using CultMath;
using FluentAssertions;
using NUnit.Framework;
namespace GameCult.Geometry.Tests
{
[TestFixture]
public sealed class IsoSurfaceTests
{
[TestCase(-1f)]
[TestCase(1f)]
public void Uniform_field_has_no_surface(float value)
{
var samples = Filled(2, value);
var mesh = CultGeometryIsoSurface.Extract(samples);
mesh.Positions.Should().BeEmpty();
mesh.Normals.Should().BeEmpty();
mesh.Indices.Should().BeEmpty();
}
[Test]
public void Plane_field_interpolates_to_the_isovalue_and_faces_outward()
{
var samples = new float[2, 2, 2];
for (var y = 0; y < 2; y++)
for (var z = 0; z < 2; z++)
{
samples[0, y, z] = -0.5f;
samples[1, y, z] = 0.5f;
}
var mesh = CultGeometryIsoSurface.Extract(samples);
mesh.TriangleCount.Should().BeGreaterThan(0);
for (var index = 0; index < mesh.Positions.Length; index += 3)
{
mesh.Positions[index].Should().BeApproximately(0.5f, 1e-6f);
}
for (var index = 0; index < mesh.Normals.Length; index += 3)
{
mesh.Normals[index].Should().BeApproximately(1f, 1e-6f);
mesh.Normals[index + 1].Should().BeApproximately(0f, 1e-6f);
mesh.Normals[index + 2].Should().BeApproximately(0f, 1e-6f);
}
}
[Test]
public void Samples_on_the_isovalue_emit_finite_normals()
{
var samples = new float[2, 2, 2];
for (var y = 0; y < 2; y++)
for (var z = 0; z < 2; z++)
{
samples[0, y, z] = 0f;
samples[1, y, z] = 1f;
}
var mesh = CultGeometryIsoSurface.Extract(samples);
mesh.TriangleCount.Should().BeGreaterThan(0);
mesh.Normals.Should().OnlyContain(value => float.IsFinite(value));
}
[Test]
public void Extraction_is_deterministic_and_applies_origin_and_cell_size()
{
var samples = new float[2, 2, 2];
samples[0, 0, 0] = -1f;
samples[1, 0, 0] = 1f;
samples[0, 1, 0] = 1f;
samples[1, 1, 0] = 1f;
samples[0, 0, 1] = 1f;
samples[1, 0, 1] = 1f;
samples[0, 1, 1] = 1f;
samples[1, 1, 1] = 1f;
var first = CultGeometryIsoSurface.Extract(samples, origin: new float3(10f, 20f, 30f), cellSize: 2f);
var second = CultGeometryIsoSurface.Extract(samples, origin: new float3(10f, 20f, 30f), cellSize: 2f);
second.Positions.Should().Equal(first.Positions);
second.Normals.Should().Equal(first.Normals);
second.Indices.Should().Equal(first.Indices);
first.Positions.Should().OnlyContain(value => value >= 10f);
}
[Test]
public void Invalid_field_shape_is_rejected()
{
Action act = () => CultGeometryIsoSurface.Extract(new float[1, 2, 2]);
act.Should().Throw<ArgumentException>();
}
private static float[,,] Filled(int size, float value)
{
var samples = new float[size, size, size];
for (var x = 0; x < size; x++)
for (var y = 0; y < size; y++)
for (var z = 0; z < size; z++) samples[x, y, z] = value;
return samples;
}
}
}