< Summary

Information
Class: FixedMathSharp.Geometry.FixedTriangle2d
Assembly: FixedMathSharp
File(s): /home/runner/work/FixedMathSharp/FixedMathSharp/src/FixedMathSharp/Geometry/Primitives/Triangles/FixedTriangle2d.cs
Line coverage
100%
Covered lines: 96
Uncovered lines: 0
Coverable lines: 96
Total lines: 382
Line coverage: 100%
Branch coverage
100%
Covered branches: 40
Total branches: 40
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
get_SignedArea()100%11100%
get_Area()100%11100%
get_Bounds()100%11100%
get_Centroid()100%11100%
get_IsDegenerate()100%11100%
GetVertex(...)100%44100%
GetEdge(...)100%44100%
GetPoint(...)100%11100%
TryGetBarycentricWeights(...)100%22100%
Contains(...)100%1212100%
ClosestPoint(...)100%22100%
DistanceSquared(...)100%11100%
Deconstruct(...)100%11100%
op_Equality(...)100%11100%
op_Inequality(...)100%11100%
Equals(...)100%44100%
Equals(...)100%22100%
GetHashCode()100%11100%
IsPointOnAnyEdge(...)100%44100%
ClosestPointOnEdges(...)100%11100%
TrySetCloserPoint(...)100%22100%
GetDoubledArea()100%11100%
GetCrossProduct(...)100%11100%
IsNonnegativeWithinEpsilon(...)100%22100%
IsNonpositiveWithinEpsilon(...)100%22100%
ComponentMin(...)100%11100%
ComponentMax(...)100%11100%

File(s)

/home/runner/work/FixedMathSharp/FixedMathSharp/src/FixedMathSharp/Geometry/Primitives/Triangles/FixedTriangle2d.cs

#LineLine coverage
 1//=======================================================================
 2// FixedTriangle2d.cs
 3//=======================================================================
 4// MIT License, Copyright (c) 2024–present David Oravsky (mrdav30)
 5// See LICENSE file in the project root for full license information.
 6//=======================================================================
 7
 8using System;
 9using System.Runtime.CompilerServices;
 10using System.Text.Json.Serialization;
 11using MemoryPack;
 12
 13namespace FixedMathSharp.Geometry;
 14
 15/// <summary>
 16/// Represents a triangle in two-dimensional fixed-point space.
 17/// </summary>
 18[Serializable]
 19[MemoryPackable]
 20public partial struct FixedTriangle2d : IEquatable<FixedTriangle2d>
 21{
 22    #region Constants
 23
 24    /// <summary>
 25    /// The number of vertices in a triangle.
 26    /// </summary>
 27    public const int VertexCount = 3;
 28
 29    /// <summary>
 30    /// The number of edges in a triangle.
 31    /// </summary>
 32    public const int EdgeCount = 3;
 33
 34    #endregion
 35
 36    #region Fields
 37
 38    /// <summary>
 39    /// The first vertex.
 40    /// </summary>
 41    [JsonInclude]
 42    [MemoryPackOrder(0)]
 43    public Vector2d A;
 44
 45    /// <summary>
 46    /// The second vertex.
 47    /// </summary>
 48    [JsonInclude]
 49    [MemoryPackOrder(1)]
 50    public Vector2d B;
 51
 52    /// <summary>
 53    /// The third vertex.
 54    /// </summary>
 55    [JsonInclude]
 56    [MemoryPackOrder(2)]
 57    public Vector2d C;
 58
 59    #endregion
 60
 61    #region Constructors
 62
 63    /// <summary>
 64    /// Initializes a triangle from ordered vertices.
 65    /// </summary>
 66    [JsonConstructor]
 67    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 68    public FixedTriangle2d(Vector2d a, Vector2d b, Vector2d c)
 69    {
 5370        A = a;
 5371        B = b;
 5372        C = c;
 5373    }
 74
 75    #endregion
 76
 77    #region Properties
 78
 79    /// <summary>
 80    /// The signed area of the triangle. Positive values indicate counter-clockwise winding.
 81    /// </summary>
 82    /// <remarks>
 83    /// The exact endpoint-difference cross product is halved and converted once
 84    /// with round-half-to-even and signed saturation.
 85    /// </remarks>
 86    [JsonIgnore]
 87    [MemoryPackIgnore]
 88    public Fixed64 SignedArea
 89    {
 90        [MethodImpl(MethodImplOptions.AggressiveInlining)]
 2391        get => Fixed64.RoundSignedToFixed(GetDoubledArea(), FixedMath.SHIFT_AMOUNT_I + 1);
 92    }
 93
 94    /// <summary>
 95    /// The non-negative saturating magnitude of <see cref="SignedArea"/>.
 96    /// </summary>
 97    [JsonIgnore]
 98    [MemoryPackIgnore]
 99    public Fixed64 Area
 100    {
 101        [MethodImpl(MethodImplOptions.AggressiveInlining)]
 8102        get => FixedMath.Abs(SignedArea);
 103    }
 104
 105    /// <summary>
 106    /// The normalized axis-aligned area that contains all vertices.
 107    /// </summary>
 108    [JsonIgnore]
 109    [MemoryPackIgnore]
 110    public FixedBoundArea Bounds
 111    {
 112        [MethodImpl(MethodImplOptions.AggressiveInlining)]
 2113        get => FixedBoundArea.FromMinMax(ComponentMin(ComponentMin(A, B), C), ComponentMax(ComponentMax(A, B), C));
 114    }
 115
 116    /// <summary>
 117    /// The arithmetic center of the three vertices, averaged independently per component.
 118    /// </summary>
 119    [JsonIgnore]
 120    [MemoryPackIgnore]
 121    public Vector2d Centroid
 122    {
 123        [MethodImpl(MethodImplOptions.AggressiveInlining)]
 4124        get => new(
 4125            FixedMath.Average(A.X, B.X, C.X),
 4126            FixedMath.Average(A.Y, B.Y, C.Y));
 127    }
 128
 129    /// <summary>
 130    /// Returns true when the exact area magnitude is less than or equal to
 131    /// <see cref="Fixed64.Epsilon"/>.
 132    /// </summary>
 133    [JsonIgnore]
 134    [MemoryPackIgnore]
 135    public bool IsDegenerate
 136    {
 137        [MethodImpl(MethodImplOptions.AggressiveInlining)]
 6138        get => WideArithmetic.IsMagnitudeAtMost(
 6139            GetDoubledArea(),
 6140            (ulong)Fixed64.Epsilon.m_rawValue,
 6141            FixedMath.SHIFT_AMOUNT_I + 1);
 142    }
 143
 144    #endregion
 145
 146    #region Geometry Access
 147
 148    /// <summary>
 149    /// Gets a vertex by stable index: 0 = A, 1 = B, 2 = C.
 150    /// </summary>
 151    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 152    public Vector2d GetVertex(int index) =>
 5153        index switch
 5154        {
 1155            0 => A,
 1156            1 => B,
 1157            2 => C,
 2158            _ => throw new ArgumentOutOfRangeException(nameof(index), $"Vertex index must be between 0 and {VertexCount 
 5159        };
 160
 161    /// <summary>
 162    /// Gets an edge by stable index: 0 = AB, 1 = BC, 2 = CA.
 163    /// </summary>
 164    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 165    public FixedSegment2d GetEdge(int index) =>
 64166        index switch
 64167        {
 24168            0 => new FixedSegment2d(A, B),
 19169            1 => new FixedSegment2d(B, C),
 19170            2 => new FixedSegment2d(C, A),
 2171            _ => throw new ArgumentOutOfRangeException(nameof(index), $"Edge index must be between 0 and {EdgeCount - 1}
 64172        };
 173
 174    /// <summary>
 175    /// Gets the point represented by barycentric weights for vertices B and C.
 176    /// </summary>
 177    /// <remarks>
 178    /// Each component uses the shared full-domain barycentric interpolation
 179    /// contract with one final round-half-to-even/saturating conversion.
 180    /// </remarks>
 181    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 182    public Vector2d GetPoint(Fixed64 weightB, Fixed64 weightC) =>
 6183        Vector2d.BarycentricCoordinates(A, B, C, weightB, weightC);
 184
 185    #endregion
 186
 187    #region Spatial Queries
 188
 189    /// <summary>
 190    /// Computes barycentric weights for a point relative to this triangle.
 191    /// </summary>
 192    /// <returns>
 193    /// True when the exact doubled-area magnitude is greater than
 194    /// <see cref="Fixed64.Epsilon"/>; otherwise false with three zero outputs.
 195    /// </returns>
 196    /// <remarks>
 197    /// The three weights use direct exact area numerators and are rounded and
 198    /// saturated independently. Reversing winding does not change the result.
 199    /// </remarks>
 200    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 201    public bool TryGetBarycentricWeights(Vector2d point, out Fixed64 weightA, out Fixed64 weightB, out Fixed64 weightC)
 202    {
 12203        Signed192 denominator = GetDoubledArea();
 12204        if (WideArithmetic.IsMagnitudeAtMost(
 12205            denominator,
 12206            (ulong)Fixed64.Epsilon.m_rawValue,
 12207            FixedMath.SHIFT_AMOUNT_I))
 208        {
 3209            weightA = Fixed64.Zero;
 3210            weightB = Fixed64.Zero;
 3211            weightC = Fixed64.Zero;
 3212            return false;
 213        }
 214
 9215        weightA = Fixed64.GetSignedRatio(GetCrossProduct(point, B, C), denominator);
 9216        weightB = Fixed64.GetSignedRatio(GetCrossProduct(point, C, A), denominator);
 9217        weightC = Fixed64.GetSignedRatio(GetCrossProduct(point, A, B), denominator);
 9218        return true;
 219    }
 220
 221    /// <summary>
 222    /// Determines whether the point is inside the triangle, including epsilon-wide edges and vertices.
 223    /// </summary>
 224    /// <remarks>
 225    /// Exact wide orientations make the winding-independent decision before
 226    /// scalar saturation. Collapsed line and point triangles retain edge-distance behavior.
 227    /// </remarks>
 228    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 229    public bool Contains(Vector2d point)
 230    {
 34231        Signed192 doubledArea = GetDoubledArea();
 34232        if (WideArithmetic.IsMagnitudeAtMost(
 34233            doubledArea,
 34234            (ulong)Fixed64.Epsilon.m_rawValue,
 34235            FixedMath.SHIFT_AMOUNT_I + 1))
 12236            return IsPointOnAnyEdge(point);
 237
 22238        Signed192 ab = GetCrossProduct(A, B, point);
 22239        Signed192 bc = GetCrossProduct(B, C, point);
 22240        Signed192 ca = GetCrossProduct(C, A, point);
 22241        ulong epsilonRaw = (ulong)Fixed64.Epsilon.m_rawValue;
 242
 22243        return doubledArea.Sign > 0
 22244            ? IsNonnegativeWithinEpsilon(ab, epsilonRaw)
 22245                && IsNonnegativeWithinEpsilon(bc, epsilonRaw)
 22246                && IsNonnegativeWithinEpsilon(ca, epsilonRaw)
 22247            : IsNonpositiveWithinEpsilon(ab, epsilonRaw)
 22248                && IsNonpositiveWithinEpsilon(bc, epsilonRaw)
 22249                && IsNonpositiveWithinEpsilon(ca, epsilonRaw);
 250    }
 251
 252    /// <summary>
 253    /// Finds the closest point on or inside this triangle to the supplied point.
 254    /// </summary>
 255    /// <remarks>
 256    /// Edge candidates are compared in AB, BC, CA order with exact squared
 257    /// distances. Exact ties retain the first candidate.
 258    /// </remarks>
 259    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 260    public Vector2d ClosestPoint(Vector2d point) =>
 11261        Contains(point) ? point : ClosestPointOnEdges(point);
 262
 263    /// <summary>
 264    /// Computes the squared distance from the supplied point to this triangle.
 265    /// </summary>
 266    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 267    public Fixed64 DistanceSquared(Vector2d point) =>
 3268        Vector2d.DistanceSquared(point, ClosestPoint(point));
 269
 270    #endregion
 271
 272    #region Deconstruction
 273
 274    /// <summary>
 275    /// Deconstructs the triangle into ordered vertices.
 276    /// </summary>
 277    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 278    public void Deconstruct(out Vector2d a, out Vector2d b, out Vector2d c)
 279    {
 1280        a = A;
 1281        b = B;
 1282        c = C;
 1283    }
 284
 285    #endregion
 286
 287    #region Operators
 288
 289    /// <summary>
 290    /// Determines whether two triangles have the same ordered vertices.
 291    /// </summary>
 292    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 2293    public static bool operator ==(FixedTriangle2d left, FixedTriangle2d right) => left.Equals(right);
 294
 295    /// <summary>
 296    /// Determines whether two triangles have different ordered vertices.
 297    /// </summary>
 298    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 2299    public static bool operator !=(FixedTriangle2d left, FixedTriangle2d right) => !left.Equals(right);
 300
 301    #endregion
 302
 303    #region Equality
 304
 305    /// <inheritdoc />
 306    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 8307    public bool Equals(FixedTriangle2d other) => A == other.A && B == other.B && C == other.C;
 308
 309    /// <inheritdoc />
 2310    public override bool Equals(object? obj) => obj is FixedTriangle2d other && Equals(other);
 311
 312    /// <inheritdoc />
 313    public override int GetHashCode()
 314    {
 315        unchecked
 316        {
 2317            int hash = 17;
 2318            hash = (hash * 31) + A.StateHash;
 2319            hash = (hash * 31) + B.StateHash;
 2320            hash = (hash * 31) + C.StateHash;
 2321            return hash;
 322        }
 323    }
 324
 325    #endregion
 326
 327    #region Helpers
 328
 329    private bool IsPointOnAnyEdge(Vector2d point)
 330    {
 12331        return GetEdge(0).DistanceSquared(point) <= Fixed64.Epsilon
 12332            || GetEdge(1).DistanceSquared(point) <= Fixed64.Epsilon
 12333            || GetEdge(2).DistanceSquared(point) <= Fixed64.Epsilon;
 334    }
 335
 336    private Vector2d ClosestPointOnEdges(Vector2d point)
 337    {
 9338        Vector2d best = GetEdge(0).ClosestPoint(point);
 9339        TrySetCloserPoint(GetEdge(1), point, ref best);
 9340        TrySetCloserPoint(GetEdge(2), point, ref best);
 9341        return best;
 342    }
 343
 344    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 345    private static void TrySetCloserPoint(FixedSegment2d edge, Vector2d point, ref Vector2d best)
 346    {
 18347        Vector2d candidate = edge.ClosestPoint(point);
 18348        if (Vector2d.CompareDistanceSquared(point, candidate, point, best) >= 0)
 14349            return;
 350
 4351        best = candidate;
 4352    }
 353
 354    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 75355    private Signed192 GetDoubledArea() => GetCrossProduct(A, B, C);
 356
 357    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 358    private static Signed192 GetCrossProduct(Vector2d origin, Vector2d first, Vector2d second) =>
 168359        WideGeometry.GetDifferenceCrossProduct2D(
 168360            first.X, origin.X, first.Y, origin.Y,
 168361            second.X, origin.X, second.Y, origin.Y);
 362
 363    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 364    private static bool IsNonnegativeWithinEpsilon(Signed192 value, ulong epsilonRaw) =>
 34365        value.Sign >= 0
 34366        || WideArithmetic.IsMagnitudeAtMost(value, epsilonRaw, FixedMath.SHIFT_AMOUNT_I);
 367
 368    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 369    private static bool IsNonpositiveWithinEpsilon(Signed192 value, ulong epsilonRaw) =>
 16370        value.Sign <= 0
 16371        || WideArithmetic.IsMagnitudeAtMost(value, epsilonRaw, FixedMath.SHIFT_AMOUNT_I);
 372
 373    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 374    private static Vector2d ComponentMin(Vector2d a, Vector2d b) =>
 4375        new(FixedMath.Min(a.X, b.X), FixedMath.Min(a.Y, b.Y));
 376
 377    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 378    private static Vector2d ComponentMax(Vector2d a, Vector2d b) =>
 4379        new(FixedMath.Max(a.X, b.X), FixedMath.Max(a.Y, b.Y));
 380
 381    #endregion
 382}