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:
parent
14621b3893
commit
20ac3b0b6c
8 changed files with 95 additions and 298 deletions
|
|
@ -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`),
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
Loading…
Reference in a new issue