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>
This commit is contained in:
Meta Vens 2026-09-29 16:18:09 +02:00
commit 20ac3b0b6c
8 changed files with 95 additions and 298 deletions

View file

@ -9,6 +9,7 @@ format for the attribute-first `GameCult.Caching` stack.
- `CultDocumentMessagePackSerialization`
- `CultDocumentResolver`
- `CultRecordRefFormatter<T>`
- `CultMathResolver`: the canonical component-array encoding of every public CultMath value type, part of the default resolver chain
Cult document payloads serialize through `MessagePackSerializer` with the
options of the document's assembly (`CultDocumentMessagePackSerialization.OptionsFor`),

View file

@ -39,7 +39,7 @@ namespace GameCult.Geometry
public static CultGeometryTriangleMesh Extract(
float[,,] samples,
float isoValue = 0f,
CultVec3 origin = default,
float3 origin = default,
float cellSize = 1f)
{
if (samples == null) throw new ArgumentNullException(nameof(samples));
@ -56,7 +56,6 @@ namespace GameCult.Geometry
var positions = new List<float>();
var normals = new List<float>();
var indices = new List<uint>();
var originValue = new float3(origin.X, origin.Y, origin.Z);
var cornerPositions = new float3[8];
var cornerValues = new float[8];
var tetraPositions = new float3[4];
@ -66,7 +65,7 @@ namespace GameCult.Geometry
for (var y = 0; y < samples.GetLength(1) - 1; y++)
for (var z = 0; z < samples.GetLength(2) - 1; z++)
{
var cubeOrigin = originValue + new float3(x, y, z) * cellSize;
var cubeOrigin = origin + new float3(x, y, z) * cellSize;
for (var corner = 0; corner < CubeCorners.Length; corner++)
{
var offset = CubeCorners[corner];

View file

@ -1,197 +1,10 @@
using System;
using System.Text.Json.Serialization;
using CultMath;
using MessagePack;
namespace GameCult.Geometry
{
/// <summary>
/// Deterministic two-dimensional float vector for cross-runtime geometry and query contracts.
/// </summary>
[MessagePackObject]
public readonly struct CultVec2 : IEquatable<CultVec2>
{
/// <summary>Creates a two-dimensional vector.</summary>
public CultVec2(float x, float y)
{
X = x;
Y = y;
}
/// <summary>Gets the x component.</summary>
[Key(0)]
public float X { get; }
/// <summary>Gets the y component.</summary>
[Key(1)]
public float Y { get; }
/// <summary>Gets the zero vector.</summary>
[IgnoreMember]
[JsonIgnore]
public static CultVec2 Zero => new(0f, 0f);
/// <summary>Gets the squared vector length.</summary>
[IgnoreMember]
[JsonIgnore]
public float LengthSquared => (X * X) + (Y * Y);
/// <summary>Returns whether this vector is equal to another vector.</summary>
public bool Equals(CultVec2 other) => X.Equals(other.X) && Y.Equals(other.Y);
/// <inheritdoc />
public override bool Equals(object? obj) => obj is CultVec2 other && Equals(other);
/// <inheritdoc />
public override int GetHashCode() => HashCode.Combine(X, Y);
/// <inheritdoc />
public override string ToString() => $"({X}, {Y})";
public static CultVec2 operator +(CultVec2 left, CultVec2 right) => new(left.X + right.X, left.Y + right.Y);
public static CultVec2 operator -(CultVec2 left, CultVec2 right) => new(left.X - right.X, left.Y - right.Y);
public static CultVec2 operator *(CultVec2 value, float scalar) => new(value.X * scalar, value.Y * scalar);
public static CultVec2 operator /(CultVec2 value, float scalar) => new(value.X / scalar, value.Y / scalar);
public static bool operator ==(CultVec2 left, CultVec2 right) => left.Equals(right);
public static bool operator !=(CultVec2 left, CultVec2 right) => !left.Equals(right);
}
/// <summary>
/// Deterministic three-dimensional float vector for cross-runtime geometry and query contracts.
/// </summary>
[MessagePackObject]
public readonly struct CultVec3 : IEquatable<CultVec3>
{
/// <summary>Creates a three-dimensional vector.</summary>
public CultVec3(float x, float y, float z)
{
X = x;
Y = y;
Z = z;
}
/// <summary>Gets the x component.</summary>
[Key(0)]
public float X { get; }
/// <summary>Gets the y component.</summary>
[Key(1)]
public float Y { get; }
/// <summary>Gets the z component.</summary>
[Key(2)]
public float Z { get; }
/// <summary>Gets the zero vector.</summary>
[IgnoreMember]
[JsonIgnore]
public static CultVec3 Zero => new(0f, 0f, 0f);
/// <summary>Gets the XY plane projection.</summary>
[IgnoreMember]
[JsonIgnore]
public CultVec2 Xy => new(X, Y);
/// <summary>Gets the XZ plane projection for Unity compatibility adapters.</summary>
[IgnoreMember]
[JsonIgnore]
public CultVec2 Xz => new(X, Z);
/// <summary>Gets the squared vector length.</summary>
[IgnoreMember]
[JsonIgnore]
public float LengthSquared => (X * X) + (Y * Y) + (Z * Z);
/// <summary>Returns whether this vector is equal to another vector.</summary>
public bool Equals(CultVec3 other) => X.Equals(other.X) && Y.Equals(other.Y) && Z.Equals(other.Z);
/// <inheritdoc />
public override bool Equals(object? obj) => obj is CultVec3 other && Equals(other);
/// <inheritdoc />
public override int GetHashCode() => HashCode.Combine(X, Y, Z);
/// <inheritdoc />
public override string ToString() => $"({X}, {Y}, {Z})";
public static CultVec3 operator +(CultVec3 left, CultVec3 right) => new(left.X + right.X, left.Y + right.Y, left.Z + right.Z);
public static CultVec3 operator -(CultVec3 left, CultVec3 right) => new(left.X - right.X, left.Y - right.Y, left.Z - right.Z);
public static CultVec3 operator *(CultVec3 value, float scalar) => new(value.X * scalar, value.Y * scalar, value.Z * scalar);
public static CultVec3 operator /(CultVec3 value, float scalar) => new(value.X / scalar, value.Y / scalar, value.Z / scalar);
public static bool operator ==(CultVec3 left, CultVec3 right) => left.Equals(right);
public static bool operator !=(CultVec3 left, CultVec3 right) => !left.Equals(right);
}
/// <summary>
/// Axis-aligned two-dimensional rectangle represented by canonical min and max corners.
/// </summary>
[MessagePackObject]
public readonly struct CultRect : IEquatable<CultRect>
{
/// <summary>Creates a rectangle and canonicalizes min/max corners.</summary>
public CultRect(CultVec2 min, CultVec2 max)
{
Min = new CultVec2(Math.Min(min.X, max.X), Math.Min(min.Y, max.Y));
Max = new CultVec2(Math.Max(min.X, max.X), Math.Max(min.Y, max.Y));
}
/// <summary>Creates a rectangle and canonicalizes min/max corners.</summary>
public CultRect(float minX, float minY, float maxX, float maxY)
: this(new CultVec2(minX, minY), new CultVec2(maxX, maxY))
{
}
/// <summary>Gets the canonical minimum corner.</summary>
[Key(0)]
public CultVec2 Min { get; }
/// <summary>Gets the canonical maximum corner.</summary>
[Key(1)]
public CultVec2 Max { get; }
/// <summary>Gets the rectangle size.</summary>
[IgnoreMember]
[JsonIgnore]
public CultVec2 Size => Max - Min;
/// <summary>Gets the rectangle center.</summary>
[IgnoreMember]
[JsonIgnore]
public CultVec2 Center => (Min + Max) * 0.5f;
/// <summary>Returns whether the point lies inside or on the rectangle edge.</summary>
public bool Contains(CultVec2 point)
{
return point.X >= Min.X &&
point.X <= Max.X &&
point.Y >= Min.Y &&
point.Y <= Max.Y;
}
/// <summary>Returns whether this rectangle intersects another rectangle.</summary>
public bool Intersects(CultRect other)
{
return Min.X <= other.Max.X &&
Max.X >= other.Min.X &&
Min.Y <= other.Max.Y &&
Max.Y >= other.Min.Y;
}
/// <summary>Returns whether this rectangle is equal to another rectangle.</summary>
public bool Equals(CultRect other) => Min == other.Min && Max == other.Max;
/// <inheritdoc />
public override bool Equals(object? obj) => obj is CultRect other && Equals(other);
/// <inheritdoc />
public override int GetHashCode() => HashCode.Combine(Min, Max);
/// <inheritdoc />
public override string ToString() => $"[{Min}..{Max}]";
public static bool operator ==(CultRect left, CultRect right) => left.Equals(right);
public static bool operator !=(CultRect left, CultRect right) => !left.Equals(right);
}
/// <summary>
/// Two-dimensional circle for spatial queries.
/// </summary>
@ -199,7 +12,7 @@ namespace GameCult.Geometry
public readonly struct CultCircle : IEquatable<CultCircle>
{
/// <summary>Creates a circle.</summary>
public CultCircle(CultVec2 center, float radius)
public CultCircle(float2 center, float radius)
{
if (radius < 0f)
{
@ -212,7 +25,7 @@ namespace GameCult.Geometry
/// <summary>Gets the circle center.</summary>
[Key(0)]
public CultVec2 Center { get; }
public float2 Center { get; }
/// <summary>Gets the circle radius.</summary>
[Key(1)]
@ -221,29 +34,29 @@ namespace GameCult.Geometry
/// <summary>Gets the circle bounds.</summary>
[IgnoreMember]
[JsonIgnore]
public CultRect Bounds => new(
Center.X - Radius,
Center.Y - Radius,
Center.X + Radius,
Center.Y + Radius);
public rect Bounds => new(
Center.x - Radius,
Center.y - Radius,
Center.x + Radius,
Center.y + Radius);
/// <summary>Returns whether the point lies inside or on the circle edge.</summary>
public bool Contains(CultVec2 point)
public bool Contains(float2 point)
{
var delta = point - Center;
return delta.LengthSquared <= Radius * Radius;
return math.lengthsq(delta) <= Radius * Radius;
}
/// <summary>Returns whether this circle intersects a rectangle.</summary>
public bool Intersects(CultRect rect)
public bool Intersects(rect rect)
{
var clampedX = Math.Max(rect.Min.X, Math.Min(Center.X, rect.Max.X));
var clampedY = Math.Max(rect.Min.Y, Math.Min(Center.Y, rect.Max.Y));
return Contains(new CultVec2(clampedX, clampedY));
var clampedX = Math.Max(rect.min.x, Math.Min(Center.x, rect.max.x));
var clampedY = Math.Max(rect.min.y, Math.Min(Center.y, rect.max.y));
return Contains(new float2(clampedX, clampedY));
}
/// <summary>Returns whether this circle is equal to another circle.</summary>
public bool Equals(CultCircle other) => Center == other.Center && Radius.Equals(other.Radius);
public bool Equals(CultCircle other) => Center.Equals(other.Center) && Radius.Equals(other.Radius);
/// <inheritdoc />
public override bool Equals(object? obj) => obj is CultCircle other && Equals(other);
@ -262,7 +75,7 @@ namespace GameCult.Geometry
public readonly struct CultSphere : IEquatable<CultSphere>
{
/// <summary>Creates a sphere.</summary>
public CultSphere(CultVec3 center, float radius)
public CultSphere(float3 center, float radius)
{
if (radius < 0f)
{
@ -275,7 +88,7 @@ namespace GameCult.Geometry
/// <summary>Gets the sphere center.</summary>
[Key(0)]
public CultVec3 Center { get; }
public float3 Center { get; }
/// <summary>Gets the sphere radius.</summary>
[Key(1)]
@ -284,17 +97,17 @@ namespace GameCult.Geometry
/// <summary>Gets the XY circle projection.</summary>
[IgnoreMember]
[JsonIgnore]
public CultCircle XyCircle => new(Center.Xy, Radius);
public CultCircle XyCircle => new(Center.xy, Radius);
/// <summary>Returns whether the point lies inside or on the sphere surface.</summary>
public bool Contains(CultVec3 point)
public bool Contains(float3 point)
{
var delta = point - Center;
return delta.LengthSquared <= Radius * Radius;
return math.lengthsq(delta) <= Radius * Radius;
}
/// <summary>Returns whether this sphere is equal to another sphere.</summary>
public bool Equals(CultSphere other) => Center == other.Center && Radius.Equals(other.Radius);
public bool Equals(CultSphere other) => Center.Equals(other.Center) && Radius.Equals(other.Radius);
/// <inheritdoc />
public override bool Equals(object? obj) => obj is CultSphere other && Equals(other);

View file

@ -44,7 +44,7 @@ namespace GameCult.Geometry
public static CultGeometryQuadMesh Extract(
float[,,] samples,
float isoValue = 0f,
CultVec3 origin = default,
float3 origin = default,
float cellSize = 1f)
{
if (samples == null) throw new ArgumentNullException(nameof(samples));
@ -66,7 +66,7 @@ namespace GameCult.Geometry
throw new ArgumentException("The isovalue must be finite.", nameof(isoValue));
}
if (!float.IsFinite(origin.X) || !float.IsFinite(origin.Y) || !float.IsFinite(origin.Z))
if (!float.IsFinite(origin.x) || !float.IsFinite(origin.y) || !float.IsFinite(origin.z))
{
throw new ArgumentException("The origin must be finite.", nameof(origin));
}
@ -80,7 +80,6 @@ namespace GameCult.Geometry
var sizeY = samples.GetLength(1);
var sizeZ = samples.GetLength(2);
var cellCounts = new[] { sizeX - 1, sizeY - 1, sizeZ - 1 };
var originValue = new float3(origin.X, origin.Y, origin.Z);
var vertexIndex = new Dictionary<(int, int, int), uint>();
var positions = new List<float>();
@ -90,7 +89,7 @@ namespace GameCult.Geometry
for (var cz = 0; cz < cellCounts[2]; cz++)
{
var cellCoord = new[] { cx, cy, cz };
var vertex = CellVertex(samples, cellCoord, isoValue, originValue, cellSize);
var vertex = CellVertex(samples, cellCoord, isoValue, origin, cellSize);
if (vertex == null) continue;
vertexIndex[(cx, cy, cz)] = (uint)(positions.Count / 3);

View file

@ -31,12 +31,9 @@ The package is deliberately substrate-shaped:
## Value Primitives
The small value layer is for cross-runtime query contracts, physics probes,
native slices, and UI surfaces:
native slices, and UI surfaces. Vectors and rectangles are CultMath's `float2`,
`float3` and `rect`, and `GameCult.Caching.MessagePack` serializes them.
- `CultVec2` and `CultVec3`: whole-vector values for positions, velocities,
accelerations, and query inputs. SoA layouts may store them efficiently, but
the semantic API does not force callers to split x/y/z into unrelated fields.
- `CultRect`: canonical XY viewport/query rectangle stored as min/max corners.
- `CultCircle`: 2D influence/query brush with rect intersection helpers.
- `CultSphere`: 3D query primitive with whole-vector center and XY projection.
@ -85,8 +82,8 @@ cache miss; that is split-brain geometry.
## Documents
- `CultVec2`, `CultVec3`, `CultRect`, `CultCircle`, `CultSphere`: shared value
primitives for query and view contracts.
- `CultCircle`, `CultSphere`: shared value primitives for query and view
contracts.
- `CultGeometryDomainDocument`: one hierarchical domain tree, suitable for
Ragnarok/Fensalir-style feature DSL output.
- `CultGeometryBuildRequest`: one LOD/frustum/budget request for workers.

View file

@ -1,4 +1,5 @@
using FluentAssertions;
using CultMath;
using GameCult.Caching;
using GameCult.Caching.MessagePack;
using GameCult.Mesh;
@ -19,54 +20,50 @@ namespace GameCult.Geometry.Tests
[Test]
public void GeometryPrimitives_CanonicalizeAndIntersect()
{
var viewport = new CultRect(8f, 4f, -8f, -4f);
var viewport = new rect(8f, 4f, -8f, -4f);
viewport.Min.Should().Be(new CultVec2(-8f, -4f));
viewport.Max.Should().Be(new CultVec2(8f, 4f));
viewport.Center.Should().Be(CultVec2.Zero);
viewport.Contains(new CultVec2(2f, 3f)).Should().BeTrue();
viewport.Intersects(new CultRect(7f, 3f, 10f, 5f)).Should().BeTrue();
viewport.Intersects(new CultRect(9f, 5f, 10f, 6f)).Should().BeFalse();
viewport.min.Should().Be(new float2(-8f, -4f));
viewport.max.Should().Be(new float2(8f, 4f));
viewport.center.Should().Be(float2.zero);
viewport.Contains(new float2(2f, 3f)).Should().BeTrue();
viewport.Intersects(new rect(7f, 3f, 10f, 5f)).Should().BeTrue();
viewport.Intersects(new rect(9f, 5f, 10f, 6f)).Should().BeFalse();
var gravityBrush = new CultCircle(new CultVec2(10f, 0f), 3f);
var gravityBrush = new CultCircle(new float2(10f, 0f), 3f);
gravityBrush.Intersects(viewport).Should().BeTrue();
gravityBrush.Contains(new CultVec2(12f, 0f)).Should().BeTrue();
gravityBrush.Contains(new CultVec2(14f, 0f)).Should().BeFalse();
gravityBrush.Contains(new float2(12f, 0f)).Should().BeTrue();
gravityBrush.Contains(new float2(14f, 0f)).Should().BeFalse();
}
[Test]
public void GeometryPrimitives_PreserveWholeVectorsForSoaAndPhysicsQueries()
{
var sphere = new CultSphere(new CultVec3(1f, 2f, 3f), 5f);
var sphere = new CultSphere(new float3(1f, 2f, 3f), 5f);
sphere.Contains(new CultVec3(1f, 6f, 3f)).Should().BeTrue();
sphere.Contains(new CultVec3(1f, 8f, 3f)).Should().BeFalse();
sphere.Center.Xy.Should().Be(new CultVec2(1f, 2f));
sphere.Center.Xz.Should().Be(new CultVec2(1f, 3f));
sphere.XyCircle.Bounds.Should().Be(new CultRect(-4f, -3f, 6f, 7f));
sphere.Contains(new float3(1f, 6f, 3f)).Should().BeTrue();
sphere.Contains(new float3(1f, 8f, 3f)).Should().BeFalse();
sphere.Center.xy.Should().Be(new float2(1f, 2f));
sphere.Center.xz.Should().Be(new float2(1f, 3f));
sphere.XyCircle.Bounds.Should().Be(new rect(-4f, -3f, 6f, 7f));
}
// The bytes CultCircle and CultSphere produced when they held CultVec2/CultVec3 (probed at 65311c9).
[Test]
public void GeometryPrimitives_RoundTripThroughMessagePack()
public void GeometryPrimitives_KeepTheirPreCollapseBytes()
{
var rect = new CultRect(new CultVec2(5f, -2f), new CultVec2(-1f, 7f));
var payload = MessagePackSerializer.Serialize(rect);
var decoded = MessagePackSerializer.Deserialize<CultRect>(payload);
var options = CultDocumentMessagePackSerialization.Options;
var circle = new CultCircle(new float2(1f, 2f), 3f);
var sphere = new CultSphere(new float3(1f, 2f, 3f), 5f);
decoded.Should().Be(rect);
decoded.Min.Should().Be(new CultVec2(-1f, -2f));
decoded.Max.Should().Be(new CultVec2(5f, 7f));
}
Convert.ToHexString(MessagePackSerializer.Serialize(circle, options))
.Should().Be("9292CA3F800000CA40000000CA40400000");
Convert.ToHexString(MessagePackSerializer.Serialize(sphere, options))
.Should().Be("9293CA3F800000CA40000000CA40400000CA40A00000");
[Test]
public void GeometryPrimitives_JsonOmitsComputedConvenienceProperties()
{
var json = JsonSerializer.Serialize(new CultVec3(1f, 2f, 3f));
json.Should().Be("""{"X":1,"Y":2,"Z":3}""");
json.Should().NotContain("LengthSquared");
json.Should().NotContain("Xy");
json.Should().NotContain("Xz");
MessagePackSerializer.Deserialize<CultCircle>(MessagePackSerializer.Serialize(circle, options), options)
.Should().Be(circle);
MessagePackSerializer.Deserialize<CultSphere>(MessagePackSerializer.Serialize(sphere, options), options)
.Should().Be(sphere);
}
[Test]

View file

@ -1,4 +1,5 @@
using System;
using CultMath;
using FluentAssertions;
using NUnit.Framework;
@ -77,8 +78,8 @@ namespace GameCult.Geometry.Tests
samples[0, 1, 1] = 1f;
samples[1, 1, 1] = 1f;
var first = CultGeometryIsoSurface.Extract(samples, origin: new CultVec3(10f, 20f, 30f), cellSize: 2f);
var second = CultGeometryIsoSurface.Extract(samples, origin: new CultVec3(10f, 20f, 30f), cellSize: 2f);
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);

View file

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using CultMath;
using FluentAssertions;
using NUnit.Framework;
@ -73,28 +74,28 @@ namespace GameCult.Geometry.Tests
for (var quad = 0; quad < mesh.QuadCount; quad++)
{
var edge = mesh.QuadEdges[quad];
var insideCoord = new CultVec3(1f, 1f, 1f);
var edgeStart = new CultVec3(edge.X, edge.Y, edge.Z);
var insideCoord = new float3(1f, 1f, 1f);
var edgeStart = new float3(edge.X, edge.Y, edge.Z);
var edgeEnd = OtherEndpoint(edge);
// The single inside sample is (1,1,1); the edge's other endpoint is outside.
var outsideCoord = edgeStart.Equals(insideCoord) ? edgeEnd : edgeStart;
var outward = Subtract(outsideCoord, insideCoord);
var outward = outsideCoord - insideCoord;
var baseIndex = quad * 4;
var v0 = Position(mesh.Positions, mesh.Quads[baseIndex]);
var v1 = Position(mesh.Positions, mesh.Quads[baseIndex + 1]);
var v2 = Position(mesh.Positions, mesh.Quads[baseIndex + 2]);
var v3 = Position(mesh.Positions, mesh.Quads[baseIndex + 3]);
var cross = Cross(Subtract(v2, v0), Subtract(v3, v1));
var cross = math.cross(v2 - v0, v3 - v1);
// Restricted to non-degenerate (non-zero-area) quads: the spec's geometric
// winding rule is only meaningful there. Degenerate/collapsed quads are covered
// separately by AssertEdgeDirectionsBalance, since a zero cross product carries
// no orientation information to check here.
if (cross.X == 0f && cross.Y == 0f && cross.Z == 0f) continue;
if (cross.Equals(float3.zero)) continue;
Dot(cross, outward).Should().BePositive($"quad for edge {edge} should wind outward");
math.dot(cross, outward).Should().BePositive($"quad for edge {edge} should wind outward");
}
}
@ -209,7 +210,7 @@ namespace GameCult.Geometry.Tests
samples[1, 1, 1] = -1f;
var mesh = CultGeometrySurfaceNets.Extract(samples, origin: new CultVec3(10f, 20f, 30f), cellSize: 2f);
var mesh = CultGeometrySurfaceNets.Extract(samples, origin: new float3(10f, 20f, 30f), cellSize: 2f);
mesh.VertexCount.Should().Be(1);
mesh.Positions[0].Should().BeApproximately(10f + (2.9f / 3f * 2f), 1e-5f);
@ -387,10 +388,10 @@ namespace GameCult.Geometry.Tests
// documented c0..c3 order is not swapped.
var expected = new[]
{
new CultVec3(1.5f, 0.5f, 0.5f), // c0 = (1, 0, 0)
new CultVec3(1.5f, 1.5f, 0.5f), // c1 = (1, 1, 0)
new CultVec3(1.5f, 1.5f, 1.5f), // c2 = (1, 1, 1)
new CultVec3(1.5f, 0.5f, 1.5f), // c3 = (1, 0, 1)
new float3(1.5f, 0.5f, 0.5f), // c0 = (1, 0, 0)
new float3(1.5f, 1.5f, 0.5f), // c1 = (1, 1, 0)
new float3(1.5f, 1.5f, 1.5f), // c2 = (1, 1, 1)
new float3(1.5f, 0.5f, 1.5f), // c3 = (1, 0, 1)
};
AssertQuadCorners(mesh, edge, expected);
@ -413,10 +414,10 @@ namespace GameCult.Geometry.Tests
// v3 trade places relative to the raw c0..c3 listing.
var expected = new[]
{
new CultVec3(0.5f, 1.5f, 0.5f), // c0 = (0, 1, 0)
new CultVec3(0.5f, 1.5f, 1.5f), // c3 = (0, 1, 1), swapped into v1
new CultVec3(1.5f, 1.5f, 1.5f), // c2 = (1, 1, 1)
new CultVec3(1.5f, 1.5f, 0.5f), // c1 = (1, 1, 0), swapped into v3
new float3(0.5f, 1.5f, 0.5f), // c0 = (0, 1, 0)
new float3(0.5f, 1.5f, 1.5f), // c3 = (0, 1, 1), swapped into v1
new float3(1.5f, 1.5f, 1.5f), // c2 = (1, 1, 1)
new float3(1.5f, 1.5f, 0.5f), // c1 = (1, 1, 0), swapped into v3
};
AssertQuadCorners(mesh, edge, expected);
@ -438,10 +439,10 @@ namespace GameCult.Geometry.Tests
// mirrored, so the documented c0..c3 order is not swapped.
var expected = new[]
{
new CultVec3(0.5f, 0.5f, 1.5f), // c0 = (0, 0, 1)
new CultVec3(1.5f, 0.5f, 1.5f), // c1 = (1, 0, 1)
new CultVec3(1.5f, 1.5f, 1.5f), // c2 = (1, 1, 1)
new CultVec3(0.5f, 1.5f, 1.5f), // c3 = (0, 1, 1)
new float3(0.5f, 0.5f, 1.5f), // c0 = (0, 0, 1)
new float3(1.5f, 0.5f, 1.5f), // c1 = (1, 0, 1)
new float3(1.5f, 1.5f, 1.5f), // c2 = (1, 1, 1)
new float3(0.5f, 1.5f, 1.5f), // c3 = (0, 1, 1)
};
AssertQuadCorners(mesh, edge, expected);
@ -476,7 +477,7 @@ namespace GameCult.Geometry.Tests
// balance regardless.
var samples = Sphere(64);
var mesh = CultGeometrySurfaceNets.Extract(samples, origin: new CultVec3(1e4f, 1e4f, 1e4f), cellSize: 0.001f);
var mesh = CultGeometrySurfaceNets.Extract(samples, origin: new float3(1e4f, 1e4f, 1e4f), cellSize: 0.001f);
mesh.QuadCount.Should().BeGreaterThan(0);
AssertEdgeDirectionsBalance(mesh);
@ -651,7 +652,7 @@ namespace GameCult.Geometry.Tests
{
var samples = new float[2, 2, 2];
Action act = () => CultGeometrySurfaceNets.Extract(samples, origin: new CultVec3(x, y, z));
Action act = () => CultGeometrySurfaceNets.Extract(samples, origin: new float3(x, y, z));
act.Should().Throw<ArgumentException>();
}
@ -725,28 +726,17 @@ namespace GameCult.Geometry.Tests
return usage;
}
private static CultVec3 OtherEndpoint(CultGeometryGridEdge edge) => edge.Axis switch
private static float3 OtherEndpoint(CultGeometryGridEdge edge) => edge.Axis switch
{
0 => new CultVec3(edge.X + 1, edge.Y, edge.Z),
1 => new CultVec3(edge.X, edge.Y + 1, edge.Z),
_ => new CultVec3(edge.X, edge.Y, edge.Z + 1),
0 => new float3(edge.X + 1, edge.Y, edge.Z),
1 => new float3(edge.X, edge.Y + 1, edge.Z),
_ => new float3(edge.X, edge.Y, edge.Z + 1),
};
private static CultVec3 Subtract(CultVec3 left, CultVec3 right) =>
new(left.X - right.X, left.Y - right.Y, left.Z - right.Z);
private static CultVec3 Cross(CultVec3 left, CultVec3 right) => new(
(left.Y * right.Z) - (left.Z * right.Y),
(left.Z * right.X) - (left.X * right.Z),
(left.X * right.Y) - (left.Y * right.X));
private static float Dot(CultVec3 left, CultVec3 right) =>
(left.X * right.X) + (left.Y * right.Y) + (left.Z * right.Z);
private static CultVec3 Position(float[] positions, uint index)
private static float3 Position(float[] positions, uint index)
{
var offset = (int)index * 3;
return new CultVec3(positions[offset], positions[offset + 1], positions[offset + 2]);
return new float3(positions[offset], positions[offset + 1], positions[offset + 2]);
}
// Hand-derived (double precision, independent of Interpolate) expected position for a
@ -760,7 +750,7 @@ namespace GameCult.Geometry.Tests
return (float)(amount / 3d);
}
private static void AssertQuadCorners(CultGeometryQuadMesh mesh, CultGeometryGridEdge edge, CultVec3[] expected)
private static void AssertQuadCorners(CultGeometryQuadMesh mesh, CultGeometryGridEdge edge, float3[] expected)
{
var quadIndex = Array.IndexOf(mesh.QuadEdges, edge);
quadIndex.Should().BeGreaterThanOrEqualTo(0, $"a quad for edge {edge} should exist");