Merge hands/cultmath-phasor-rename
This commit is contained in:
commit
a085c0b3f1
13 changed files with 161 additions and 52 deletions
|
|
@ -429,7 +429,7 @@ together, laid out as `float4(gradient.xyz, value.w)`, with no value-only twin
|
|||
- `phacelle(float3 p, float3 side, float offset, float normalization)` is Rune
|
||||
Skovbo Johansen's Phacelle noise ("phase + cell": a stripe pattern built by
|
||||
blending one cosine/sine wave per jittered cell), generalised to 3D cells. It
|
||||
returns `CultPhasor { cos, sin }`, each `(∇, value)`. `side` is the stripe
|
||||
returns `CultPhasor { cosine, sine }`, each `(∇, value)`. `side` is the stripe
|
||||
wave vector, whose length is 2π times the stripe frequency; in 3D the
|
||||
perpendicular to the flow is not unique, so the caller chooses it (on a unit
|
||||
sphere, `cross(p̂, flow)`). `offset` is the phase in cycles. It visits 4×4×4
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@
|
|||
// against CultMath.CultPhasor, same field order.
|
||||
struct CultPhasor
|
||||
{
|
||||
vec4 cos;
|
||||
vec4 sin;
|
||||
vec4 cosine;
|
||||
vec4 sine;
|
||||
};
|
||||
|
||||
// Johansen's Phacelle noise on 3D cells. See math.phacelle's comment (math.Phacelle.cs) for the stripe wave
|
||||
|
|
@ -84,13 +84,13 @@ CultPhasor cultmath_phacelle(vec3 p, vec3 side, float offset, float normalizatio
|
|||
float outCos = rawCos / rawLength;
|
||||
float outSin = rawSin / rawLength;
|
||||
vec3 gradLength = (rawCos * gradRawCos + rawSin * gradRawSin) / rawLength;
|
||||
result.cos = vec4((gradRawCos - outCos * gradLength) / rawLength, outCos);
|
||||
result.sin = vec4((gradRawSin - outSin * gradLength) / rawLength, outSin);
|
||||
result.cosine = vec4((gradRawCos - outCos * gradLength) / rawLength, outCos);
|
||||
result.sine = vec4((gradRawSin - outSin * gradLength) / rawLength, outSin);
|
||||
}
|
||||
else
|
||||
{
|
||||
result.cos = vec4(gradRawCos / floorLength, rawCos / floorLength);
|
||||
result.sin = vec4(gradRawSin / floorLength, rawSin / floorLength);
|
||||
result.cosine = vec4(gradRawCos / floorLength, rawCos / floorLength);
|
||||
result.sine = vec4(gradRawSin / floorLength, rawSin / floorLength);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@
|
|||
// against CultMath.CultPhasor, same field order.
|
||||
struct CultPhasor
|
||||
{
|
||||
float4 cos;
|
||||
float4 sin;
|
||||
float4 cosine;
|
||||
float4 sine;
|
||||
};
|
||||
|
||||
// Johansen's Phacelle noise on 3D cells. See math.phacelle's comment (math.Phacelle.cs) for the stripe wave
|
||||
|
|
@ -84,13 +84,13 @@ CultPhasor cultmath_phacelle(float3 p, float3 side, float offset, float normaliz
|
|||
float outCos = rawCos / rawLength;
|
||||
float outSin = rawSin / rawLength;
|
||||
float3 gradLength = (rawCos * gradRawCos + rawSin * gradRawSin) / rawLength;
|
||||
result.cos = float4((gradRawCos - outCos * gradLength) / rawLength, outCos);
|
||||
result.sin = float4((gradRawSin - outSin * gradLength) / rawLength, outSin);
|
||||
result.cosine = float4((gradRawCos - outCos * gradLength) / rawLength, outCos);
|
||||
result.sine = float4((gradRawSin - outSin * gradLength) / rawLength, outSin);
|
||||
}
|
||||
else
|
||||
{
|
||||
result.cos = float4(gradRawCos / floorLength, rawCos / floorLength);
|
||||
result.sin = float4(gradRawSin / floorLength, rawSin / floorLength);
|
||||
result.cosine = float4(gradRawCos / floorLength, rawCos / floorLength);
|
||||
result.sine = float4(gradRawSin / floorLength, rawSin / floorLength);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
|
|
|||
|
|
@ -14,14 +14,14 @@ namespace CultMath;
|
|||
public struct CultPhasor
|
||||
{
|
||||
/// <summary>(∇cos, cos): the normalized cosine wave and its gradient.</summary>
|
||||
public float4 cos;
|
||||
public float4 cosine;
|
||||
|
||||
/// <summary>(∇sin, sin): the normalized sine wave and its gradient.</summary>
|
||||
public float4 sin;
|
||||
public float4 sine;
|
||||
|
||||
public CultPhasor(float4 cos, float4 sin)
|
||||
public CultPhasor(float4 cosine, float4 sine)
|
||||
{
|
||||
this.cos = cos;
|
||||
this.sin = sin;
|
||||
this.cosine = cosine;
|
||||
this.sine = sine;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -93,12 +93,12 @@ public sealed class PhacelleTests
|
|||
var actual = math.phacelle(p, side, offset, normalization);
|
||||
var expected = Evaluate(p, side, offset, normalization);
|
||||
|
||||
maxError = Math.Max(maxError, Math.Max(Math.Abs(actual.cos.w - expected.Cos), Math.Abs(actual.sin.w - expected.Sin)));
|
||||
maxError = Math.Max(maxError, Math.Max(Math.Abs(actual.cosine.w - expected.Cos), Math.Abs(actual.sine.w - expected.Sin)));
|
||||
if (expected.RawLength >= 1.0 - normalization + ThresholdBand)
|
||||
{
|
||||
// The output magnitude is 1 wherever the raw magnitude reaches 1 - normalization.
|
||||
above++;
|
||||
var magnitude = Math.Sqrt((double)actual.cos.w * actual.cos.w + (double)actual.sin.w * actual.sin.w);
|
||||
var magnitude = Math.Sqrt((double)actual.cosine.w * actual.cosine.w + (double)actual.sine.w * actual.sine.w);
|
||||
Assert.True(Math.Abs(magnitude - 1.0) < ValueTolerance, $"magnitude {magnitude} above the threshold at {p}");
|
||||
}
|
||||
else if (expected.RawLength <= 1.0 - normalization - ThresholdBand)
|
||||
|
|
@ -130,10 +130,10 @@ public sealed class PhacelleTests
|
|||
var quarter = math.phacelle(p, side, offset + 0.25f, 0.5f);
|
||||
var whole = math.phacelle(p, side, offset + 1.0f, 0.5f);
|
||||
|
||||
Assert.True(MathF.Abs(quarter.cos.w + basePhasor.sin.w) < 2.0e-5f);
|
||||
Assert.True(MathF.Abs(quarter.sin.w - basePhasor.cos.w) < 2.0e-5f);
|
||||
Assert.True(MathF.Abs(whole.cos.w - basePhasor.cos.w) < 2.0e-5f);
|
||||
Assert.True(MathF.Abs(whole.sin.w - basePhasor.sin.w) < 2.0e-5f);
|
||||
Assert.True(MathF.Abs(quarter.cosine.w + basePhasor.sine.w) < 2.0e-5f);
|
||||
Assert.True(MathF.Abs(quarter.sine.w - basePhasor.cosine.w) < 2.0e-5f);
|
||||
Assert.True(MathF.Abs(whole.cosine.w - basePhasor.cosine.w) < 2.0e-5f);
|
||||
Assert.True(MathF.Abs(whole.sine.w - basePhasor.sine.w) < 2.0e-5f);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -166,9 +166,9 @@ public sealed class PhacelleTests
|
|||
if (distance > 0.0) above++; else below++;
|
||||
|
||||
var analytic = math.phacelle(p, side, offset, normalization);
|
||||
var numericCos = CentralDifference(q => math.phacelle(q, side, offset, normalization).cos.w, p, GradientEps);
|
||||
var numericSin = CentralDifference(q => math.phacelle(q, side, offset, normalization).sin.w, p, GradientEps);
|
||||
maxError = MathF.Max(maxError, MathF.Max(MaxComponentError(numericCos, analytic.cos), MaxComponentError(numericSin, analytic.sin)));
|
||||
var numericCos = CentralDifference(q => math.phacelle(q, side, offset, normalization).cosine.w, p, GradientEps);
|
||||
var numericSin = CentralDifference(q => math.phacelle(q, side, offset, normalization).sine.w, p, GradientEps);
|
||||
maxError = MathF.Max(maxError, MathF.Max(MaxComponentError(numericCos, analytic.cosine), MaxComponentError(numericSin, analytic.sine)));
|
||||
}
|
||||
|
||||
Assert.True(above >= 150 && below >= 150, $"{above} points above and {below} below the threshold");
|
||||
|
|
@ -199,7 +199,7 @@ public sealed class PhacelleTests
|
|||
var flow = math.normalize(tangent);
|
||||
var sideDirection = math.cross(hat, flow);
|
||||
var phasor = math.phacelle(hat * radiusInCells, sideDirection * math.TAU, 0.25f, 0.5f);
|
||||
var gradient = new float3(phasor.cos.x, phasor.cos.y, phasor.cos.z);
|
||||
var gradient = new float3(phasor.cosine.x, phasor.cosine.y, phasor.cosine.z);
|
||||
alongFlow += Math.Abs(math.dot(gradient, flow));
|
||||
acrossFlow += Math.Abs(math.dot(gradient, sideDirection));
|
||||
used++;
|
||||
|
|
@ -232,7 +232,7 @@ public sealed class PhacelleTests
|
|||
{
|
||||
var low = math.phacelle(SetAxis(p, axis, GetAxis(p, axis) - steps[k] * 0.5f), side, offset, 0.5f);
|
||||
var high = math.phacelle(SetAxis(p, axis, GetAxis(p, axis) + steps[k] * 0.5f), side, offset, 0.5f);
|
||||
maxDifference[k] = MathF.Max(maxDifference[k], MathF.Max(MathF.Abs(low.cos.w - high.cos.w), MathF.Abs(low.sin.w - high.sin.w)));
|
||||
maxDifference[k] = MathF.Max(maxDifference[k], MathF.Max(MathF.Abs(low.cosine.w - high.cosine.w), MathF.Abs(low.sine.w - high.sine.w)));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -260,8 +260,8 @@ public sealed class PhacelleTests
|
|||
var pruned = math.phacelle(p, side, offset, normalization);
|
||||
var unpruned = math.phacelle(p, side, offset, normalization, false);
|
||||
|
||||
AssertBitEqual(unpruned.cos, pruned.cos);
|
||||
AssertBitEqual(unpruned.sin, pruned.sin);
|
||||
AssertBitEqual(unpruned.cosine, pruned.cosine);
|
||||
AssertBitEqual(unpruned.sine, pruned.sine);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -325,8 +325,8 @@ public sealed class PhacelleTests
|
|||
var pruned = math.phacelle(p, PruneCutSide, PruneCutOffset, PruneCutNormalization);
|
||||
var unpruned = math.phacelle(p, PruneCutSide, PruneCutOffset, PruneCutNormalization, false);
|
||||
|
||||
AssertBitEqual(unpruned.cos, pruned.cos);
|
||||
AssertBitEqual(unpruned.sin, pruned.sin);
|
||||
AssertBitEqual(unpruned.cosine, pruned.cosine);
|
||||
AssertBitEqual(unpruned.sine, pruned.sine);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,15 @@ All notable changes to this package are documented in this file.
|
|||
Earlier releases (0.2.0-0.2.3) predate this package's changelog and are not
|
||||
backfilled here; see `docs/semver-policy.md` for why.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Breaking
|
||||
|
||||
- `CultPhasor.cos` and `.sin` are now `.cosine` and `.sine` in C#, HLSL and GLSL,
|
||||
constructor parameters included, because SPIRV-Cross renames members that match
|
||||
GLSL builtins. This is a minor bump on 0.y.z, so the next cultmath-unity release
|
||||
is 0.5.0.
|
||||
|
||||
## [0.4.1]
|
||||
|
||||
### Fixed
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@
|
|||
// against CultMath.CultPhasor, same field order.
|
||||
struct CultPhasor
|
||||
{
|
||||
float4 cos;
|
||||
float4 sin;
|
||||
float4 cosine;
|
||||
float4 sine;
|
||||
};
|
||||
|
||||
// Johansen's Phacelle noise on 3D cells. See math.phacelle's comment (math.Phacelle.cs) for the stripe wave
|
||||
|
|
@ -84,13 +84,13 @@ CultPhasor cultmath_phacelle(float3 p, float3 side, float offset, float normaliz
|
|||
float outCos = rawCos / rawLength;
|
||||
float outSin = rawSin / rawLength;
|
||||
float3 gradLength = (rawCos * gradRawCos + rawSin * gradRawSin) / rawLength;
|
||||
result.cos = float4((gradRawCos - outCos * gradLength) / rawLength, outCos);
|
||||
result.sin = float4((gradRawSin - outSin * gradLength) / rawLength, outSin);
|
||||
result.cosine = float4((gradRawCos - outCos * gradLength) / rawLength, outCos);
|
||||
result.sine = float4((gradRawSin - outSin * gradLength) / rawLength, outSin);
|
||||
}
|
||||
else
|
||||
{
|
||||
result.cos = float4(gradRawCos / floorLength, rawCos / floorLength);
|
||||
result.sin = float4(gradRawSin / floorLength, rawSin / floorLength);
|
||||
result.cosine = float4(gradRawCos / floorLength, rawCos / floorLength);
|
||||
result.sine = float4(gradRawSin / floorLength, rawSin / floorLength);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
|
|
|||
|
|
@ -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>, <c>CultPhasor</c> is <c>{"cos":..,"sin":..}</c>, <c>SpanNoise</c> is <c>{"field":..,"curvature":..,"fold":..}</c>, <c>SpanPath</c> is <c>{"midpoint":..,"velocity":..,"speed":..,"acceleration":..,"halfLength":..}</c>, <c>SpanStage</c> is <c>{"frequency":..,"stretch":..,"axis":..,"shift":..,"offset":..,"octaveStep":..}</c>, and <c>CultSiteCells</c> is <c>{"nearest":..,"second":..,"nearestSite":..,"secondSite":..}</c>.
|
||||
/// <c>{"min":..,"max":..}</c>, <c>CultCellular</c> is <c>{"nearest":..,"edge":..,"id":..}</c>, <c>CultPhasor</c> is <c>{"cosine":..,"sine":..}</c> (reading also accepts the legacy <c>cos</c>/<c>sin</c> names), <c>SpanNoise</c> is <c>{"field":..,"curvature":..,"fold":..}</c>, <c>SpanPath</c> is <c>{"midpoint":..,"velocity":..,"speed":..,"acceleration":..,"halfLength":..}</c>, <c>SpanStage</c> is <c>{"frequency":..,"stretch":..,"axis":..,"shift":..,"offset":..,"octaveStep":..}</c>, and <c>CultSiteCells</c> is <c>{"nearest":..,"second":..,"nearestSite":..,"secondSite":..}</c>.
|
||||
/// Property names are fixed: <c>PropertyNamingPolicy</c> does not rename them, and
|
||||
/// <c>PropertyNameCaseInsensitive</c> makes reading case-insensitive.
|
||||
/// <para>
|
||||
|
|
@ -270,14 +270,24 @@ public static class CultMathJson
|
|||
{
|
||||
private readonly string[] names;
|
||||
private readonly byte[][] utf8Names;
|
||||
private readonly string?[] legacyNames;
|
||||
private readonly byte[]?[] legacyUtf8Names;
|
||||
private readonly JsonEncodedText[] encoded;
|
||||
private readonly Node<S>[] children;
|
||||
private readonly int[] offsets;
|
||||
|
||||
public Fields(params (string Name, Node<S> Child)[] fields)
|
||||
: this(Array.ConvertAll(fields, f => (f.Name, f.Child, (string?)null)))
|
||||
{
|
||||
}
|
||||
|
||||
// A legacy name is matched on read only and maps to the same field as its current name.
|
||||
public Fields(params (string Name, Node<S> Child, string? Legacy)[] fields)
|
||||
{
|
||||
names = new string[fields.Length];
|
||||
utf8Names = new byte[fields.Length][];
|
||||
legacyNames = new string?[fields.Length];
|
||||
legacyUtf8Names = new byte[]?[fields.Length];
|
||||
encoded = new JsonEncodedText[fields.Length];
|
||||
children = new Node<S>[fields.Length];
|
||||
offsets = new int[fields.Length];
|
||||
|
|
@ -285,6 +295,8 @@ public static class CultMathJson
|
|||
{
|
||||
names[i] = fields[i].Name;
|
||||
utf8Names[i] = Encoding.UTF8.GetBytes(fields[i].Name);
|
||||
legacyNames[i] = fields[i].Legacy;
|
||||
legacyUtf8Names[i] = fields[i].Legacy is { } legacy ? Encoding.UTF8.GetBytes(legacy) : null;
|
||||
encoded[i] = JsonEncodedText.Encode(fields[i].Name);
|
||||
children[i] = fields[i].Child;
|
||||
offsets[i] = Count;
|
||||
|
|
@ -339,13 +351,14 @@ public static class CultMathJson
|
|||
if (!ignoreCase)
|
||||
{
|
||||
for (var i = 0; i < utf8Names.Length; i++)
|
||||
if (r.ValueTextEquals(utf8Names[i])) return i;
|
||||
if (r.ValueTextEquals(utf8Names[i]) || (legacyUtf8Names[i] is { } legacy && r.ValueTextEquals(legacy))) return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
var name = r.GetString()!;
|
||||
for (var i = 0; i < names.Length; i++)
|
||||
if (string.Equals(names[i], name, StringComparison.OrdinalIgnoreCase)) return i;
|
||||
if (string.Equals(names[i], name, StringComparison.OrdinalIgnoreCase)
|
||||
|| (legacyNames[i] is { } legacy && string.Equals(legacy, name, StringComparison.OrdinalIgnoreCase))) return i;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
|
@ -481,7 +494,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}, SpanNoise is {field, curvature, fold}, SpanPath is {midpoint, velocity, speed, acceleration, halfLength}, SpanStage is {frequency, stretch, axis, shift, offset, octaveStep}, CultSiteCells is {nearest, second, nearestSite, secondSite}.
|
||||
// Compositions: matrices are arrays of row objects, rect is {min, max}, CultCellular is {nearest, edge, id}, CultPhasor is {cosine, sine}, SpanNoise is {field, curvature, fold}, SpanPath is {midpoint, velocity, speed, acceleration, halfLength}, SpanStage is {frequency, stretch, axis, shift, offset, octaveStep}, CultSiteCells is {nearest, second, nearestSite, secondSite}.
|
||||
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; }),
|
||||
|
|
@ -507,12 +520,12 @@ public static class CultMathJson
|
|||
a[4] = v.edge.x; a[5] = v.edge.y; a[6] = v.edge.z; a[7] = v.edge.w;
|
||||
a[8] = v.id;
|
||||
}),
|
||||
Add<CultPhasor, float>(f, new Fields<float>(("cos", Xyzw<float>()), ("sin", Xyzw<float>())),
|
||||
Add<CultPhasor, float>(f, new Fields<float>(("cosine", Xyzw<float>(), "cos"), ("sine", Xyzw<float>(), "sin")),
|
||||
a => new CultPhasor(new float4(a[0], a[1], a[2], a[3]), new float4(a[4], a[5], a[6], a[7])),
|
||||
(v, a) =>
|
||||
{
|
||||
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;
|
||||
a[0] = v.cosine.x; a[1] = v.cosine.y; a[2] = v.cosine.z; a[3] = v.cosine.w;
|
||||
a[4] = v.sine.x; a[5] = v.sine.y; a[6] = v.sine.z; a[7] = v.sine.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]),
|
||||
|
|
|
|||
|
|
@ -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], SpanNoise is [field, curvature, fold], SpanPath is [midpoint, velocity, speed, acceleration, halfLength], SpanStage is [frequency, stretch, axis, shift, offset, octaveStep], CultSiteCells is [nearest, second, nearestSite, secondSite].
|
||||
// Compositions: matrices are rows, rect is [min, max], CultCellular is [nearest, edge, id], CultPhasor is [cosine, sine], SpanNoise is [field, curvature, fold], SpanPath is [midpoint, velocity, speed, acceleration, halfLength], SpanStage is [frequency, stretch, axis, shift, offset, octaveStep], CultSiteCells is [nearest, second, nearestSite, secondSite].
|
||||
Shape<float2x2>((ref MessagePackWriter w, float2x2 v) => { w.WriteArrayHeader(2); PutFloat2(ref w, v[0]); PutFloat2(ref w, v[1]); },
|
||||
(ref MessagePackReader r) =>
|
||||
{
|
||||
|
|
@ -221,7 +221,7 @@ public sealed class CultMathResolver : IFormatterResolver
|
|||
}
|
||||
return value;
|
||||
}),
|
||||
Shape<CultPhasor>((ref MessagePackWriter w, CultPhasor v) => { w.WriteArrayHeader(2); PutFloat4(ref w, v.cos); PutFloat4(ref w, v.sin); },
|
||||
Shape<CultPhasor>((ref MessagePackWriter w, CultPhasor v) => { w.WriteArrayHeader(2); PutFloat4(ref w, v.cosine); PutFloat4(ref w, v.sine); },
|
||||
(ref MessagePackReader r) =>
|
||||
{
|
||||
var value = default(CultPhasor);
|
||||
|
|
@ -229,8 +229,8 @@ public sealed class CultMathResolver : IFormatterResolver
|
|||
{
|
||||
switch (i)
|
||||
{
|
||||
case 0: value.cos = GetFloat4(ref r); break;
|
||||
case 1: value.sin = GetFloat4(ref r); break;
|
||||
case 0: value.cosine = GetFloat4(ref r); break;
|
||||
case 1: value.sine = GetFloat4(ref r); break;
|
||||
default: r.Skip(); break;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ broken graph whole (an `ExprGraphException` naming the rule), so a consumer
|
|||
| `float2x2`, `float3x3` | array of row vectors (row-major) | array of row objects |
|
||||
| `rect` | `[min, max]` of `float2` | `{"min":{..},"max":{..}}` |
|
||||
| `CultCellular` | `[nearest, edge, id]` | `{"nearest":{..},"edge":{..},"id":..}` |
|
||||
| `CultPhasor` | `[cos, sin]` of `float4` | `{"cos":{..},"sin":{..}}` |
|
||||
| `CultPhasor` | `[cosine, sine]` of `float4` | `{"cosine":{..},"sine":{..}}` (reads legacy `cos`/`sin`) |
|
||||
| `CultSiteCells` | `[nearest, second, nearestSite, secondSite]` | `{"nearest":{..},"second":{..},"nearestSite":..,"secondSite":..}` |
|
||||
|
||||
Floats are MessagePack float32 (`0xCA`), doubles float64 (`0xCB`), integers the
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ namespace GameCult.Caching.Tests
|
|||
Json(new CultCellular(new float4(1f, 2f, 3f, 4f), new float4(5f, 6f, 7f, 8f), 0.25f)).Should().Be(
|
||||
"""{"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}}""");
|
||||
"""{"cosine":{"x":1,"y":2,"z":3,"w":4},"sine":{"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}""");
|
||||
Json(new SpanPath { midpoint = new float3(1f, 2f, 3f), velocity = new float3(4f, 5f, 6f), speed = 7f, acceleration = 8f, halfLength = 9f }).Should().Be(
|
||||
|
|
@ -92,6 +92,87 @@ namespace GameCult.Caching.Tests
|
|||
Parse<float2?>("null").Should().BeNull();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Reading_AcceptsTheLegacyPhasorNames()
|
||||
{
|
||||
// The fixture is what the 0.4.1-era encoder wrote: keys cos/sin.
|
||||
var path = System.IO.Path.Combine(TestContext.CurrentContext.TestDirectory, "Fixtures", "cultphasor-json-legacy.json");
|
||||
var legacy = Parse<CultPhasor[]>(System.IO.File.ReadAllText(path));
|
||||
var current = new[]
|
||||
{
|
||||
new CultPhasor(new float4(1f, 2f, 3f, 4f), new float4(5f, 6f, 7f, 8f)),
|
||||
math.phacelle(new float3(0.3f, 1.7f, -2.2f), new float3(6.2831853f, 0f, 0f), 0.25f, 0.5f),
|
||||
};
|
||||
legacy.Should().HaveCount(2);
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
BitsOf(legacy[i]).Should().Equal(BitsOf(current[i]));
|
||||
BitsOf(Parse<CultPhasor>(Json(current[i]))).Should().Equal(BitsOf(current[i]));
|
||||
}
|
||||
|
||||
var caseInsensitive = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }.AddCultMathConverters();
|
||||
BitsOf(JsonSerializer.Deserialize<CultPhasor>("""{"COS":{"x":1,"y":2,"z":3,"w":4},"Sin":{"x":5,"y":6,"z":7,"w":8}}""", caseInsensitive))
|
||||
.Should().Equal(BitsOf(current[0]));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Reading_RefusesALegacyAndCurrentNameTogether()
|
||||
{
|
||||
var property = typeof(JsonSerializerOptions).GetProperty("AllowDuplicateProperties");
|
||||
Assert.That(property, Is.Not.Null, "the test runtime's System.Text.Json has AllowDuplicateProperties");
|
||||
var strict = new JsonSerializerOptions().AddCultMathConverters();
|
||||
property!.SetValue(strict, false);
|
||||
Action both = () => JsonSerializer.Deserialize<CultPhasor>("""{"cos":{"x":1},"cosine":{"x":2}}""", strict);
|
||||
both.Should().Throw<JsonException>().WithMessage("*appears more than once*");
|
||||
Action reversed = () => JsonSerializer.Deserialize<CultPhasor>("""{"sine":{"x":1},"sin":{"x":2}}""", strict);
|
||||
reversed.Should().Throw<JsonException>().WithMessage("*appears more than once*");
|
||||
|
||||
// With duplicates allowed the later property replaces the earlier one, as for any repeated name.
|
||||
Parse<CultPhasor>("""{"cos":{"x":1,"y":9},"cosine":{"x":2}}""").cosine.Should().Be(new float4(2f, 0f, 0f, 0f));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Reading_AcceptsTheLegacyPhasorNamesUnderDisallow()
|
||||
{
|
||||
var options = new JsonSerializerOptions { UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow }.AddCultMathConverters();
|
||||
var phasor = JsonSerializer.Deserialize<CultPhasor>("""{"cos":{"x":1,"y":2,"z":3,"w":4},"sin":{"x":5,"y":6,"z":7,"w":8}}""", options);
|
||||
BitsOf(phasor).Should().Equal(BitsOf(new CultPhasor(new float4(1f, 2f, 3f, 4f), new float4(5f, 6f, 7f, 8f))));
|
||||
Action unknown = () => JsonSerializer.Deserialize<CultPhasor>("""{"cos":{"x":1},"coss":{"x":2}}""", options);
|
||||
unknown.Should().Throw<JsonException>().WithMessage("*not a component*");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Reading_TreatsAThirdOccurrenceOfAPhasorNameAsADuplicate()
|
||||
{
|
||||
var property = typeof(JsonSerializerOptions).GetProperty("AllowDuplicateProperties");
|
||||
Assert.That(property, Is.Not.Null, "the test runtime's System.Text.Json has AllowDuplicateProperties");
|
||||
var strict = new JsonSerializerOptions().AddCultMathConverters();
|
||||
property!.SetValue(strict, false);
|
||||
Action thrice = () => JsonSerializer.Deserialize<CultPhasor>("""{"cos":{"x":1},"cosine":{"x":2},"cos":{"x":3}}""", strict);
|
||||
thrice.Should().Throw<JsonException>().WithMessage("*appears more than once*");
|
||||
|
||||
// Allowed, the last occurrence replaces the whole value, including after a third.
|
||||
Parse<CultPhasor>("""{"cos":{"x":1,"y":9},"cosine":{"x":2},"cos":{"z":3}}""").cosine.Should().Be(new float4(0f, 0f, 3f, 0f));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Reading_SkipsUnknownNamesWhenCaseInsensitive()
|
||||
{
|
||||
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }.AddCultMathConverters();
|
||||
JsonSerializer.Deserialize<CultPhasor>("""{"extra":[1,2],"COSINE":{"x":3},"coss":{"x":4}}""", options).cosine.x.Should().Be(3f);
|
||||
JsonSerializer.Deserialize<float2>("""{"X":1,"q":2,"Y":3}""", options).Should().Be(new float2(1f, 3f));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Writing_NeverEmitsLegacyNames()
|
||||
{
|
||||
using var document = JsonDocument.Parse(Json(new CultPhasor(new float4(1f, 1f, 1f, 1f), new float4(2f, 2f, 2f, 2f))));
|
||||
document.RootElement.EnumerateObject().Select(p => p.Name).Should().Equal("cosine", "sine");
|
||||
}
|
||||
|
||||
private static int[] BitsOf(CultPhasor v) => new[] { v.cosine.x, v.cosine.y, v.cosine.z, v.cosine.w, v.sine.x, v.sine.y, v.sine.z, v.sine.w }
|
||||
.Select(BitConverter.SingleToInt32Bits).ToArray();
|
||||
|
||||
[Test]
|
||||
public void Reading_RefusesShapesThatAreNotTheType()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
[{"cos":{"x":1,"y":2,"z":3,"w":4},"sin":{"x":5,"y":6,"z":7,"w":8}},{"cos":{"x":-5.849812,"y":0.051139403,"z":0.14261721,"w":-0.087419346},"sin":{"x":-2.0218675,"y":0.7666904,"z":-0.22879826,"w":0.9240916}}]
|
||||
|
|
@ -6,6 +6,11 @@ All notable changes to this package are documented in this file.
|
|||
|
||||
### Breaking
|
||||
|
||||
- `CultMathJson` writes `CultPhasor` as `{"cosine":..,"sine":..}` and still reads
|
||||
the old `cos`/`sin` keys. A reader from before this change does not know the
|
||||
new keys: with default options it skips them and decodes zeros, and with
|
||||
`UnmappedMemberHandling.Disallow` it throws. Upgrade readers before writers.
|
||||
MessagePack bytes are unchanged.
|
||||
- CultCache refuses an unknown union arm on read and an unnamed union subtype on
|
||||
write. A payload whose arm key the union does not declare used to decode to
|
||||
`null` (and the next save wrote the loss back); a value whose type the union
|
||||
|
|
|
|||
Loading…
Reference in a new issue