diff --git a/.editorconfig b/.editorconfig index a4ac5b64..27feb2d7 100644 --- a/.editorconfig +++ b/.editorconfig @@ -254,8 +254,7 @@ dotnet_diagnostic.ca1506.severity = warning # Avoid excessive class coupling dotnet_diagnostic.ca1507.severity = warning # Use nameof in place of string dotnet_diagnostic.ca1508.severity = warning # Avoid dead conditional code dotnet_diagnostic.ca1509.severity = warning # Invalid entry in code metrics configuration file -dotnet_diagnostic.ca1861.severity = none # Prefer 'static readonly' fields over constant array arguments if the called method is called repeatedly and is not mutating the passed array (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1861) - +dotnet_diagnostic.ca1861.severity = suggestion # Prefer 'static readonly' fields over constant array arguments if the called method is called repeatedly and is not mutating the passed array (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1861) # Performance rules diff --git a/Speckle.Sdk.sln.DotSettings b/Speckle.Sdk.sln.DotSettings index 9e5ec22f..691a8f91 100644 --- a/Speckle.Sdk.sln.DotSettings +++ b/Speckle.Sdk.sln.DotSettings @@ -1,2 +1,3 @@  - QL \ No newline at end of file + QL + XYZ \ No newline at end of file diff --git a/src/Speckle.Objects/Geometry/Mesh.cs b/src/Speckle.Objects/Geometry/Mesh.cs index 64079c69..27f2e60a 100644 --- a/src/Speckle.Objects/Geometry/Mesh.cs +++ b/src/Speckle.Objects/Geometry/Mesh.cs @@ -1,24 +1,46 @@ +using System.Diagnostics.Contracts; using Speckle.Newtonsoft.Json; using Speckle.Objects.Other; +using Speckle.Objects.Utils; using Speckle.Sdk; using Speckle.Sdk.Common; using Speckle.Sdk.Models; namespace Speckle.Objects.Geometry; +/// More docs on notion [SpeckleType("Objects.Geometry.Mesh")] public class Mesh : Base, IHasBoundingBox, IHasVolume, IHasArea, ITransformable { + /// + /// Flat list of vertex data (flat x,y,z,x,y,z... list) + /// [DetachProperty, Chunkable(31250)] public required List vertices { get; set; } + /// + /// Flat list of face data
+ /// Each face starts with the length of the face (e.g. 3 in the case of triangles), followed by that many indices + ///
+ /// + /// N-gons are supported, but large values of n (> ~50) tend to cause significant performance problems for consumers (e.g. HostApps and . + /// + /// + /// [ + /// 3, 0, 1, 2, //first face, a triangle (3-gon) + /// 4, 1, 2, 3, 4, //second face, a quad (4-gon) + /// 6, 4, 5, 6, 7, 8, 9, //third face, an n-gon (6-gon) + /// ]; [DetachProperty, Chunkable(62500)] public required List faces { get; set; } - /// Vertex colors as ARGB s + /// Vertex colors as ARGB s + /// Expected that there are either 1 color per vertex, or an empty [DetachProperty, Chunkable(62500)] public List colors { get; set; } = new(); + /// Flat list of texture coordinates (flat u,v,u,v,u,v... list) + /// Expected that there are either 1 texture coordinate per vertex, or an empty [DetachProperty, Chunkable(31250)] public List textureCoordinates { get; set; } = new(); @@ -85,8 +107,6 @@ public bool TransformTo(Transform transform, out ITransformable transformed) return res; } - #region Convenience Methods - [JsonIgnore] public int VerticesCount => vertices.Count / 3; @@ -98,6 +118,8 @@ public bool TransformTo(Transform transform, out ITransformable transformed) /// /// The index of the vertex /// Vertex as a + /// It is usually recommended to instead consume the list manually for better performance + [Pure] public Point GetPoint(int index) { index *= 3; @@ -106,6 +128,8 @@ public Point GetPoint(int index) /// as list of s /// when list is malformed + /// It is usually recommended to instead consume the list manually for better performance + [Pure] public List GetPoints() { if (vertices.Count % 3 != 0) @@ -129,78 +153,10 @@ public List GetPoints() /// /// The index of the texture coordinate /// Texture coordinate as a + [Pure] public (double, double) GetTextureCoordinate(int index) { index *= 2; return (textureCoordinates[index], textureCoordinates[index + 1]); } - - /// - /// If not already so, this method will align - /// such that a vertex and its corresponding texture coordinates have the same index. - /// This alignment is what is expected by most applications.
- ///
- /// - /// If the calling application expects - /// vertices.count == textureCoordinates.count - /// Then this method should be called by the MeshToNative method before parsing and - /// to ensure compatibility with geometry originating from applications that map to using vertex instance index (rather than vertex index) - ///
- /// , , and lists will be modified to contain no shared vertices (vertices shared between polygons) - ///
- public void AlignVerticesWithTexCoordsByIndex() - { - if (textureCoordinates.Count == 0) - { - return; - } - - if (TextureCoordinatesCount == VerticesCount) - { - return; //Tex-coords already aligned as expected - } - - var facesUnique = new List(faces.Count); - var verticesUnique = new List(TextureCoordinatesCount * 3); - bool hasColors = colors.Count > 0; - var colorsUnique = hasColors ? new List(TextureCoordinatesCount) : null; - - int nIndex = 0; - while (nIndex < faces.Count) - { - int n = faces[nIndex]; - if (n < 3) - { - n += 3; // 0 -> 3, 1 -> 4 - } - - if (nIndex + n >= faces.Count) - { - break; //Malformed face list - } - - facesUnique.Add(n); - for (int i = 1; i <= n; i++) - { - int vertIndex = faces[nIndex + i]; - int newVertIndex = verticesUnique.Count / 3; - - var (x, y, z) = GetPoint(vertIndex); - verticesUnique.Add(x); - verticesUnique.Add(y); - verticesUnique.Add(z); - - colorsUnique?.Add(colors[vertIndex]); - facesUnique.Add(newVertIndex); - } - - nIndex += n + 1; - } - - vertices = verticesUnique; - colors = colorsUnique ?? colors; - faces = facesUnique; - } - - #endregion } diff --git a/src/Speckle.Objects/Geometry/Vector.cs b/src/Speckle.Objects/Geometry/Vector.cs index 83114648..a0c06698 100644 --- a/src/Speckle.Objects/Geometry/Vector.cs +++ b/src/Speckle.Objects/Geometry/Vector.cs @@ -86,15 +86,9 @@ public bool TransformTo(Transform transform, out ITransformable transformed) /// Returns the coordinates of this as a list of numbers /// /// A list of coordinates {x, y, z} - public List ToList() - { - return new List { x, y, z }; - } + public List ToList() => [x, y, z]; - public Point ToPoint() - { - return new Point(x, y, z, units, applicationId); - } + public Point ToPoint() => new(x, y, z, units, applicationId); /// /// Creates a new vector based on a list of coordinates and the unit they're drawn in. @@ -176,11 +170,6 @@ public static Vector CrossProduct(Vector u, Vector v) return new Vector(x, y, z, units: u.units); } - public static double Angle(Vector u, Vector v) - { - return Math.Acos(DotProduct(u, v) / (u.Length * v.Length)); - } - /// /// Compute and return a unit vector from this vector /// @@ -205,23 +194,6 @@ public Vector Negate() return this; } - /// - /// Returns a normalized copy of this vector. - /// - /// A copy of this vector unitized. - public Vector Unit() - { - return this / Length; - } - - /// - /// Constructs a new from a - /// - /// The point whose coordinates will be used - /// The unique application ID of the object. - [Obsolete($"Use {nameof(Point.ToVector)}", true)] - public Vector(Point point, string? applicationId = null) { } - /// /// Gets or sets the coordinates of the vector /// diff --git a/src/Speckle.Sdk/Common/Units.cs b/src/Speckle.Sdk/Common/Units.cs index e6d4c854..f4a7e12f 100644 --- a/src/Speckle.Sdk/Common/Units.cs +++ b/src/Speckle.Sdk/Common/Units.cs @@ -1,4 +1,5 @@ using System.Diagnostics.Contracts; +using Speckle.Sdk.Dependencies; namespace Speckle.Sdk.Common; @@ -19,7 +20,7 @@ public static class Units /// US Survey foot, now not supported by Speckle, kept privately for backwards compatibility private const string USFeet = "us_ft"; - internal static readonly List SupportedUnits = new() + internal static readonly IReadOnlyCollection SupportedUnits = new[] { Millimeters, Centimeters, @@ -30,7 +31,7 @@ public static class Units Yards, Miles, None, - }; + }.Freeze(); /// /// if is a recognised/supported unit string, otherwise diff --git a/tests/Speckle.Objects.Tests.Unit/Geometry/MeshTests.cs b/tests/Speckle.Objects.Tests.Unit/Geometry/MeshTests.cs index f1ec0d5e..31a44bd4 100644 --- a/tests/Speckle.Objects.Tests.Unit/Geometry/MeshTests.cs +++ b/tests/Speckle.Objects.Tests.Unit/Geometry/MeshTests.cs @@ -6,22 +6,7 @@ namespace Speckle.Objects.Tests.Unit.Geometry; public class MeshTests { - private static readonly Mesh[] TestCaseSource = { CreateBlenderStylePolygon(), CreateRhinoStylePolygon() }; - - [Theory] - [MemberData(nameof(GetTestCaseSource))] - public void CanAlignVertices(Mesh inPolygon) - { - inPolygon.AlignVerticesWithTexCoordsByIndex(); - - inPolygon.VerticesCount.Should().Be(inPolygon.TextureCoordinatesCount); - - var expectedPolygon = CreateRhinoStylePolygon(); - - inPolygon.vertices.Should().BeEquivalentTo(expectedPolygon.vertices); - inPolygon.faces.Should().BeEquivalentTo(expectedPolygon.faces); - inPolygon.textureCoordinates.Should().BeEquivalentTo(expectedPolygon.textureCoordinates); - } + private static readonly Mesh[] TestCaseSource = { CreateRhinoStylePolygon(), CreateEmpty() }; public static IEnumerable GetTestCaseSource() => TestCaseSource.Select(mesh => new object[] { mesh }); @@ -36,14 +21,51 @@ private static Mesh CreateRhinoStylePolygon() }; } - private static Mesh CreateBlenderStylePolygon() + private static Mesh CreateEmpty() { return new Mesh { - vertices = new List { 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0 }, - faces = new List { 3, 0, 1, 2, 3, 0, 2, 3 }, - textureCoordinates = new List { 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0 }, + vertices = [], + faces = [], + textureCoordinates = [], units = Units.Meters, }; } + + [Theory] + [MemberData(nameof(GetTestCaseSource))] + public void GetTextureCoordinate_ReturnsCorrectUVValue(Mesh testCase) + { + for (int i = 0, j = 0; i < testCase.textureCoordinates.Count; i += 2, j++) + { + var (u, v) = testCase.GetTextureCoordinate(j); + + u.Should().Be(testCase.textureCoordinates[i]); + v.Should().Be(testCase.textureCoordinates[i + 1]); + } + + Assert.Throws(() => testCase.GetTextureCoordinate(testCase.textureCoordinates.Count)); + } + + [Theory] + [MemberData(nameof(GetTestCaseSource))] + public void GetPoints_ReturnsVerticesAsPoints(Mesh testCase) + { + testCase.VerticesCount.Should().Be(testCase.vertices.Count / 3); + + var getPoints = testCase.GetPoints(); + var getPoint = Enumerable.Range(0, testCase.VerticesCount).Select(i => testCase.GetPoint(i)); + + //Test each point has correct units + getPoints.Select(x => x.units).Should().AllBe(testCase.units).And.HaveCount(testCase.VerticesCount); + getPoints.Select(x => x.units).Should().AllBe(testCase.units).And.HaveCount(testCase.VerticesCount); + + //Convert back to flat list + var expected = testCase.vertices; + var getPointsList = getPoints.SelectMany(x => x.ToList()); + var getPointList = getPoint.SelectMany(x => x.ToList()); + + getPointsList.Should().BeEquivalentTo(expected); + getPointList.Should().BeEquivalentTo(expected); + } } diff --git a/tests/Speckle.Objects.Tests.Unit/Geometry/PointTests.cs b/tests/Speckle.Objects.Tests.Unit/Geometry/PointTests.cs index 3bd9d736..8de0cba0 100644 --- a/tests/Speckle.Objects.Tests.Unit/Geometry/PointTests.cs +++ b/tests/Speckle.Objects.Tests.Unit/Geometry/PointTests.cs @@ -1,6 +1,8 @@ using System.Diagnostics.CodeAnalysis; using FluentAssertions; +using Speckle.DoubleNumerics; using Speckle.Objects.Geometry; +using Speckle.Objects.Other; using Speckle.Sdk.Common; namespace Speckle.Objects.Tests.Unit.Geometry; @@ -64,4 +66,131 @@ bool expectedResult (p1 == p2).Should().Be(expectedResult); } + + [Fact] + public void TestDistanceTo() + { + //Arrange + var p1 = new Point(1, 0, 0, units: Units.Meters); + var p2 = new Point(0, 0, 0, units: Units.Meters); + + //Act + var result = p1.DistanceTo(p2); + + //Assert + result.Should().Be(1); + } + + private static IReadOnlyList MatrixTestData => + [ + Matrix4x4.Identity, + Matrix4x4.CreateScale(1, 2, 3), + Matrix4x4.CreateTranslation(100, 10, 0), + Matrix4x4.CreateRotationZ(1), + ]; + + private static IReadOnlyList PointTestData => + [ + new(1, 2, 3, Units.Meters), + new(0.5, 100.5, 123.123, Units.Meters), + new(1, 2, 3, Units.Meters, applicationId: "Test me!"), + new(0, 0, 0, Units.Feet), + ]; + + public static TheoryData PointTestCases() => new(PointTestData); + + public static TheoryData TransformTestCases() + { + TheoryData testCases = new(); + for (int i = 0; i < PointTestData.Count; i++) + { + testCases.Add(MatrixTestData[i], PointTestData[i]); + } + return testCases; + } + + [Theory] + [MemberData(nameof(TransformTestCases))] + public void TransformPoint_SameUnits(Matrix4x4 matrix, Point point) + { + //Arrange + Transform t = new() { matrix = Matrix4x4.Transpose(matrix), units = point.units }; + + Vector3 expectedVector = Vector3.Transform(new(point.x, point.y, point.z), matrix); + var expectedResult = (expectedVector.X, expectedVector.Y, expectedVector.Z); + + //Act + point.TransformTo(t, out Point transformedPoint); + var actualResult = (transformedPoint.x, transformedPoint.y, transformedPoint.z); + + //Assert + actualResult.Should().Be(expectedResult); + transformedPoint.applicationId.Should().Be(point.applicationId); + + transformedPoint.applicationId.Should().Be(point.applicationId); + transformedPoint.id.Should().Be(point.id); + transformedPoint.units.Should().Be(point.units); + } + + [Fact(Skip = "Something clearly wrong with units!!!")] + public void TransformingPoint_ChangeOfUnits() + { + //Arrange + Point point = new(0, 0, 10, Units.Meters); + Transform t = new() + { + matrix = Matrix4x4.Transpose(Matrix4x4.CreateTranslation(1000, 0, 0)), + units = Units.Millimeters, + }; + Vector3 expected = new(1, 0, 10); + + //Act + point.TransformTo(t, out Point transformedPoint); + + //Assert + (double x, double y, double z) = transformedPoint; + transformedPoint.units.Should().Be(point.units); + transformedPoint.applicationId.Should().Be(point.applicationId); + + new Vector3(x, y, z).Should().Be(expected); + } + + [Theory] + [MemberData(nameof(PointTestCases))] + public void ToVector(Point testCase) + { + var expectedXYZ = (testCase.x, testCase.y, testCase.z); + var expectedUnits = testCase.units; + var expectedApplicationId = testCase.applicationId; + + var asVector = testCase.ToVector(); + var resultXYZ = (asVector.x, asVector.y, asVector.z); + + resultXYZ.Should().Be(expectedXYZ); + asVector.units.Should().Be(expectedUnits); + asVector.applicationId.Should().Be(expectedApplicationId); + } + + [Theory] + [MemberData(nameof(PointTestCases))] + public void Deconstruct_Double_Double_Double_String(Point testCase) + { + (double x, double y, double z, string? units) = testCase; + + x.Should().Be(testCase.x); + y.Should().Be(testCase.y); + z.Should().Be(testCase.z); + units.Should().Be(testCase.units); + } + + [Theory] + [MemberData(nameof(PointTestCases))] + public void Deconstruct_Double_Double_Double(Point testCase) + { + (double x, double y, double z) = testCase; + + x.Should().Be(testCase.x); + y.Should().Be(testCase.y); + z.Should().Be(testCase.z); + } } diff --git a/tests/Speckle.Objects.Tests.Unit/Geometry/VectorTests.cs b/tests/Speckle.Objects.Tests.Unit/Geometry/VectorTests.cs new file mode 100644 index 00000000..1631dd1e --- /dev/null +++ b/tests/Speckle.Objects.Tests.Unit/Geometry/VectorTests.cs @@ -0,0 +1,233 @@ +using FluentAssertions; +using Speckle.DoubleNumerics; +using Speckle.Objects.Geometry; + +namespace Speckle.Objects.Tests.Unit.Geometry; + +public class VectorTests +{ + private const float FLOAT_TOLERANCE = 1e-6f; + + public static TheoryData TestCases() => + new() + { + { 0d, 0d, 0d, "m" }, + { 1d, 2d, 3d, "ft" }, + { 0d, 0d, -1d, "km" }, + { 100d, 0d, -200d, "in" }, + { 123.123d, 456.456d, 5789.789d, "cm" }, + { -123.123d, -456.456d, -5789.789d, "mm" }, + }; + + [Theory] + [MemberData(nameof(TestCases))] + public void Constructors_AreTheSame(double x, double y, double z, string units) + { + const string appId = "asdfasdfasdf"; + var pctor = new Vector(x, y, z, units, applicationId: appId); + + pctor.x.Should().Be(x); + pctor.y.Should().Be(y); + pctor.z.Should().Be(z); + pctor.units.Should().Be(units); + pctor.applicationId.Should().Be(appId); + + var init = new Vector + { + x = x, + y = y, + z = z, + units = units, + applicationId = appId, + }; + + Assert.Equal(pctor.x, init.x); + Assert.Equal(pctor.y, init.y); + Assert.Equal(pctor.z, init.z); + Assert.Equal(pctor.units, init.units); + Assert.Equal(pctor.applicationId, init.applicationId); + } + + [Theory] + [InlineData(1d, 0d, 0d, 1d)] + [InlineData(0d, 2d, 0d, 2d)] + [InlineData(0d, 0d, -3d, 3d)] + [InlineData(1d, 1d, 0d, 1.4142135623730951d)] + public void LengthCalculated(double x, double y, double z, double expected) + { + var testCase = new Vector(x, y, z, ""); + var actual = testCase.Length; + actual.Should().Be(expected); + } + + [Theory] + [MemberData(nameof(TestCases))] + public void EncodingToAndFromList(double x, double y, double z, string units) + { + var testCase = new Vector(x, y, z, units); + + var encoded = testCase.ToList(); + encoded.Should().BeEquivalentTo([x, y, z]); + + const string NEW_UNIT = "something different..."; + var decoded = Vector.FromList(encoded, NEW_UNIT); + + decoded.x.Should().Be(x); + decoded.y.Should().Be(y); + decoded.z.Should().Be(z); + decoded.units.Should().Be(NEW_UNIT); + } + + [Theory] + [MemberData(nameof(TestCases))] + public void ToPoint(double x, double y, double z, string units) + { + var testCase = new Vector(x, y, z, units, "asdfasdf"); + + var asPoint = testCase.ToPoint(); + + asPoint.x.Should().Be(x); + asPoint.y.Should().Be(y); + asPoint.z.Should().Be(z); + asPoint.units.Should().Be(units); + asPoint.applicationId.Should().Be("asdfasdf"); + } + + [Theory] + [MemberData(nameof(TestCases))] + public void Normalize(double x, double y, double z, string units) + { + var sut = new Vector(x, y, z, units); + var originalLength = sut.Length; + sut.Normalize(); + + if (!(originalLength > 0)) + { + sut.Length.Should().Be(double.NaN); + return; + } + + sut.Length.Should().BeApproximately(1, FLOAT_TOLERANCE); + + var rescaled = sut * originalLength; + + rescaled.x.Should().BeApproximately(x, FLOAT_TOLERANCE); + rescaled.y.Should().BeApproximately(y, FLOAT_TOLERANCE); + rescaled.z.Should().BeApproximately(z, FLOAT_TOLERANCE); + rescaled.units.Should().Be(units); + } + + [Theory] + [MemberData(nameof(TestCases))] + public void Negate(double x, double y, double z, string units) + { + var sut = new Vector(x, y, z, units); + var originalLength = sut.Length; + sut.Negate(); + + sut.Length.Should().Be(originalLength); + var rescaled = sut.Negate(); + + rescaled.x.Should().BeApproximately(x, FLOAT_TOLERANCE); + rescaled.y.Should().BeApproximately(y, FLOAT_TOLERANCE); + rescaled.z.Should().BeApproximately(z, FLOAT_TOLERANCE); + rescaled.units.Should().Be(units); + } + + [Theory] + [MemberData(nameof(TestCases))] + public void TestAddition(double x, double y, double z, string units) + { + var operand1 = new Vector(x, y, z, units); + var operand2 = new Vector(x, y, z, units); + + var result = operand1 + operand2; + double[] expected = [x + x, y + y, z + z]; + + result.ToList().Should().BeEquivalentTo(expected); + result.units.Should().BeEquivalentTo(units); + } + + [Theory] + [MemberData(nameof(TestCases))] + public void TestSubtraction(double x, double y, double z, string units) + { + var operand1 = new Vector(x, y, z, units); + var operand2 = new Vector(x, y, z, units); + + var result = operand1 - operand2; + double[] expected = [x - x, y - y, z - z]; + + result.ToList().Should().BeEquivalentTo(expected); + result.units.Should().BeEquivalentTo(units); + } + + [Theory] + [MemberData(nameof(TestCases))] + public void TestDivision(double x, double y, double z, string units) + { + var operand1 = new Vector(x, y, z, units); + const int OPERAND2 = 2; + + var result = operand1 / OPERAND2; + double[] expected = [x / OPERAND2, y / OPERAND2, z / OPERAND2]; + + result.ToList().Should().BeEquivalentTo(expected); + result.units.Should().BeEquivalentTo(units); + } + + [Theory] + [MemberData(nameof(TestCases))] + public void TestMultiplication(double x, double y, double z, string units) + { + var operand1 = new Vector(x, y, z, units); + const int OPERAND2 = 2; + + var result = operand1 * OPERAND2; + double[] expected = [x * OPERAND2, y * OPERAND2, z * OPERAND2]; + + result.ToList().Should().BeEquivalentTo(expected); + result.units.Should().BeEquivalentTo(units); + } + + [Theory] + [MemberData(nameof(TestCases))] + public void TestDotProduct(double x, double y, double z, string units) + { + var operand1 = new Vector(x, y, z, units); + var operand2 = new Vector(x, y, z, units); + + var result = Vector.DotProduct(operand1, operand2); + double expected = Vector3.Dot(new Vector3(x, y, z), new Vector3(x, y, z)); + + result.Should().BeApproximately(expected, FLOAT_TOLERANCE); + } + + [Theory] + [MemberData(nameof(TestCases))] + public void TestCrossProduct(double x, double y, double z, string units) + { + var operand1 = new Vector(x, y, z, units); + var operand2 = new Vector(x, y, z, units); + + var result = Vector.CrossProduct(operand1, operand2); + var expected = Vector3.Cross(new Vector3(x, y, z), new Vector3(x, y, z)); + + result.x.Should().BeApproximately(expected.X, FLOAT_TOLERANCE); + result.y.Should().BeApproximately(expected.Y, FLOAT_TOLERANCE); + result.z.Should().BeApproximately(expected.Z, FLOAT_TOLERANCE); + } + + [Theory] + [MemberData(nameof(TestCases))] + [Obsolete("Tests Obsolete legacy behaviour to maintain backwards json compatibility with ~2.13? data")] + public void TestLegacyValueProp(double x, double y, double z, string _) + { + var vector = Activator.CreateInstance(); + vector.value = [x, y, z]; + + vector.x.Should().Be(x); + vector.y.Should().Be(y); + vector.z.Should().Be(z); + } +} diff --git a/tests/Speckle.Sdk.Tests.Unit/Common/UnitsTest.cs b/tests/Speckle.Sdk.Tests.Unit/Common/UnitsTest.cs index c93a0b51..3f8caa7c 100644 --- a/tests/Speckle.Sdk.Tests.Unit/Common/UnitsTest.cs +++ b/tests/Speckle.Sdk.Tests.Unit/Common/UnitsTest.cs @@ -8,11 +8,9 @@ public class UnitsTest { private const double EPS = 0.00022; - public static List OfficiallySupportedUnits => Units.SupportedUnits; - - public static List NotSupportedUnits => ["feeters", "liters", "us_ft"]; - - public static List ConversionSupport => Units.SupportedUnits.Concat([null]).ToList(); + public static IReadOnlyCollection OfficiallySupportedUnits => Units.SupportedUnits; + public static IReadOnlyCollection NotSupportedUnits => ["feeters", "liters", "us_ft"]; + public static IReadOnlyCollection ConversionSupport => [.. Units.SupportedUnits, null]; [Theory] [MemberData(nameof(ConversionSupportGenerator))]