GameCult.Caching: SpanNoise MessagePack and JSON shapes

MessagePack [field, curvature, fold] beside CultPhasor in CultMathResolver, JSON {field, curvature,
fold} in CultMathJson; round-trip, hex and JSON golden cases beside CultPhasor's.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-14 20:48:44 +02:00
commit 427d81feda
4 changed files with 35 additions and 3 deletions

View file

@ -14,7 +14,7 @@ namespace GameCult.Caching.MessagePack;
/// The canonical System.Text.Json shape of every public CultMath value type, the JSON twin of
/// <see cref="CultMathResolver"/>. Vectors, quaternions and colors are objects keyed by their
/// field names (<c>{"x":1,"y":2}</c>), matrices are arrays of row objects, <c>rect</c> is
/// <c>{"min":..,"max":..}</c>, <c>CultCellular</c> is <c>{"nearest":..,"edge":..,"id":..}</c>, and <c>CultPhasor</c> is <c>{"cos":..,"sin":..}</c>.
/// <c>{"min":..,"max":..}</c>, <c>CultCellular</c> is <c>{"nearest":..,"edge":..,"id":..}</c>, <c>CultPhasor</c> is <c>{"cos":..,"sin":..}</c>, and <c>SpanNoise</c> is <c>{"field":..,"curvature":..,"fold":..}</c>.
/// Property names are fixed: <c>PropertyNamingPolicy</c> does not rename them, and
/// <c>PropertyNameCaseInsensitive</c> makes reading case-insensitive.
/// <para>
@ -481,7 +481,7 @@ public static class CultMathJson
Add<Random, uint>(UIntScalar.Instance, new Fields<uint>(("state", Val<uint>())),
a => new Random { state = a[0] }, (v, a) => a[0] = v.state),
// Compositions: matrices are arrays of row objects, rect is {min, max}, CultCellular is {nearest, edge, id}, CultPhasor is {cos, sin}.
// Compositions: matrices are arrays of row objects, rect is {min, max}, CultCellular is {nearest, edge, id}, CultPhasor is {cos, sin}, SpanNoise is {field, curvature, fold}.
Add<float2x2, float>(f, new Rows<float>(2, Xy<float>()),
a => new float2x2(a[0], a[1], a[2], a[3]),
(v, a) => { a[0] = v[0].x; a[1] = v[0].y; a[2] = v[1].x; a[3] = v[1].y; }),
@ -514,6 +514,13 @@ public static class CultMathJson
a[0] = v.cos.x; a[1] = v.cos.y; a[2] = v.cos.z; a[3] = v.cos.w;
a[4] = v.sin.x; a[5] = v.sin.y; a[6] = v.sin.z; a[7] = v.sin.w;
}),
Add<SpanNoise, float>(f, new Fields<float>(("field", Xyzw<float>()), ("curvature", Val<float>()), ("fold", Val<float>())),
a => new SpanNoise(new float4(a[0], a[1], a[2], a[3]), a[4], a[5]),
(v, a) =>
{
a[0] = v.field.x; a[1] = v.field.y; a[2] = v.field.z; a[3] = v.field.w;
a[4] = v.curvature; a[5] = v.fold;
}),
};
var map = new Dictionary<Type, JsonConverter>();

View file

@ -180,7 +180,7 @@ public sealed class CultMathResolver : IFormatterResolver
return value;
}),
// Compositions: matrices are rows, rect is [min, max], CultCellular is [nearest, edge, id], CultPhasor is [cos, sin].
// Compositions: matrices are rows, rect is [min, max], CultCellular is [nearest, edge, id], CultPhasor is [cos, sin], SpanNoise is [field, curvature, fold].
Shape<float2x2>((ref MessagePackWriter w, float2x2 v) => { w.WriteArrayHeader(2); PutFloat2(ref w, v[0]); PutFloat2(ref w, v[1]); },
(ref MessagePackReader r) =>
{
@ -236,6 +236,22 @@ public sealed class CultMathResolver : IFormatterResolver
}
return value;
}),
Shape<SpanNoise>((ref MessagePackWriter w, SpanNoise v) => { w.WriteArrayHeader(3); PutFloat4(ref w, v.field); w.Write(v.curvature); w.Write(v.fold); },
(ref MessagePackReader r) =>
{
var value = default(SpanNoise);
for (int i = 0, n = r.ReadArrayHeader(); i < n; i++)
{
switch (i)
{
case 0: value.field = GetFloat4(ref r); break;
case 1: value.curvature = r.ReadSingle(); break;
case 2: value.fold = r.ReadSingle(); break;
default: r.Skip(); break;
}
}
return value;
}),
};
var map = new Dictionary<Type, object>();

View file

@ -45,6 +45,8 @@ namespace GameCult.Caching.Tests
"""{"nearest":{"x":1,"y":2,"z":3,"w":4},"edge":{"x":5,"y":6,"z":7,"w":8},"id":0.25}""");
Json(new CultPhasor(new float4(1f, 2f, 3f, 4f), new float4(5f, 6f, 7f, 8f))).Should().Be(
"""{"cos":{"x":1,"y":2,"z":3,"w":4},"sin":{"x":5,"y":6,"z":7,"w":8}}""");
Json(new SpanNoise(new float4(1f, 2f, 3f, 4f), 0.25f, 0.5f)).Should().Be(
"""{"field":{"x":1,"y":2,"z":3,"w":4},"curvature":0.25,"fold":0.5}""");
}
[Test]
@ -69,6 +71,7 @@ namespace GameCult.Caching.Tests
RoundTrip(new rect(-1f, -2f, 5f, 7f));
RoundTrip(new CultCellular(new float4(1, 2, 3, 4), new float4(5, 6, 7, 8), 0.25f));
RoundTrip(new CultPhasor(new float4(1, 2, 3, 4), new float4(5, 6, 7, 8)));
RoundTrip(new SpanNoise(new float4(1, 2, 3, 4), 0.25f, 0.5f));
}
[Test]

View file

@ -102,6 +102,7 @@ namespace GameCult.Caching.Tests
RoundTrip(new rect(-1f, -2f, 5f, 7f));
RoundTrip(new CultCellular(new float4(1, 2, 3, 4), new float4(5, 6, 7, 8), 0.25f));
RoundTrip(new CultPhasor(new float4(1, 2, 3, 4), new float4(5, 6, 7, 8)));
RoundTrip(new SpanNoise(new float4(1, 2, 3, 4), 0.25f, 0.5f));
}
[Test]
@ -137,6 +138,11 @@ namespace GameCult.Caching.Tests
"92" +
"94CA3F800000CA40000000CA40400000CA40800000" +
"94CA40A00000CA40C00000CA40E00000CA41000000");
Hex(new SpanNoise(new float4(1f, 2f, 3f, 4f), 0.25f, 0.5f)).Should().Be(
"93" +
"94CA3F800000CA40000000CA40400000CA40800000" +
"CA3E800000" +
"CA3F000000");
}
[Test]