Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
History note: All version sections were reformatted on 2026-07-05 for one-time CONVENTIONS §CHANGELOG conformance: forbidden sub-headers folded into the six Keep a Changelog headers, non-user-facing content removed (internal refactors, test counts, coverage numbers, CI and build hygiene, governance churn, roadmap notes), bullets kept in past-tense active voice with code-formatted API leads. The nuget.org Release Notes tab and the GitHub Release for each shipped version are unchanged. A CI
family-lintgate keeps future sections conforming; each is frozen per Rule 7 once shipped.
Unreleased
0.5.0 - 2026-06-14: scale-invariant parallelism, angle and rotation assertions
Minor release. Fixes the scale-dependence of the parallel check and adds an angle-between assertion and a proper-rotation matrix assertion.
Added
Assert.That(vector).HasAngleBetweenApproximately(other, expectedRadians, tolerance)asserts the unsigned angle between two vectors (on[0, pi]) is within tolerance of an expected value. The angle is computed asatan2(|u x v|, u . v), which stays accurate across the whole range whereacosof the normalized dot product loses precision near0andpi.LinearAlgebra.AngleBetween(u, v)exposes the same computation on the framework-agnostic core.Assert.That(matrix).IsRotation(tolerance)asserts aMatrix4x4is a proper rotation: orthogonal (M * M^T = I) with determinant+1. Reflections (determinant-1) and matrices carrying translation or scale are rejected. Exposed on the core asLinearAlgebra.IsRotation.
Fixed
AreParallel/IsParallelToare now scale-invariant. The check compared the raw cross-product magnitude|u x v|against an absolute tolerance, so whether two vectors read as parallel depended on their length, not just their direction: the same angular deviation passed at unit scale and failed at large scale. It now compares the sine of the angle,|u x v| / (|u| |v|), against the tolerance (an angular measure), the same normalization the package already applied inIsCollinear. A zero or shorter-than-tolerance vector is still treated as parallel to any vector. Tolerances chosen for unit vectors are unchanged in meaning; tolerances applied to non-unit vectors now express an angle rather than a magnitude.
0.4.3 - 2026-06-12: double-precision magnitude for IsNormalized and HasMagnitudeApproximately
Patch release. Computes vector and quaternion magnitudes in double precision so a tight tolerance reflects the value's actual deviation from unit length rather than single-precision arithmetic noise. No public API change.
Fixed
IsNormalized(Vector3 and Quaternion) andHasMagnitudeApproximately(Vector3) now compute the magnitude in double precision. They previously calledSystem.Numerics.Vector3.Length()/Quaternion.Length(), which compute the dot product and square root infloat. The residual single-precision error could defeat a tight tolerance: a quaternion normalized to float precision could failIsNormalizedat a small tolerance, and at large coordinates the float square overflows to infinity, which makesHasMagnitudeApproximatelyunsatisfiable. The components now widen todoublebefore the sum of squares and the square root, the same widening already used forPositionDistanceand the0.4.1quaternion-angle fix. The comparison semantics and the meaning oftoleranceare unchanged.
0.4.2 - 2026-06-05: pose-tolerance migration and projection docs
Documentation and release-tooling patch. No code, public API, or behavior change; the 0.4.1 ApiCompat baseline surface is unchanged.
Changed
- README adds a "Migrating from a per-component quaternion tolerance" note under the pose section:
rotationToleranceDegreesis a geodesic angle in degrees, not a per-component quaternion delta, so a component epsilon must not be reused as the rotation tolerance. The note gives the rough angle-to-component mapping (a component toleranceepsiloncorresponds to about115*epsilondegrees) so a fresh degrees value can be chosen. - README adds a "Projecting a non-
System.Numericspose" example:IsPoseApproximatelyEqualTooperates onSystem.Numerics, so a pose arriving as another shape (for example a protobuf message with separate position and orientation fields) is projected toVector3/Quaternionfirst, with a short conversion snippet. - The packed package README cross-references the migration note from the
PoseAssertionsentry.
0.4.1 - 2026-06-03: rotation-angle self-comparison and degenerate-orientation fixes
Bug-fix release. No public API signature change.
Fixed
IsPoseApproximatelyEqualTo(and theMatrix4x4overloadIsRigidTransformApproximatelyEqualTo, and the underlyingMathTolerance.RotationAngleDegrees) reported a non-zero rotation angle for a pose compared with itself, failing on identical inputs. The geodesic angle was computed as2 * acos(|dot|), andacoshas infinite slope atdot = 1, so the residual float error of normalizing a non-unit quaternion (any quaternion whose magnitude is not exactly 1) was amplified into a spurious angle large enough to exceed a tight rotation tolerance. The metric is now the numerically-stabletheta = 2 * atan2(||imag(q1 * conj(q2))||, |real(q1 * conj(q2))|), which is well-conditioned across the whole 0-180 degree range, and both quaternions are normalized in double precision before measuring (a quaternion and any positive scalar multiple are the same rotation), so a quaternion compared with itself now measures exactly zero.IsPoseApproximatelyEqualnow handles a degenerate, non-normalizable orientation (squared magnitude underflows to zero, e.g.default(Quaternion)=(0, 0, 0, 0)), which has no defined geodesic angle. The orientation half falls back to a componentwise comparison against a tight fixed tolerance, independent of the caller's rotation tolerance: the rotation tolerance is a degrees value, and reusing it would be far too permissive (a tolerance of 1 or more would accept a zero quaternion as equal to a real rotation, whose nearest component delta is at most 1). So a default-pose round-trip ((0,0,0,0)vs(0,0,0,0)) passes, while(0,0,0,0)compared with a real rotation fails regardless of the angular tolerance, with the combined diagnostic naming the rotation half.
0.4.0 - 2026-06-02: pose and rigid-transform approximate-equality assertions
Feature release. Adds the pose assertion the package's PoseRenderer was waiting for: a pose is a position and an orientation together, so IsPoseApproximatelyEqualTo compares both halves in one call with separate position (length) and rotation (degrees) tolerances and one combined diagnostic that names which half missed.
Added
Assert.That((position, orientation)).IsPoseApproximatelyEqualTo((expectedPosition, expectedOrientation), positionTolerance, rotationToleranceDegrees)(TUnit adapter) compares a(Vector3, Quaternion)pose in one call. The two halves use separate tolerances because they carry different units: a Euclidean position distance and a geodesic rotation angle in degrees. On failure the combined message renders both poses and the measured position and rotation deltas, flagging which half exceeded its tolerance. The orientation comparison uses the SO(3) metric, so a quaternion and its negation are the same rotation. Source-generated via[GenerateAssertion].Assert.That(matrix).IsRigidTransformApproximatelyEqualTo(expectedMatrix, positionTolerance, rotationToleranceDegrees)(TUnit adapter) is theMatrix4x4overload (a pose is a rigid transform). Translation is read fromMatrix4x4.Translationand rotation viaQuaternion.CreateFromRotationMatrix; the overload assumes a rigid transform (orthonormal rotation, unit scale).MathTolerance.IsPoseApproximatelyEqual(...),RotationAngleDegrees(Quaternion, Quaternion), andPositionDistance(Vector3, Vector3)(framework-agnostic core) back the assertions: the geodesic rotation angle in degrees, the double-precision Euclidean position distance, and the combined pose predicate.
Changed
- TUnit dependency bumped
1.44.0->1.44.39(and the external-consumer smoke-test pin). 1.44.39 carries the[GenerateAssertion]source-generator fix for value-type optional parameters; no behavioral change for this package, taken for family lockstep.
0.3.0 - 2026-05-14: PoseRenderer + family dependency lockstep
Feature release. Lockstep version bump for both packages. Adds the first concrete renderer under the family-shared *.Render namespace convention and brings the dependency pins back into lockstep with the rest of the assertion family. No breaking changes; the public surface grows by one additive namespace.
Added
MathAssertions.Render.PoseRenderer, a pure static renderer that turns a 3D pose into deterministic, snapshot-friendly text.Render(Vector3, Quaternion, double)emits a two-line block: apos: (x, y, z) tol=tline and aquat: (x, y, z, w) tol=tline; theRender(Vector3, double)andRender(Quaternion, double)overloads emit the position or orientation line alone.- Self-contained and snapshot-framework-agnostic. The renderer takes no dependency on
SnapshotAssertions.TUnit; the canonical pairing is the two-lineAssert.That(PoseRenderer.Render(...)).MatchesSnapshot()composition. - Orientation rendered verbatim. The quaternion sign is not canonicalized, so a regression that flips a quaternion sign surfaces as a snapshot diff. Callers asserting rotational equivalence rather than component identity use
IsRotationallyEquivalentToinstead. - The tolerance is recorded, not applied. The renderer performs no comparison; it echoes the supplied tolerance into the output so a tolerance silently loosened during a refactor surfaces as a snapshot diff.
- Deterministic formatting. Components use the invariant-culture
F6fixed format, the tolerance the invariant-cultureGgeneral format. Lines are terminated with the literal LF byte, neverEnvironment.NewLine, so a baseline committed on one OS stays byte-stable for test runs on every other.
- Self-contained and snapshot-framework-agnostic. The renderer takes no dependency on
Changed
- Bumped
Microsoft.SourceLink.GitHub10.0.203->10.0.300. - New cookbook section "Pin a pose as a snapshot" in
README.md. Worked example pairingPoseRenderer.RenderwithMatchesSnapshot()fromSnapshotAssertions.TUnit.
0.2.0 - 2026-05-12: per-component delta diagnostics, axis-angle assertions, family lockstep
Feature release. Lockstep version bump for both packages.
Added
- Rich per-component failure-message rendering for the compound
IsApproximatelyEqualoverloads (Vector2,Vector3,Vector4,Quaternion,Matrix4x4,Plane,Complex,ReadOnlySpan<double>,ReadOnlySpan<float>) plusIsRotationallyEquivalentandIsGeometricallyEquivalent. Each renderer surfaces three sections under an expectation header:actual:echoing the value,delta:showing the absolute per-axis / per-cell / first-failing-element difference, andexceeded:naming the components whose delta crossed tolerance. The exceeded classification routes through the boolean predicate itself (MathTolerance.IsApproximatelyEqual), so equal-special-value pairs (NaN vs NaN, same-sign infinity vs same-sign infinity) are correctly treated as equal even though their absolute delta is NaN. Plane geometric equivalence additionally renders the delta against both the direct and the sign-flipped candidate representation. Numeric formatting isCultureInfo.InvariantCultureG-form throughout so the rendered text is stable across locales. The renderer type itself is internal (consumed via[InternalsVisibleTo]by the adapter assembly); failure-message text remains explicitly not part of the stable public surface. MathTolerance.HasAxisAngleApproximately(Quaternion, Vector3, double, double). Asserts a quaternion, viewed as a rotation in axis-angle form, lies within tolerance of the rotationexpectedAngleDegreesdegrees aroundexpectedAxis. Normalizes both inputs internally; rejects zero-length expected axis withArgumentException. Uses the rotational-equivalence formulation under the hood (|dot(unit_q, unit_expected)| >= 1 - tolerance) so every edge case is handled uniformly: the SO(3)qvs-qdouble cover; the 180-degree boundary where(axis, +180),(axis, -180), and(-axis, +/-180)all encode the same rotation; non-unit inputs. Reference: Hanson, Visualizing Quaternions, §4.6.QuaternionAssertions.HasAxisAngleApproximately. Fluent entry point delegating toMathTolerance.HasAxisAngleApproximately. Failure messages render the extracted axis, the extracted angle in degrees, and the delta angle (in the shorter-arc form modulo 360 degrees, so 359 vs 1 renders as 2 not 358).- Rich failure messages on every adapter
IsApproximatelyEqualTochain (Vector2/Vector3/Vector4/Quaternion/Matrix4x4/Plane/Complex/double[]/float[]) plusIsRotationallyEquivalentToandIsGeometricallyEquivalentTo. Generated extension methods now invoke the per-component renderer on mismatch instead of returning a static expectation-only template.
Changed
- Compound
IsApproximatelyEqualTosource-method return types changed frombooltoTUnit.Assertions.Core.AssertionResult. This applies to the static helpers inVectorAssertions,QuaternionAssertions,MatrixAssertions,PlaneAssertions,ComplexAssertions,ArrayAssertions, plusIsRotationallyEquivalentToandIsGeometricallyEquivalentTo. The change enables the rich per-component failure-message rendering; the corresponding generated TUnit chain extensions (Assert.That(x).IsApproximatelyEqualTo(...)) are unaffected at the chain-syntax level. Consumers calling the static helpers directly (rather than throughAssert.That) need to read.IsPassedoff the returnedAssertionResult. - Reframing carry-forward. Package descriptions, tags, and README taglines reframed from "tolerance-aware numeric assertions" / "math-assertion DSL" to "math assertion library" to match the actual surface (tolerance comparisons, sequences, statistics, linear algebra, number theory, 3D geometry). Affects
<Description>and<PackageTags>in both csprojs (so the nuget.org listing reflects the broader scope), the tagline in the rootREADME.md, and the taglines insrc/MathAssertions.TUnit/README.mdandsrc/MathAssertions/README.md. No API surface change here. - Bumped
TUnit/TUnit.Assertions/TUnit.Core1.43.11->1.44.0. - Cookbook entries on
IsApproximatelyEqualTo(component-wise) vsIsRotationallyEquivalentToforQuaternion(the SO(3)qvs-qdouble-cover) and vsIsGeometricallyEquivalentToforPlane(the(n, d)vs(-n, -d)sign flip), with worked examples in both READMEs. - Cookbook entry on asserting a quaternion is the zero-valued sentinel using
IsApproximatelyEqualTo(Quaternion.Zero, tolerance). - New
HasRoundtripIdentitycookbook entry in both READMEs documenting the existing framework-agnostic primitive with three worked examples (Sin / Asin, degree / radian, encode / decode delegate pair). - "NaN and infinity semantics" section in
src/MathAssertions.TUnit/README.mdmirroring the table already in the root README.
0.1.0 - 2026-05-09: seven-cluster tolerance-aware core and full TUnit adapter parity
The wider catalog. 0.1.0 expands the framework-agnostic core from a 3-method skeleton (scalar, Vector3) into a ~85-method tolerance-aware mathematical-assertion library across seven clusters, and brings the MathAssertions.TUnit adapter to parity so consumers can write await Assert.That(value).Method(...) for the entire core surface. Both packages remain AOT-compatible, trimmable, and reflection-free in the assertion path.
Added
MathTolerance tolerance primitives
- ULP-distance equality (
IsCloseInUlpsfordouble/float); positive and negative zero compare equal regardless of distance, opposite-sign operands never within finite ULPs, NaN handling matches the absolute-tolerance overloads. - Combined relative + absolute tolerance (
IsRelativelyAndAbsolutelyClose) per Knuth, TAOCP Vol. 2; absolute term is the floor near zero, relative term scales with magnitude. IsFinite(double)/IsFinite(float),IsNonNegativeFinite(double),IsProbability(double)([0, 1]),IsPercentage(double)([0, 100]) domain predicates.HasRoundtripIdentity(double, Func<double,double>, Func<double,double>, double): invertible-transformation check; double-only on purpose, consumers compose their own predicate for other types.
System.Numerics compounds
- Component-wise
MathTolerance.IsApproximatelyEqualforVector2,Vector4(alongside the existingVector3overload),Quaternion,Matrix4x4,Plane,Complex. Components widen todoublebefore comparison so a tightdoubletolerance is honored at full precision (M-1 fix from the 0.1.0 plan). IsRotationallyEquivalent(Quaternion, Quaternion, double): SO(3) double-cover view treatingqand-qas the same rotation. Normalizes inputs internally so non-unit operands produce the correct verdict (Hanson, Visualizing Quaternions, §4.6; M-2 fix).IsGeometricallyEquivalent(Plane, Plane, double): plane equivalence under the(n, d) ~ (-n, -d)sign flip (M-3 fix).IsApproximatelyEqual(ReadOnlySpan<double>, ...)and(ReadOnlySpan<float>, ...)for element-wise comparison; length mismatch returnsfalserather than throwing.- Generic
IsApproximatelyEqual<T>(ReadOnlyTensorSpan<T>, ReadOnlyTensorSpan<T>, T) where T : INumber<T>: element-wise tolerance comparison forSystem.Numerics.Tensors.ReadOnlyTensorSpan<T>. Shape mismatch returnsfalse. NaN handling matches the floating-point scalar overloads. Iteration uses the tensor span's enumerator so strided shapes from slicing work correctly.
Sequences over ReadOnlySpan<double>
- Monotonicity (strict and non-strict, increasing and decreasing),
IsSorted,IsBoundedwith NaN-aware element check, arithmetic and geometric progressions with tolerance,ConvergesTo(last value within tolerance of a limit),IsCauchyConvergent(single-step proxy of the Cauchy criterion), and genericHasLength<T>/HasMinLength<T>length predicates over anyReadOnlySpan<T>. The geometric-progression zero check uses a bit-magnitude mask so both+0and-0are caught without tripping the operator-==floating-point analyzer flag.
Statistics over ReadOnlySpan<double>
MeanAndVariance: numerically stable single-pass mean + unbiased sample variance via Welford's online algorithm (Knuth, TAOCP Vol. 2, §4.2.2).HasMeanApproximately,HasVarianceApproximately,HasStdDevApproximately,HasSumApproximately,HasMedianApproximately.HasPercentileApproximately: linear interpolation between adjacent ranks per the NIST/SEMATECH e-Handbook of Statistical Methods §1.3.5.6. Validates the percentile is in[0, 100]and not NaN.IsWithinSigmasOfMean(value, sample, sigmas)andAreAllWithinSigmasOfMean(values, sigmas). The latter computes mean and standard deviation once and reuses them across all element checks (O(N), not the O(N^2) shape that per-element delegation would produce).- Median uses
a/2 + b/2and percentile uses the convex-combination lerpa*(1-f) + b*fso very-large finite operands do not overflow to infinity. AreAllWithinSigmasOfMeanuses the!(<= )idiom so NaN-poisoned thresholds short-circuit to false rather than the>form's vacuous true.
LinearAlgebra invariants
Matrix4x4:IsSymmetric,IsOrthogonal(M*M^T ~= I),IsIdentity,HasDeterminantApproximately,HasTraceApproximately,IsInvertible.Vector3pair:AreOrthogonal,AreParallel(zero vector treated as parallel to every other vector),AreLinearlyIndependentoverReadOnlySpan<Vector3>(triple-product test for sets of up to three vectors inR^3).
NumberTheory exact integer predicates over long
IsDivisibleBy,IsPrime(wheel-of-six trial division with overflow-safe loop boundi <= value / i),AreCoprime,GreatestCommonDivisor,LeastCommonMultiple(validatedlong.MinValuerejection at entry; multiply runs inchecked()so overflow throwsOverflowExceptionrather than wrapping),IsPowerOf,IsPerfectSquare(with a successor check skipped when(sqrt+1)^2would overflow),IsCongruent(canonical-residue form so signed-range straddling inputs do not overflow the subtraction).
Geometry3D 3D primitives + property predicates + containment + distance + intersection + pointcloud
- Eight primitive
record structtypes underMathAssertions.Geometry3D:Sphere(withVolume/SurfaceArea),AxisAlignedBox(withCenter,Size,HalfExtents,Volume),OrientedBox,Ray3D,LineSegment3D(withDirection,Length),Triangle3D(withNormal(right-hand-rule unit normal; NaN for degenerate triangles, whereIsDegenerateis the precondition guard),Centroid,Area),Capsule,Cylinder(distinct fromCapsule; flat caps). Geometry3D.Properties:IsDegenerate(Triangle3D),IsCollinear(span)andAreCoplanar(span)overReadOnlySpan<Vector3>. BothIsCollinearandAreCoplanarscan for the first non-trivial direction or a non-collinear triple respectively, so leading-coincident points do not lock in a degenerate zero direction.IsCollinear's perpendicular-distance test divides by the direction magnitude so the verdict is dimensionally invariant under baseline-length scaling.Geometry3D.Containment:Contains(box, point/AABB/sphere),Contains(sphere, point),Contains(OBB, point)(via inverse-orientation rotation into the box's local frame),ConvexHullContains(hullVertices, point, tolerance)over flat triples-of-three CCW-outward face triangles. The face-plane half-space test divides the signed dot value by the normal length so the verdict is invariant to face-area scaling.Geometry3D.Distance:point.DistanceFrom(plane/segment/triangle)per Ericson, Real-Time Collision Detection, §§5.1.2 and 5.1.5. Degenerate segments where both endpoints coincide are handled via a bit-magnitude zero check on|ab|^2and fall through to a point-to-point distance.Geometry3D.Intersection:Intersects(Sphere, Sphere),Intersects(AABB, AABB), ray-plane / ray-sphere / ray-triangle (Moller-Trumbore) / ray-AABB (slab test) per Akenine-Moller, Haines, Hoffman, Real-Time Rendering 4th ed., §§22.6, 22.7, 22.8. Each ray-intersection has both a boolean shorthand and aout float toverload exposing the hit-distance parameter.Geometry3D.Pointcloud:cloud.IsBoundedBy(AxisAlignedBox/Sphere),cloud.HasCentroidAt(expected, tolerance),cloud.IsApproximatelyOnPlane(plane, maxResidual),cloud.IsApproximatelyOnSphere(center, radius, maxResidual). Empty cloud is vacuously bounded / vacuously on-surface.
Cross-cutting
- All tolerance- and sigma-taking methods validate the bound up front so an invalid input throws even when an early-return short-circuit would otherwise skip the inner
MathTolerancecall. Family-wide validation-order pattern acrossSequences.ConvergesTo,Sequences.IsBounded,Statistics.HasMeanApproximately,LinearAlgebra.*,NumberTheory.*(where applicable), and theGeometry3Dsurface.
TUnit adapter
The adapter exposes the entire 0.1.0 core surface as fluent extensions; consumers write await Assert.That(value).Method(...) and TUnit's [GenerateAssertion] source generator emits the assertion-builder plumbing. New files:
ScalarAssertions:IsApproximatelyEqualTo/IsCloseInUlpsTo/IsRelativelyAndAbsolutelyCloseTo/IsNonNegativeFinite/IsProbability/IsPercentage/HasRoundtripIdentityfordoubleandfloat. (IsFiniteis left to TUnit's built-inDoubleAssertionExtensions.IsFinite/SingleAssertionExtensions.IsFinite.)VectorAssertions: extended to coverVector2/Vector4(alongside the 0.0.1Vector3overload) plusVector3.HasMagnitudeApproximately/IsNormalized.QuaternionAssertions:IsApproximatelyEqualTo,IsRotationallyEquivalentTo,IsIdentity,IsNormalized.MatrixAssertions(coversMatrix4x4):IsApproximatelyEqualToplus the fullLinearAlgebramatrix-invariant surface.PlaneAssertions: component-wiseIsApproximatelyEqualToandIsGeometricallyEquivalentTo.ComplexAssertions:IsApproximatelyEqualTo.ArrayAssertions: fluentdouble[]andfloat[]element-wise comparison with the M-4 null-array guard. Operates on arrays rather thanReadOnlySpan<T>because TUnit's assertion-builder cannot capture ref-struct values across anawait.SequencesAssertions,StatisticsAssertions: array-based wrappers for theSequencesandStatisticssurfaces. GenericT[]length predicates onSequencesAssertions.LinearAlgebraAssertions: Vector3 pairIsOrthogonalTo/IsParallelToand Vector3-arrayAreLinearlyIndependent.NumberTheoryAssertions: predicate methods onlong.Geometry3DAssertions: triangle / point-set property predicates, containment, predicate-styleHasDistanceFrom*, intersection, and pointcloud aggregates over theGeometry3Dprimitives.
Changed
MathAssertionscore depends onSystem.Numerics.Tensors10.0.7 (BCL, MIT, no transitive deps on net8/9/10) for the newReadOnlyTensorSpan<T>overload. Both packages remain AOT-compatible, trimmable, reflection-free.
0.0.1 - 2026-05-08: skeleton release establishing repository, package identifiers, and quality bar
First public release. Two packages ship together: MathAssertions (framework-agnostic core, BCL only) and MathAssertions.TUnit (TUnit fluent adapter). .NET 10, AOT-compatible, trimmable, no runtime reflection in the assertion path.
The 0.0.1 scope is intentionally narrow. The release exists to establish the repository, claim the MathAssertions and MathAssertions.TUnit package identifiers on nuget.org, and lock the API style and quality bar before the wider catalog ships at 0.1.0. Consumers needing the full v0.1.0 surface can install 0.0.1 to lock the dependency relationship and watch the CHANGELOG.
Added
MathTolerance.IsApproximatelyEqual(double, double, double): NaN-aware, infinity-aware tolerance comparison. Both NaN equal under tolerance; same-sign infinity equal; opposite-sign infinity not equal; finite values useMath.Abs(a - b) <= tolerance. Mirrors TUnit'sIsCloseToprimitive semantics.MathTolerance.IsApproximatelyEqual(float, float, float): same semantics, single-precision signature.MathTolerance.IsApproximatelyEqual(Vector3, Vector3, double): component-wise tolerance comparison. Component values widen todoublebefore the per-axis comparison so adoubletolerance is honored at full precision; casting the tolerance down tofloatwould discard up to 22 bits of mantissa for tight tolerances such as1e-9. The precision-preserving cast is pinned by tests.- All helpers validate
tolerance(rejects NaN and negative values viaArgumentOutOfRangeException). Vector3.IsApproximatelyEqualTo(expected, tolerance): fluent component-wise assertion entry point, generated via TUnit's[GenerateAssertion]. CallsMathTolerance.IsApproximatelyEqual(Vector3, Vector3, double)internally.