├── src └── FixedMathSharp │ ├── .gitignore │ ├── Bounds │ ├── IBoundExtensions.cs │ ├── IBound.cs │ ├── BoundingSphere.cs │ └── BoundingArea.cs │ ├── Numerics │ ├── Extensions │ │ ├── Fixed4x4.Extensions.cs │ │ ├── FixedQuaternion.Extensions.cs │ │ ├── Fixed3x3.Extensions.cs │ │ ├── Vector2d.Extensions.cs │ │ ├── Vector3d.Extensions.cs │ │ └── Fixed64.Extensions.cs │ ├── FixedCurveKey.cs │ ├── FixedCurve.cs │ └── FixedRange.cs │ ├── Utility │ ├── ThreadLocalRandom.cs │ └── DeterministicRandom.cs │ └── FixedMathSharp.csproj ├── tests └── FixedMathSharp.Tests │ ├── .gitignore │ ├── FixedMathSharp.Tests.csproj │ ├── Support │ └── FixedMathTestHelper.cs │ ├── FixedCurveTests.cs │ ├── Fixed64.Tests.cs │ ├── Bounds │ ├── BoundingArea.Tests.cs │ ├── BoundingBox.Tests.cs │ └── BoundingSphere.Tests.cs │ ├── FixedRange.Tests.cs │ ├── DeterministicRandom.Tests.cs │ ├── Vector2d.Tests.cs │ ├── Fixed3x3.Tests.cs │ └── FixedMath.Tests.cs ├── .gitignore ├── icon.png ├── .assets ├── icon_128x128.png ├── icon_512x512.png └── scripts │ ├── set-version-and-build.ps1 │ └── utilities.ps1 ├── .gitattributes ├── LICENSE.md ├── FixedMathSharp.sln ├── CONTRIBUTING.md ├── .github └── workflows │ └── dotnet.yml └── README.md /src/FixedMathSharp/.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | obj/ 3 | -------------------------------------------------------------------------------- /tests/FixedMathSharp.Tests/.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | obj/ 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vs/ 2 | src/FixedMathSharp/FixedMathSharp.csproj.user 3 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrdav30/FixedMathSharp/HEAD/icon.png -------------------------------------------------------------------------------- /.assets/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrdav30/FixedMathSharp/HEAD/.assets/icon_128x128.png -------------------------------------------------------------------------------- /.assets/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrdav30/FixedMathSharp/HEAD/.assets/icon_512x512.png -------------------------------------------------------------------------------- /src/FixedMathSharp/Bounds/IBoundExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace FixedMathSharp 4 | { 5 | internal static class IBoundExtensions 6 | { 7 | /// 8 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 9 | public static Vector3d ProjectPointWithinBounds(this IBound bounds, Vector3d point) 10 | { 11 | return new Vector3d( 12 | FixedMath.Clamp(point.x, bounds.Min.x, bounds.Max.x), 13 | FixedMath.Clamp(point.y, bounds.Min.y, bounds.Max.y), 14 | FixedMath.Clamp(point.z, bounds.Min.z, bounds.Max.z) 15 | ); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto eol=lf 3 | 4 | # Declare files that will always have LF line endings on checkout. 5 | *.sh text eol=lf 6 | 7 | # Don't check these into the repo as LF to work around TeamCity bug 8 | *.xml -text 9 | *.targets -text 10 | 11 | # Custom for Visual Studio 12 | *.cs diff=csharp 13 | *.sln 14 | *.csproj 15 | *.vbproj 16 | *.fsproj 17 | *.dbproj 18 | 19 | # Denote all files that are truly binary and should not be modified. 20 | *.dll binary 21 | *.exe binary 22 | *.png binary 23 | *.ico binary 24 | *.snk binary 25 | *.pdb binary 26 | *.svg binary 27 | 28 | # Don't check for trailing whitespace at end of lines in the doc pages 29 | *.md -whitespace=blank-at-eol -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 mrdav30 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/FixedMathSharp/Bounds/IBound.cs: -------------------------------------------------------------------------------- 1 | namespace FixedMathSharp 2 | { 3 | public interface IBound 4 | { 5 | /// 6 | /// The minimum bounds of the IBound. 7 | /// 8 | Vector3d Min { get; } 9 | 10 | /// 11 | /// The maximum bounds of the IBound. 12 | /// 13 | Vector3d Max { get; } 14 | 15 | /// 16 | /// Checks if a point is inside the IBound. 17 | /// 18 | /// The point to check. 19 | /// True if the point is inside the IBound, otherwise false. 20 | bool Contains(Vector3d point); 21 | 22 | /// 23 | /// Checks if the IBound intersects with another IBound. 24 | /// 25 | /// The other IBound to check for intersection. 26 | /// True if the IBounds intersect, otherwise false. 27 | bool Intersects(IBound other); 28 | 29 | /// 30 | /// Projects a point onto the IBound. If the point is outside the IBound, it returns the closest point on the surface. 31 | /// 32 | Vector3d ProjectPoint(Vector3d point); 33 | } 34 | } -------------------------------------------------------------------------------- /.assets/scripts/set-version-and-build.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | [string]$BuildType = "Release" 3 | ) 4 | 5 | # Import shared functions 6 | Set-Location (Split-Path $MyInvocation.MyCommand.Path) 7 | . .\utilities.ps1 8 | 9 | # Locate solution directory and switch to it 10 | $solutionDir = Get-SolutionDirectory 11 | Set-Location $solutionDir 12 | 13 | # Ensure GitVersion environment variables are set 14 | Ensure-GitVersion-Environment 15 | 16 | # Build the project with the version information applied 17 | Build-Project -Configuration $BuildType 18 | 19 | $solutionName = Split-Path $solutionDir -Leaf 20 | 21 | # Output directory 22 | $releaseDir = Join-Path $solutionDir "src\$solutionName\bin\Release" 23 | 24 | # Ensure release directory exists 25 | if (Test-Path $releaseDir) { 26 | Get-ChildItem -Path $releaseDir -Directory | ForEach-Object { 27 | $targetDir = $_.FullName 28 | $frameworkName = $_.Name 29 | 30 | # Construct final archive name 31 | $zipFileName = "${solutionName}-v$($Env:GitVersion_FullSemVer)-${frameworkName}-release.zip" 32 | $zipPath = Join-Path $releaseDir $zipFileName 33 | 34 | Write-Host "Creating archive: $zipPath" 35 | 36 | if (Test-Path $zipPath) { 37 | Remove-Item $zipPath -Force 38 | } 39 | 40 | Compress-Archive -Path "$targetDir\*" -DestinationPath $zipPath -Force 41 | 42 | if (Test-Path $zipPath) { 43 | Write-Host "Archive created for $frameworkName" 44 | } else { 45 | Write-Warning "Failed to create archive for $frameworkName" 46 | } 47 | } 48 | } else { 49 | Write-Warning "Release directory not found: $releaseDir" 50 | } -------------------------------------------------------------------------------- /src/FixedMathSharp/Numerics/Extensions/Fixed4x4.Extensions.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace FixedMathSharp 4 | { 5 | public static class Fixed4x4Extensions 6 | { 7 | #region Extraction, and Setters 8 | 9 | /// 10 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 11 | public static Vector3d ExtractLossyScale(this Fixed4x4 matrix) 12 | { 13 | return Fixed4x4.ExtractLossyScale(matrix); 14 | } 15 | 16 | /// 17 | public static Fixed4x4 SetGlobalScale(this ref Fixed4x4 matrix, Vector3d globalScale) 18 | { 19 | return matrix = Fixed4x4.SetGlobalScale(matrix, globalScale); 20 | } 21 | 22 | /// 23 | public static Fixed4x4 SetTranslation(this ref Fixed4x4 matrix, Vector3d position) 24 | { 25 | return matrix = Fixed4x4.SetTranslation(matrix, position); 26 | } 27 | 28 | /// 29 | public static Fixed4x4 SetRotation(this ref Fixed4x4 matrix, FixedQuaternion rotation) 30 | { 31 | return matrix = Fixed4x4.SetRotation(matrix, rotation); 32 | } 33 | 34 | /// 35 | public static Vector3d TransformPoint(this Fixed4x4 matrix, Vector3d point) 36 | { 37 | return Fixed4x4.TransformPoint(matrix, point); 38 | } 39 | 40 | /// 41 | public static Vector3d InverseTransformPoint(this Fixed4x4 matrix, Vector3d point) 42 | { 43 | return Fixed4x4.InverseTransformPoint(matrix, point); 44 | } 45 | 46 | #endregion 47 | } 48 | } -------------------------------------------------------------------------------- /tests/FixedMathSharp.Tests/FixedMathSharp.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9.0 4 | net48;net8 5 | disable 6 | enable 7 | false 8 | true 9 | Debug;Release 10 | 11 | 12 | 13 | True 14 | compile; build 15 | all 16 | 17 | 18 | 19 | all 20 | runtime; build; native; contentfiles; analyzers; buildtransitive 21 | 22 | 23 | 24 | 25 | all 26 | runtime; build; native; contentfiles; analyzers; buildtransitive 27 | 28 | 29 | runtime; build; native; contentfiles; analyzers; buildtransitive 30 | all 31 | 32 | 33 | runtime; build; native; contentfiles; analyzers; buildtransitive 34 | all 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /FixedMathSharp.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.5.33516.290 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FixedMathSharp", "src\FixedMathSharp\FixedMathSharp.csproj", "{61834921-141D-4BD5-9E75-31188DF32E93}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FixedMathSharp.Tests", "tests\FixedMathSharp.Tests\FixedMathSharp.Tests.csproj", "{86CD72E6-2A40-494C-9D9B-A38EF90A12A8}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{882324AE-67F3-4F5B-8485-6EF324A15CBF}" 11 | ProjectSection(SolutionItems) = preProject 12 | CONTRIBUTING.md = CONTRIBUTING.md 13 | LICENSE.md = LICENSE.md 14 | README.md = README.md 15 | EndProjectSection 16 | EndProject 17 | Global 18 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 19 | Debug|Any CPU = Debug|Any CPU 20 | Release|Any CPU = Release|Any CPU 21 | EndGlobalSection 22 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 23 | {61834921-141D-4BD5-9E75-31188DF32E93}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {61834921-141D-4BD5-9E75-31188DF32E93}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {61834921-141D-4BD5-9E75-31188DF32E93}.Release|Any CPU.ActiveCfg = Release|Any CPU 26 | {61834921-141D-4BD5-9E75-31188DF32E93}.Release|Any CPU.Build.0 = Release|Any CPU 27 | {86CD72E6-2A40-494C-9D9B-A38EF90A12A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 28 | {86CD72E6-2A40-494C-9D9B-A38EF90A12A8}.Debug|Any CPU.Build.0 = Debug|Any CPU 29 | {86CD72E6-2A40-494C-9D9B-A38EF90A12A8}.Release|Any CPU.ActiveCfg = Release|Any CPU 30 | {86CD72E6-2A40-494C-9D9B-A38EF90A12A8}.Release|Any CPU.Build.0 = Release|Any CPU 31 | EndGlobalSection 32 | GlobalSection(SolutionProperties) = preSolution 33 | HideSolutionNode = FALSE 34 | EndGlobalSection 35 | GlobalSection(ExtensibilityGlobals) = postSolution 36 | SolutionGuid = {DBAC9EE5-C388-434D-8B2C-1816DF31554F} 37 | EndGlobalSection 38 | EndGlobal 39 | -------------------------------------------------------------------------------- /tests/FixedMathSharp.Tests/Support/FixedMathTestHelper.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace FixedMathSharp.Tests 4 | { 5 | internal static class FixedMathTestHelper 6 | { 7 | private static readonly Fixed64 RelativeTolerance = new Fixed64(0.0001); // 0.01% 8 | 9 | /// 10 | /// Asserts that the difference between the expected and actual values is within the specified relative tolerance. 11 | /// 12 | /// The expected value. 13 | /// The actual value. 14 | /// The relative tolerance to apply. 15 | /// Optional message for assertion failures. 16 | public static void AssertWithinRelativeTolerance( 17 | Fixed64 expected, 18 | Fixed64 actual, 19 | Fixed64 tolerance = default, 20 | string message = "") 21 | { 22 | if (tolerance == default) 23 | tolerance = RelativeTolerance; 24 | 25 | var difference = (actual - expected).Abs(); 26 | var allowedError = expected.Abs() * tolerance; 27 | 28 | Assert.True(difference <= allowedError, 29 | string.IsNullOrWhiteSpace(message) 30 | ? $"Relative error {difference} exceeds tolerance of {allowedError} for expected value {expected}." 31 | : message); 32 | } 33 | 34 | /// 35 | /// Asserts that a given value falls within a specified range [min, max]. 36 | /// If the value is outside the range, the test fails with the provided error message. 37 | /// 38 | /// The value to check. 39 | /// The minimum bound of the range. 40 | /// The maximum bound of the range. 41 | /// 42 | /// An optional message to display if the assertion fails. 43 | /// 44 | public static void AssertWithinRange( 45 | Fixed64 value, 46 | Fixed64 min, 47 | Fixed64 max, 48 | string message = "") 49 | { 50 | Assert.True(value >= min && value <= max, 51 | string.IsNullOrWhiteSpace(message) 52 | ? $"Value {value} is not within the range [{min}, {max}]." 53 | : message); 54 | } 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/FixedMathSharp/Numerics/Extensions/FixedQuaternion.Extensions.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace FixedMathSharp 4 | { 5 | public static partial class FixedQuaternionExtensions 6 | { 7 | /// 8 | public static Vector3d ToAngularVelocity( 9 | this FixedQuaternion currentRotation, 10 | FixedQuaternion previousRotation, 11 | Fixed64 deltaTime) 12 | { 13 | return FixedQuaternion.ToAngularVelocity(currentRotation, previousRotation, deltaTime); 14 | } 15 | 16 | #region Equality 17 | 18 | /// 19 | /// Compares two quaternions for approximate equality, allowing a fixed absolute difference between components. 20 | /// 21 | /// The current quaternion. 22 | /// The quaternion to compare against. 23 | /// The allowed absolute difference between each component. 24 | /// True if the components are within the allowed difference, false otherwise. 25 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 26 | public static bool FuzzyEqualAbsolute(this FixedQuaternion q1, FixedQuaternion q2, Fixed64 allowedDifference) 27 | { 28 | return (q1.x - q2.x).Abs() <= allowedDifference && 29 | (q1.y - q2.y).Abs() <= allowedDifference && 30 | (q1.z - q2.z).Abs() <= allowedDifference && 31 | (q1.w - q2.w).Abs() <= allowedDifference; 32 | } 33 | 34 | /// 35 | /// Compares two quaternions for approximate equality, allowing a fractional percentage (defaults to ~1%) difference between components. 36 | /// 37 | /// The current quaternion. 38 | /// The quaternion to compare against. 39 | /// The allowed fractional difference (percentage) for each component. 40 | /// True if the components are within the allowed percentage difference, false otherwise. 41 | public static bool FuzzyEqual(this FixedQuaternion q1, FixedQuaternion q2, Fixed64? percentage = null) 42 | { 43 | Fixed64 p = percentage ?? Fixed64.Epsilon; 44 | return q1.x.FuzzyComponentEqual(q2.x, p) && 45 | q1.y.FuzzyComponentEqual(q2.y, p) && 46 | q1.z.FuzzyComponentEqual(q2.z, p) && 47 | q1.w.FuzzyComponentEqual(q2.w, p); 48 | } 49 | 50 | #endregion 51 | } 52 | } -------------------------------------------------------------------------------- /.assets/scripts/utilities.ps1: -------------------------------------------------------------------------------- 1 | function Get-SolutionDirectory { 2 | param ( 3 | [string]$StartPath = $(Get-Location), 4 | [string]$SolutionPath = "FixedMathSharp.sln" 5 | ) 6 | 7 | $currentPath = $StartPath 8 | while ($true) { 9 | if (Test-Path (Join-Path $currentPath $SolutionPath)) { 10 | return $currentPath 11 | } 12 | $parent = [System.IO.Directory]::GetParent($currentPath) 13 | if ($parent -eq $null) { break } 14 | $currentPath = $parent.FullName 15 | } 16 | throw "Solution directory not found." 17 | } 18 | 19 | function Ensure-GitVersion-Environment { 20 | # Ensure GitVersion is installed and available 21 | if (-not (Get-Command "dotnet-gitversion" -ErrorAction SilentlyContinue)) { 22 | Write-Host "GitVersion is not installed. Install it with:" 23 | Write-Host "dotnet tool install -g GitVersion.Tool" 24 | exit 1 25 | } 26 | 27 | Write-Host "Fetching version information using GitVersion..." 28 | 29 | # Capture GitVersion output as JSON and convert it to PowerShell objects 30 | $gitVersionOutput = dotnet-gitversion -output json | ConvertFrom-Json 31 | 32 | if ($null -eq $gitVersionOutput) { 33 | Write-Host "ERROR: Failed to get version information from GitVersion." -ForegroundColor Red 34 | exit 1 35 | } 36 | 37 | # Extract key version properties 38 | $semVer = $gitVersionOutput.MajorMinorPatch 39 | $assemblySemVer = $gitVersionOutput.AssemblySemVer 40 | $assemblySemFileVer = $gitVersionOutput.AssemblySemFileVer 41 | $infoVersion = $gitVersionOutput.InformationalVersion 42 | 43 | # Set environment variables for the build process 44 | [System.Environment]::SetEnvironmentVariable('GitVersion_FullSemVer', $semVer, 'Process') 45 | [System.Environment]::SetEnvironmentVariable('GitVersion_AssemblySemVer', $assemblySemVer, 'Process') 46 | [System.Environment]::SetEnvironmentVariable('GitVersion_AssemblySemFileVer', $assemblySemFileVer, 'Process') 47 | [System.Environment]::SetEnvironmentVariable('GitVersion_InformationalVersion', $infoVersion, 'Process') 48 | 49 | Write-Host "Environment variables set:" 50 | Write-Host " GitVersion_FullSemVer = $semVer" 51 | Write-Host " GitVersion_AssemblySemVer = $assemblySemVer" 52 | Write-Host " GitVersion_AssemblySemFileVer = $assemblySemFileVer" 53 | Write-Host " GitVersion_InformationalVersion = $infoVersion" 54 | } 55 | 56 | function Build-Project { 57 | param ( 58 | [string]$SolutionPath = "FixedMathSharp.sln", 59 | [string]$Configuration = "Release" 60 | ) 61 | 62 | Write-Host "Building $SolutionPath in $Configuration mode..." 63 | # Clean and build the project with the selected configuration 64 | dotnet clean 65 | dotnet build $SolutionPath -c $Configuration 66 | 67 | if ($LASTEXITCODE -ne 0) { 68 | Write-Host "Build failed." -ForegroundColor Red 69 | exit 1 70 | } 71 | 72 | Write-Host "Build succeeded!" -ForegroundColor Green 73 | } 74 | -------------------------------------------------------------------------------- /src/FixedMathSharp/Utility/ThreadLocalRandom.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | 4 | namespace FixedMathSharp.Utility 5 | { 6 | /// 7 | /// Deterministic per-thread RNG facade. 8 | /// 9 | [Obsolete("ThreadLocalRandom is deprecated. Use DeterministicRandom or DeterministicRandom.FromWorldFeature(...) for deterministic streams.", false)] 10 | public static class ThreadLocalRandom 11 | { 12 | private static ulong _rootSeed = 0; 13 | private static Func _threadIndexProvider = null!; 14 | private static ThreadLocal _threadRng = null!; 15 | 16 | /// 17 | /// Initialize global deterministic seeding. 18 | /// Provide a stable threadIndex (0..T-1) for each thread. 19 | /// 20 | public static void Initialize(ulong rootSeed, Func threadIndexProvider) 21 | { 22 | _rootSeed = rootSeed; 23 | _threadIndexProvider = threadIndexProvider ?? throw new ArgumentNullException(nameof(threadIndexProvider)); 24 | 25 | _threadRng = new ThreadLocal(() => 26 | { 27 | int idx = _threadIndexProvider(); 28 | // Derive a unique stream per thread deterministically from rootSeed + idx. 29 | return DeterministicRandom.FromWorldFeature(_rootSeed, (ulong)idx); 30 | }); 31 | } 32 | 33 | /// 34 | /// Create a new independent RNG from a specific seed (does not affect thread Instance). 35 | /// 36 | public static DeterministicRandom NewRandom(ulong seed) => new(seed); 37 | 38 | /// 39 | /// Per-thread RNG instance (requires Initialize to be called first). 40 | /// 41 | public static DeterministicRandom Instance 42 | { 43 | get 44 | { 45 | if (_threadRng == null) 46 | throw new InvalidOperationException("ThreadLocalRandom.Initialize(rootSeed, threadIndexProvider) must be called first."); 47 | return _threadRng.Value; 48 | } 49 | } 50 | 51 | #region Convenience mirrors 52 | 53 | public static int Next() => Instance.Next(); 54 | public static int Next(int maxExclusive) => Instance.Next(maxExclusive); 55 | public static int Next(int minInclusive, int maxExclusive) => Instance.Next(minInclusive, maxExclusive); 56 | public static double NextDouble() => Instance.NextDouble(); 57 | public static double NextDouble(double min, double max) => Instance.NextDouble() * (max - min) + min; 58 | 59 | public static void NextBytes(byte[] buffer) 60 | { 61 | if (buffer == null) throw new ArgumentNullException(nameof(buffer)); 62 | Instance.NextBytes(buffer); 63 | } 64 | 65 | public static Fixed64 NextFixed6401() => Instance.NextFixed6401(); 66 | public static Fixed64 NextFixed64(Fixed64 maxExclusive) => Instance.NextFixed64(maxExclusive); 67 | public static Fixed64 NextFixed64(Fixed64 minInclusive, Fixed64 maxExclusive) => Instance.NextFixed64(minInclusive, maxExclusive); 68 | 69 | #endregion 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/FixedMathSharp/Numerics/FixedCurveKey.cs: -------------------------------------------------------------------------------- 1 | using MessagePack; 2 | using System; 3 | 4 | #if NET8_0_OR_GREATER 5 | using System.Text.Json.Serialization; 6 | #endif 7 | 8 | namespace FixedMathSharp 9 | { 10 | /// 11 | /// Represents a keyframe in a , defining a value at a specific time. 12 | /// 13 | [Serializable] 14 | [MessagePackObject] 15 | public struct FixedCurveKey : IEquatable 16 | { 17 | /// The time at which this keyframe occurs. 18 | [Key(0)] 19 | public Fixed64 Time; 20 | 21 | /// The value of the curve at this keyframe. 22 | [Key(1)] 23 | public Fixed64 Value; 24 | 25 | /// The incoming tangent for cubic interpolation. 26 | [Key(2)] 27 | public Fixed64 InTangent; 28 | 29 | /// The outgoing tangent for cubic interpolation. 30 | [Key(3)] 31 | public Fixed64 OutTangent; 32 | 33 | /// 34 | /// Creates a keyframe with a specified time and value. 35 | /// 36 | public FixedCurveKey(double time, double value) 37 | : this(new Fixed64(time), new Fixed64(value)) { } 38 | 39 | /// 40 | /// Creates a keyframe with optional tangents for cubic interpolation. 41 | /// 42 | public FixedCurveKey(double time, double value, double inTangent, double outTangent) 43 | : this(new Fixed64(time), new Fixed64(value), new Fixed64(inTangent), new Fixed64(outTangent)) { } 44 | 45 | /// 46 | /// Creates a keyframe with a specified time and value. 47 | /// 48 | public FixedCurveKey(Fixed64 time, Fixed64 value) 49 | : this(time, value, Fixed64.Zero, Fixed64.Zero) { } 50 | 51 | /// 52 | /// Creates a keyframe with optional tangents for cubic interpolation. 53 | /// 54 | #if NET8_0_OR_GREATER 55 | [JsonConstructor] 56 | #endif 57 | public FixedCurveKey(Fixed64 time, Fixed64 value, Fixed64 inTangent, Fixed64 outTangent) 58 | { 59 | Time = time; 60 | Value = value; 61 | InTangent = inTangent; 62 | OutTangent = outTangent; 63 | } 64 | 65 | public bool Equals(FixedCurveKey other) 66 | { 67 | return Time == other.Time && 68 | Value == other.Value && 69 | InTangent == other.InTangent && 70 | OutTangent == other.OutTangent; 71 | } 72 | 73 | public override bool Equals(object? obj) => obj is FixedCurveKey other && Equals(other); 74 | 75 | public override int GetHashCode() 76 | { 77 | unchecked 78 | { 79 | int hash = Time.GetHashCode(); 80 | hash = (hash * 31) ^ Value.GetHashCode(); 81 | hash = (hash * 31) ^ InTangent.GetHashCode(); 82 | hash = (hash * 31) ^ OutTangent.GetHashCode(); 83 | return hash; 84 | } 85 | } 86 | 87 | public static bool operator ==(FixedCurveKey left, FixedCurveKey right) => left.Equals(right); 88 | 89 | public static bool operator !=(FixedCurveKey left, FixedCurveKey right) => !(left == right); 90 | } 91 | } -------------------------------------------------------------------------------- /src/FixedMathSharp/Numerics/Extensions/Fixed3x3.Extensions.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace FixedMathSharp 4 | { 5 | public static class Fixed3x3Extensions 6 | { 7 | #region Transformations 8 | 9 | /// 10 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 11 | public static Vector3d ExtractScale(this Fixed3x3 matrix) 12 | { 13 | return Fixed3x3.ExtractScale(matrix); 14 | } 15 | 16 | /// 17 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 18 | public static Fixed3x3 SetScale(this ref Fixed3x3 matrix, Vector3d localScale) 19 | { 20 | return matrix = Fixed3x3.SetScale(matrix, localScale); 21 | } 22 | 23 | /// 24 | public static Fixed3x3 SetGlobalScale(this ref Fixed3x3 matrix, Vector3d globalScale) 25 | { 26 | return matrix = Fixed3x3.SetGlobalScale(matrix, globalScale); 27 | } 28 | 29 | #endregion 30 | 31 | #region Equality 32 | 33 | /// 34 | /// Compares two Fixed3x3 for approximate equality, allowing a fixed absolute difference between components. 35 | /// 36 | /// The current Fixed3x3. 37 | /// The Fixed3x3 to compare against. 38 | /// The allowed absolute difference between each component. 39 | /// True if the components are within the allowed difference, false otherwise. 40 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 41 | public static bool FuzzyEqualAbsolute(this Fixed3x3 f1, Fixed3x3 f2, Fixed64 allowedDifference) 42 | { 43 | return (f1.m00 - f2.m00).Abs() <= allowedDifference && 44 | (f1.m01 - f2.m01).Abs() <= allowedDifference && 45 | (f1.m02 - f2.m02).Abs() <= allowedDifference && 46 | (f1.m10 - f2.m10).Abs() <= allowedDifference && 47 | (f1.m11 - f2.m11).Abs() <= allowedDifference && 48 | (f1.m12 - f2.m12).Abs() <= allowedDifference && 49 | (f1.m20 - f2.m20).Abs() <= allowedDifference && 50 | (f1.m21 - f2.m21).Abs() <= allowedDifference && 51 | (f1.m22 - f2.m22).Abs() <= allowedDifference; 52 | } 53 | 54 | /// 55 | /// Compares two Fixed3x3 for approximate equality, allowing a fractional percentage (defaults to ~1%) difference between components. 56 | /// 57 | /// The current Fixed3x3. 58 | /// The Fixed3x3 to compare against. 59 | /// The allowed fractional difference (percentage) for each component. 60 | /// True if the components are within the allowed percentage difference, false otherwise. 61 | public static bool FuzzyEqual(this Fixed3x3 f1, Fixed3x3 f2, Fixed64? percentage = null) 62 | { 63 | Fixed64 p = percentage ?? Fixed64.Epsilon; 64 | return f1.m00.FuzzyComponentEqual(f2.m00, p) && 65 | f1.m01.FuzzyComponentEqual(f2.m01, p) && 66 | f1.m02.FuzzyComponentEqual(f2.m02, p) && 67 | f1.m10.FuzzyComponentEqual(f2.m10, p) && 68 | f1.m11.FuzzyComponentEqual(f2.m11, p) && 69 | f1.m12.FuzzyComponentEqual(f2.m12, p) && 70 | f1.m20.FuzzyComponentEqual(f2.m20, p) && 71 | f1.m21.FuzzyComponentEqual(f2.m21, p) && 72 | f1.m22.FuzzyComponentEqual(f2.m22, p); 73 | } 74 | 75 | #endregion 76 | } 77 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via issue, 4 | email, or any other method with the owners of this repository before making a change. 5 | 6 | Please note we have a code of conduct, please follow it in all your interactions with the project. 7 | 8 | ## Pull Request Process 9 | 10 | 1. Ensure any install or build dependencies are removed before the end of the layer when doing a 11 | build. 12 | 2. Update the README.md with details of changes to the interface, this includes new environment 13 | variables, exposed ports, useful file locations and container parameters. 14 | 3. Increase the version numbers in any examples files and the README.md to the new version that this 15 | Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/). 16 | 4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you 17 | do not have permission to do that, you may request the second reviewer to merge it for you. 18 | 19 | ## Code of Conduct 20 | 21 | ### Our Pledge 22 | 23 | In the interest of fostering an open and welcoming environment, we as 24 | contributors and maintainers pledge to making participation in our project and 25 | our community a harassment-free experience for everyone, regardless of age, body 26 | size, disability, ethnicity, gender identity and expression, level of experience, 27 | nationality, personal appearance, race, religion, or sexual identity and 28 | orientation. 29 | 30 | ### Our Standards 31 | 32 | Examples of behavior that contributes to creating a positive environment 33 | include: 34 | 35 | * Using welcoming and inclusive language 36 | * Being respectful of differing viewpoints and experiences 37 | * Gracefully accepting constructive criticism 38 | * Focusing on what is best for the community 39 | * Showing empathy towards other community members 40 | 41 | Examples of unacceptable behavior by participants include: 42 | 43 | * The use of sexualized language or imagery and unwelcome sexual attention or 44 | advances 45 | * Trolling, insulting/derogatory comments, and personal or political attacks 46 | * Public or private harassment 47 | * Publishing others' private information, such as a physical or electronic 48 | address, without explicit permission 49 | * Other conduct which could reasonably be considered inappropriate in a 50 | professional setting 51 | 52 | ### Our Responsibilities 53 | 54 | Project maintainers are responsible for clarifying the standards of acceptable 55 | behavior and are expected to take appropriate and fair corrective action in 56 | response to any instances of unacceptable behavior. 57 | 58 | Project maintainers have the right and responsibility to remove, edit, or 59 | reject comments, commits, code, wiki edits, issues, and other contributions 60 | that are not aligned to this Code of Conduct, or to ban temporarily or 61 | permanently any contributor for other behaviors that they deem inappropriate, 62 | threatening, offensive, or harmful. 63 | 64 | ### Scope 65 | 66 | This Code of Conduct applies both within project spaces and in public spaces 67 | when an individual is representing the project or its community. Examples of 68 | representing a project or community include using an official project e-mail 69 | address, posting via an official social media account, or acting as an appointed 70 | representative at an online or offline event. Representation of a project may be 71 | further defined and clarified by project maintainers. 72 | 73 | ### Enforcement 74 | 75 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 76 | reported by contacting the project team at `david.oravsky@gmail.com`. All 77 | complaints will be reviewed and investigated and will result in a response that 78 | is deemed necessary and appropriate to the circumstances. The project team is 79 | obligated to maintain confidentiality with regard to the reporter of an incident. 80 | Further details of specific enforcement policies may be posted separately. 81 | 82 | Project maintainers who do not follow or enforce the Code of Conduct in good 83 | faith may face temporary or permanent repercussions as determined by other 84 | members of the project's leadership. 85 | 86 | ### Attribution 87 | 88 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 89 | available at [http://contributor-covenant.org/version/1/4][version] 90 | 91 | [homepage]: http://contributor-covenant.org 92 | [version]: http://contributor-covenant.org/version/1/4/ -------------------------------------------------------------------------------- /src/FixedMathSharp/Numerics/Extensions/Vector2d.Extensions.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace FixedMathSharp 4 | { 5 | public static partial class Vector2dExtensions 6 | { 7 | #region Vector2d Operations 8 | 9 | /// 10 | /// Clamps each component of the vector to the range [-1, 1] in place and returns the modified vector. 11 | /// 12 | /// The vector to clamp. 13 | /// The clamped vector with each component between -1 and 1. 14 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 15 | public static Vector2d ClampOneInPlace(this Vector2d v) 16 | { 17 | v.x = v.x.ClampOne(); 18 | v.y = v.y.ClampOne(); 19 | return v; 20 | } 21 | 22 | /// 23 | /// Checks if the distance between two vectors is less than or equal to a specified factor. 24 | /// 25 | /// The current vector. 26 | /// The vector to compare distance to. 27 | /// The maximum allowable distance. 28 | /// True if the distance between the vectors is less than or equal to the factor, false otherwise. 29 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 30 | public static bool CheckDistance(this Vector2d me, Vector2d other, Fixed64 factor) 31 | { 32 | var dis = Vector2d.Distance(me, other); 33 | return dis <= factor; 34 | } 35 | 36 | /// 37 | public static Vector2d Rotate(this Vector2d vec, Fixed64 angleInRadians) 38 | { 39 | return Vector2d.Rotate(vec, angleInRadians); 40 | } 41 | 42 | /// 43 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 44 | public static Vector2d Abs(this Vector2d value) 45 | { 46 | return Vector2d.Abs(value); 47 | } 48 | 49 | /// 50 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 51 | public static Vector2d Sign(this Vector2d value) 52 | { 53 | return Vector2d.Sign(value); 54 | } 55 | 56 | #endregion 57 | 58 | #region Conversion 59 | 60 | /// 61 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 62 | public static Vector2d ToDegrees(this Vector2d radians) 63 | { 64 | return Vector2d.ToDegrees(radians); 65 | } 66 | 67 | /// 68 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 69 | public static Vector2d ToRadians(this Vector2d degrees) 70 | { 71 | return Vector2d.ToRadians(degrees); 72 | } 73 | 74 | #endregion 75 | 76 | #region Equality 77 | 78 | /// 79 | /// Compares two vectors for approximate equality, allowing a fixed absolute difference. 80 | /// 81 | /// The current vector. 82 | /// The vector to compare against. 83 | /// The allowed absolute difference between each component. 84 | /// True if the components are within the allowed difference, false otherwise. 85 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 86 | public static bool FuzzyEqualAbsolute(this Vector2d me, Vector2d other, Fixed64 allowedDifference) 87 | { 88 | return (me.x - other.x).Abs() <= allowedDifference && 89 | (me.y - other.y).Abs() <= allowedDifference; 90 | } 91 | 92 | /// 93 | /// Compares two vectors for approximate equality, allowing a fractional difference (percentage). 94 | /// Handles zero components by only using the allowed percentage difference. 95 | /// 96 | /// The current vector. 97 | /// The vector to compare against. 98 | /// The allowed fractional difference (percentage) for each component. 99 | /// True if the components are within the allowed percentage difference, false otherwise. 100 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 101 | public static bool FuzzyEqual(this Vector2d me, Vector2d other, Fixed64? percentage = null) 102 | { 103 | Fixed64 p = percentage ?? Fixed64.Epsilon; 104 | return me.x.FuzzyComponentEqual(other.x, p) && me.y.FuzzyComponentEqual(other.y, p); 105 | } 106 | 107 | #endregion 108 | } 109 | } -------------------------------------------------------------------------------- /src/FixedMathSharp/FixedMathSharp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9.0 6 | net48;net8 7 | 8 | 9 | $(GitVersion_FullSemVer) 10 | 0.0.0 11 | 12 | $(GitVersion_InformationalVersion) 13 | $(SemVer) 14 | 15 | $(GitVersion_AssemblySemVer) 16 | $(SemVer).0 17 | $(GitVersion_AssemblySemFileVer) 18 | $(AssemblySemVer) 19 | 20 | $(InfoVer) 21 | $(SemVer) 22 | $(AssemblySemVer) 23 | $(AssemblySemFileVer) 24 | disable 25 | enable 26 | true 27 | portable 28 | true 29 | 1591 30 | 31 | bin\$(Configuration)\$(TargetFramework)\FixedMathSharp.xml 32 | 33 | true 34 | true 35 | Debug;Release 36 | 37 | 38 | 39 | true 40 | false 41 | DEBUG;TRACE 42 | 43 | 44 | true 45 | TRACE 46 | 47 | 48 | 49 | FixedMathSharp 50 | mrdav30 51 | FixedMathSharp: A high-precision, deterministic fixed-point math library for .NET. Ideal for simulations, games, and physics engines requiring reliable arithmetic without floating-point inaccuracies. 52 | fixed-point;math;precision;deterministic;arithmetic;fixed-point-arithmetic;math-library;trigonometry;dotnet;unity;simulations;physics-engine;game-development;high-precision;nuget 53 | https://github.com/mrdav30/FixedMathSharp 54 | icon.png 55 | https://raw.githubusercontent.com/mrdav30/fixedmathsharp/main/icon.png 56 | README.md 57 | LICENSE.md 58 | true 59 | snupkg 60 | 61 | 62 | 63 | FixedMathSharp 64 | FixedMathSharp 65 | {61834921-141D-4BD5-9E75-31188DF32E93} 66 | bin\$(Configuration)\ 67 | 68 | 69 | 70 | 71 | 72 | runtime; build; native; contentfiles; analyzers; buildtransitive 73 | all 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /.github/workflows/dotnet.yml: -------------------------------------------------------------------------------- 1 | # This workflow builds and tests the FixedMathSharp .NET project. 2 | # Documentation: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net 3 | 4 | name: .NET CI 5 | 6 | on: 7 | # Run the workflow on all branch pushes and pull requests 8 | push: 9 | branches-ignore: 10 | - 'dependabot/**' #avoid duplicates: only run the PR, not the push 11 | - 'gh-pages' #github pages do not trigger all tests 12 | tags-ignore: 13 | - 'v*' #avoid rerun existing commit on release 14 | pull_request: 15 | branches: 16 | - 'main' 17 | 18 | jobs: 19 | build-and-test-linux: 20 | if: | 21 | (github.event_name != 'pull_request' && !github.event.pull_request.head.repo.fork) 22 | || (github.event_name == 'pull_request' && (github.event.pull_request.head.repo.fork 23 | || startsWith(github.head_ref, 'dependabot/'))) 24 | runs-on: ubuntu-latest 25 | 26 | steps: 27 | - name: Checkout repository 28 | uses: actions/checkout@v4 29 | with: 30 | fetch-depth: 0 31 | persist-credentials: false # Ensure credentials aren't retained 32 | 33 | - name: Setup .NET 34 | uses: actions/setup-dotnet@v4 35 | with: 36 | dotnet-version: 8.0.x 37 | 38 | - name: Install Mono (required for .NET Framework tests on Linux) 39 | run: | 40 | sudo apt update 41 | sudo apt install -y mono-complete 42 | 43 | - name: Install GitVersion 44 | uses: gittools/actions/gitversion/setup@v3.1.1 45 | with: 46 | versionSpec: '6.0.x' 47 | 48 | - name: Cache NuGet packages 49 | uses: actions/cache@v3 50 | with: 51 | path: ~/.nuget/packages 52 | key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', '**/*.sln') }} 53 | restore-keys: | 54 | ${{ runner.os }}-nuget- 55 | 56 | - name: Determine Version 57 | id: version_step 58 | run: | 59 | chown -R $(whoami) $(pwd) 60 | dotnet-gitversion /output json > version.json 61 | echo "FULL_SEM_VER=$(grep -oP '"FullSemVer":\s*"\K[^"]+' version.json)" >> $GITHUB_ENV 62 | echo "ASSEMBLY_VERSION=$(grep -oP '"AssemblySemFileVer":\s*"\K[^"]+' version.json)" >> $GITHUB_ENV 63 | 64 | - name: Restore dependencies 65 | run: dotnet restore 66 | 67 | - name: Build Solution 68 | run: | 69 | echo "Version: ${{ env.FULL_SEM_VER }}" 70 | echo "Assembly Version: ${{ env.ASSEMBLY_VERSION }}" 71 | dotnet build --configuration Debug --no-restore 72 | 73 | - name: Test .NET48 74 | run: | 75 | mono ~/.nuget/packages/xunit.runner.console/2.9.3/tools/net48/xunit.console.exe ${{github.workspace}}/tests/FixedMathSharp.Tests/bin/Debug/net48/FixedMathSharp.Tests.dll 76 | 77 | - name: Test .NET8 78 | run: | 79 | dotnet test -f net8 --verbosity normal 80 | 81 | build-and-test-windows: 82 | if: | 83 | (github.event_name != 'pull_request' && !github.event.pull_request.head.repo.fork) 84 | || (github.event_name == 'pull_request' && (github.event.pull_request.head.repo.fork 85 | || startsWith(github.head_ref, 'dependabot/'))) 86 | runs-on: windows-latest 87 | steps: 88 | - name: Checkout repository 89 | uses: actions/checkout@v4 90 | with: 91 | fetch-depth: 0 92 | persist-credentials: false 93 | 94 | - name: Setup .NET 95 | uses: actions/setup-dotnet@v4 96 | with: 97 | dotnet-version: 8.0.x 98 | 99 | - name: Install GitVersion 100 | uses: gittools/actions/gitversion/setup@v3.1.1 101 | with: 102 | versionSpec: 6.0.x 103 | 104 | - name: Cache NuGet packages 105 | uses: actions/cache@v3 106 | with: 107 | path: ~/.nuget/packages 108 | key: '${{ runner.os }}-nuget-${{ hashFiles(''**/*.csproj'', ''**/*.sln'') }}' 109 | restore-keys: | 110 | ${{ runner.os }}-nuget- 111 | 112 | - name: Determine Version 113 | id: version_step 114 | shell: pwsh 115 | run: | 116 | chown -R $env:USERNAME $(Get-Location) 117 | dotnet-gitversion /output json | Out-File -FilePath version.json 118 | $json = Get-Content version.json | ConvertFrom-Json 119 | echo "FULL_SEM_VER=$($json.FullSemVer)" | Out-File -FilePath $env:GITHUB_ENV -Append 120 | echo "ASSEMBLY_VERSION=$($json.AssemblySemFileVer)" | Out-File -FilePath $env:GITHUB_ENV -Append 121 | 122 | - name: Restore dependencies 123 | run: dotnet restore 124 | 125 | - name: Build Solution 126 | run: | 127 | echo "Version: ${{ env.FULL_SEM_VER }}" 128 | echo "Assembly Version: ${{ env.ASSEMBLY_VERSION }}" 129 | dotnet build --configuration Debug --no-restore 130 | 131 | - name: Test .NET48 & .NET8 132 | run: | 133 | dotnet --info 134 | dotnet test --verbosity normal 135 | -------------------------------------------------------------------------------- /src/FixedMathSharp/Numerics/FixedCurve.cs: -------------------------------------------------------------------------------- 1 | using MessagePack; 2 | using System; 3 | using System.Linq; 4 | 5 | #if NET8_0_OR_GREATER 6 | using System.Text.Json.Serialization; 7 | #endif 8 | 9 | namespace FixedMathSharp 10 | { 11 | /// 12 | /// Specifies the interpolation method used when evaluating a . 13 | /// 14 | public enum FixedCurveMode 15 | { 16 | /// Linear interpolation between keyframes. 17 | Linear, 18 | 19 | /// Step interpolation, instantly jumping between keyframe values. 20 | Step, 21 | 22 | /// Smooth interpolation using a cosine function (SmoothStep). 23 | Smooth, 24 | 25 | /// Cubic interpolation for smoother curves using tangents. 26 | Cubic 27 | } 28 | 29 | /// 30 | /// A deterministic fixed-point curve that interpolates values between keyframes. 31 | /// Used for animations, physics calculations, and procedural data. 32 | /// 33 | [Serializable] 34 | [MessagePackObject] 35 | public class FixedCurve : IEquatable 36 | { 37 | [Key(0)] 38 | public FixedCurveMode Mode { get; private set; } 39 | 40 | [Key(1)] 41 | public FixedCurveKey[] Keyframes { get; private set; } 42 | 43 | /// 44 | /// Initializes a new instance of the with a default linear interpolation mode. 45 | /// 46 | /// The keyframes defining the curve. 47 | public FixedCurve(params FixedCurveKey[] keyframes) 48 | : this(FixedCurveMode.Linear, keyframes) { } 49 | 50 | /// 51 | /// Initializes a new instance of the with a specified interpolation mode. 52 | /// 53 | /// The interpolation method to use. 54 | /// The keyframes defining the curve. 55 | #if NET8_0_OR_GREATER 56 | [JsonConstructor] 57 | #endif 58 | [SerializationConstructor] 59 | public FixedCurve(FixedCurveMode mode, params FixedCurveKey[] keyframes) 60 | { 61 | Keyframes = keyframes.OrderBy(k => k.Time).ToArray(); 62 | Mode = mode; 63 | } 64 | 65 | /// 66 | /// Evaluates the curve at a given time using the specified interpolation mode. 67 | /// 68 | /// The time at which to evaluate the curve. 69 | /// The interpolated value at the given time. 70 | public Fixed64 Evaluate(Fixed64 time) 71 | { 72 | if (Keyframes.Length == 0) return Fixed64.One; 73 | 74 | // Clamp input within the keyframe range 75 | if (time <= Keyframes[0].Time) return Keyframes[0].Value; 76 | if (time >= Keyframes[Keyframes.Length - 1].Time) return Keyframes[Keyframes.Length - 1].Value; 77 | 78 | // Find the surrounding keyframes 79 | for (int i = 0; i < Keyframes.Length - 1; i++) 80 | { 81 | if (time >= Keyframes[i].Time && time < Keyframes[i + 1].Time) 82 | { 83 | // Compute interpolation factor 84 | Fixed64 t = (time - Keyframes[i].Time) / (Keyframes[i + 1].Time - Keyframes[i].Time); 85 | 86 | // Choose interpolation method 87 | return Mode switch 88 | { 89 | FixedCurveMode.Step => Keyframes[i].Value,// Immediate transition 90 | FixedCurveMode.Smooth => FixedMath.SmoothStep(Keyframes[i].Value, Keyframes[i + 1].Value, t), 91 | FixedCurveMode.Cubic => FixedMath.CubicInterpolate( 92 | Keyframes[i].Value, Keyframes[i + 1].Value, 93 | Keyframes[i].OutTangent, Keyframes[i + 1].InTangent, t), 94 | _ => FixedMath.LinearInterpolate(Keyframes[i].Value, Keyframes[i + 1].Value, t), 95 | }; 96 | } 97 | } 98 | 99 | return Fixed64.One; // Fallback (should never be hit) 100 | } 101 | 102 | public bool Equals(FixedCurve? other) 103 | { 104 | if (other is null) return false; 105 | if (ReferenceEquals(this, other)) return true; 106 | return Mode == other.Mode && Keyframes.SequenceEqual(other.Keyframes); 107 | } 108 | 109 | public override bool Equals(object? obj) => obj is FixedCurve other && Equals(other); 110 | 111 | public override int GetHashCode() 112 | { 113 | unchecked 114 | { 115 | int hash = (int)Mode; 116 | foreach (var key in Keyframes) 117 | hash = (hash * 31) ^ key.GetHashCode(); 118 | return hash; 119 | } 120 | } 121 | 122 | public static bool operator ==(FixedCurve left, FixedCurve right) => left?.Equals(right) ?? right is null; 123 | 124 | public static bool operator !=(FixedCurve left, FixedCurve right) => !(left == right); 125 | } 126 | } -------------------------------------------------------------------------------- /src/FixedMathSharp/Numerics/Extensions/Vector3d.Extensions.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace FixedMathSharp 4 | { 5 | public static partial class Vector3dExtensions 6 | { 7 | #region Vector3d Operations 8 | 9 | /// 10 | /// Clamps each component of the vector to the range [-1, 1] in place and returns the modified vector. 11 | /// 12 | /// The vector to clamp. 13 | /// The clamped vector with each component between -1 and 1. 14 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 15 | public static Vector3d ClampOneInPlace(this Vector3d v) 16 | { 17 | v.x = v.x.ClampOne(); 18 | v.y = v.y.ClampOne(); 19 | v.z = v.z.ClampOne(); 20 | return v; 21 | } 22 | 23 | public static Vector3d ClampMagnitude(this Vector3d value, Fixed64 maxMagnitude) 24 | { 25 | return Vector3d.ClampMagnitude(value, maxMagnitude); 26 | } 27 | 28 | /// 29 | /// Checks if the distance between two vectors is less than or equal to a specified factor. 30 | /// 31 | /// The current vector. 32 | /// The vector to compare distance to. 33 | /// The maximum allowable distance. 34 | /// True if the distance between the vectors is less than or equal to the factor, false otherwise. 35 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 36 | public static bool CheckDistance(this Vector3d me, Vector3d other, Fixed64 factor) 37 | { 38 | var dis = Vector3d.Distance(me, other); 39 | return dis <= factor; 40 | } 41 | 42 | /// 43 | public static Vector3d Rotate(this Vector3d source, Vector3d position, FixedQuaternion rotation) 44 | { 45 | return Vector3d.Rotate(source, position, rotation); 46 | } 47 | 48 | /// 49 | public static Vector3d InverseRotate(this Vector3d source, Vector3d position, FixedQuaternion rotation) 50 | { 51 | return Vector3d.InverseRotate(source, position, rotation); 52 | } 53 | 54 | #endregion 55 | 56 | #region Conversion 57 | 58 | /// 59 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 60 | public static Vector3d ToDegrees(this Vector3d radians) 61 | { 62 | return Vector3d.ToDegrees(radians); 63 | } 64 | 65 | /// 66 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 67 | public static Vector3d ToRadians(this Vector3d degrees) 68 | { 69 | return Vector3d.ToRadians(degrees); 70 | } 71 | 72 | /// 73 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 74 | public static Vector3d Abs(this Vector3d value) 75 | { 76 | return Vector3d.Abs(value); 77 | } 78 | 79 | /// 80 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 81 | public static Vector3d Sign(Vector3d value) 82 | { 83 | return Vector3d.Sign(value); 84 | } 85 | 86 | #endregion 87 | 88 | #region Equality 89 | 90 | /// 91 | /// Compares two vectors for approximate equality, allowing a fixed absolute difference. 92 | /// 93 | /// The current vector. 94 | /// The vector to compare against. 95 | /// The allowed absolute difference between each component. 96 | /// True if the components are within the allowed difference, false otherwise. 97 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 98 | public static bool FuzzyEqualAbsolute(this Vector3d me, Vector3d other, Fixed64 allowedDifference) 99 | { 100 | return (me.x - other.x).Abs() <= allowedDifference && 101 | (me.y - other.y).Abs() <= allowedDifference && 102 | (me.z - other.z).Abs() <= allowedDifference; 103 | } 104 | 105 | /// 106 | /// Compares two vectors for approximate equality, allowing a fractional difference (percentage). 107 | /// Handles zero components by only using the allowed percentage difference. 108 | /// 109 | /// The current vector. 110 | /// The vector to compare against. 111 | /// The allowed fractional difference (percentage) for each component. 112 | /// True if the components are within the allowed percentage difference, false otherwise. 113 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 114 | public static bool FuzzyEqual(this Vector3d me, Vector3d other, Fixed64? percentage = null) 115 | { 116 | Fixed64 p = percentage ?? Fixed64.Epsilon; 117 | return me.x.FuzzyComponentEqual(other.x, p) && 118 | me.y.FuzzyComponentEqual(other.y, p) && 119 | me.z.FuzzyComponentEqual(other.z, p); 120 | } 121 | 122 | #endregion 123 | } 124 | } -------------------------------------------------------------------------------- /src/FixedMathSharp/Bounds/BoundingSphere.cs: -------------------------------------------------------------------------------- 1 | using MessagePack; 2 | using System; 3 | using System.Runtime.CompilerServices; 4 | 5 | namespace FixedMathSharp 6 | { 7 | /// 8 | /// Represents a spherical bounding volume with fixed-point precision, optimized for fast, rotationally invariant spatial checks in 3D space. 9 | /// 10 | /// 11 | /// The BoundingSphere provides a simple yet effective way to represent the spatial extent of objects, especially when rotational invariance is required. 12 | /// Compared to BoundingBox, it offers faster intersection checks but is less precise in tightly fitting non-spherical objects. 13 | /// 14 | /// Use Cases: 15 | /// - Ideal for broad-phase collision detection, proximity checks, and culling in physics engines and rendering pipelines. 16 | /// - Useful when fast, rotationally invariant checks are needed, such as detecting overlaps or distances between moving objects. 17 | /// - Suitable for encapsulating objects with roughly spherical shapes or objects that rotate frequently, where the bounding box may need constant updates. 18 | /// 19 | [Serializable] 20 | [MessagePackObject] 21 | public struct BoundingSphere : IBound, IEquatable 22 | { 23 | #region Fields 24 | 25 | /// 26 | /// The center point of the sphere. 27 | /// 28 | [Key(0)] 29 | public Vector3d Center; 30 | 31 | /// 32 | /// The radius of the sphere. 33 | /// 34 | [Key(1)] 35 | public Fixed64 Radius; 36 | 37 | #endregion 38 | 39 | #region Constructors 40 | 41 | /// 42 | /// Initializes a new instance of the BoundingSphere struct with the specified center and radius. 43 | /// 44 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 45 | public BoundingSphere(Vector3d center, Fixed64 radius) 46 | { 47 | Center = center; 48 | Radius = radius; 49 | } 50 | 51 | #endregion 52 | 53 | #region Properties and Methods (Instance) 54 | 55 | [IgnoreMember] 56 | public Vector3d Min 57 | { 58 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 59 | get => Center - new Vector3d(Radius, Radius, Radius); 60 | } 61 | 62 | [IgnoreMember] 63 | public Vector3d Max 64 | { 65 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 66 | get => Center + new Vector3d(Radius, Radius, Radius); 67 | } 68 | 69 | /// 70 | /// The squared radius of the sphere. 71 | /// 72 | [IgnoreMember] 73 | public Fixed64 SqrRadius 74 | { 75 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 76 | get => Radius * Radius; 77 | } 78 | 79 | /// 80 | /// Checks if a point is inside the sphere. 81 | /// 82 | /// The point to check. 83 | /// True if the point is inside the sphere, otherwise false. 84 | public bool Contains(Vector3d point) 85 | { 86 | return Vector3d.SqrDistance(Center, point) <= SqrRadius; 87 | } 88 | 89 | /// 90 | /// Checks if this sphere intersects with another IBound. 91 | /// 92 | /// The other IBound to check for intersection. 93 | /// True if the IBounds intersect, otherwise false. 94 | public bool Intersects(IBound other) 95 | { 96 | switch (other) 97 | { 98 | case BoundingBox or BoundingArea: 99 | // Find the closest point on the BoundingArea to the sphere's center 100 | // Check if the closest point is within the sphere's radius 101 | return Vector3d.SqrDistance(Center, other.ProjectPointWithinBounds(Center)) <= SqrRadius; 102 | case BoundingSphere otherSphere: 103 | { 104 | Fixed64 distanceSquared = Vector3d.SqrDistance(Center, otherSphere.Center); 105 | Fixed64 combinedRadius = Radius + otherSphere.Radius; 106 | return distanceSquared <= combinedRadius * combinedRadius; 107 | } 108 | 109 | default: return false; // Default case for unknown or unsupported types 110 | }; 111 | } 112 | 113 | /// 114 | /// Projects a point onto the bounding sphere. If the point is outside the sphere, it returns the closest point on the surface. 115 | /// 116 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 117 | public Vector3d ProjectPoint(Vector3d point) 118 | { 119 | var direction = point - Center; 120 | if (direction.IsZero) return Center; // If the point is the center, return the center itself 121 | 122 | return Center + direction.Normalize() * Radius; 123 | } 124 | 125 | /// 126 | /// Calculates the distance from a point to the surface of the sphere. 127 | /// 128 | /// The point to calculate the distance from. 129 | /// The distance from the point to the surface of the sphere. 130 | public Fixed64 DistanceToSurface(Vector3d point) 131 | { 132 | return Vector3d.Distance(Center, point) - Radius; 133 | } 134 | 135 | #endregion 136 | 137 | #region Operators 138 | 139 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 140 | public static bool operator ==(BoundingSphere left, BoundingSphere right) => left.Equals(right); 141 | 142 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 143 | public static bool operator !=(BoundingSphere left, BoundingSphere right) => !left.Equals(right); 144 | 145 | #endregion 146 | 147 | #region Equality and HashCode Overrides 148 | 149 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 150 | public override bool Equals(object? obj) => obj is BoundingSphere other && Equals(other); 151 | 152 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 153 | public bool Equals(BoundingSphere other) => Center.Equals(other.Center) && Radius.Equals(other.Radius); 154 | 155 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 156 | public override int GetHashCode() 157 | { 158 | unchecked 159 | { 160 | int hash = 17; 161 | hash = hash * 23 + Center.GetHashCode(); 162 | hash = hash * 23 + Radius.GetHashCode(); 163 | return hash; 164 | } 165 | } 166 | 167 | #endregion 168 | } 169 | } -------------------------------------------------------------------------------- /tests/FixedMathSharp.Tests/FixedCurveTests.cs: -------------------------------------------------------------------------------- 1 | using MessagePack; 2 | 3 | #if NET48_OR_GREATER 4 | using System.IO; 5 | using System.Runtime.Serialization.Formatters.Binary; 6 | using System.Security.Cryptography; 7 | #endif 8 | 9 | #if NET8_0_OR_GREATER 10 | using System.Text.Json; 11 | using System.Text.Json.Serialization; 12 | #endif 13 | 14 | using Xunit; 15 | 16 | namespace FixedMathSharp.Tests 17 | { 18 | public class FixedCurveTests 19 | { 20 | [Fact] 21 | public void Evaluate_LinearInterpolation_ShouldInterpolateCorrectly() 22 | { 23 | FixedCurve curve = new FixedCurve(FixedCurveMode.Linear, 24 | new FixedCurveKey(0, 0), 25 | new FixedCurveKey(10, 100)); 26 | 27 | Assert.Equal(Fixed64.Zero, curve.Evaluate(Fixed64.Zero)); 28 | Assert.Equal((Fixed64)50, curve.Evaluate((Fixed64)5)); // Midpoint 29 | Assert.Equal((Fixed64)100, curve.Evaluate((Fixed64)10)); 30 | } 31 | 32 | [Fact] 33 | public void Evaluate_StepInterpolation_ShouldJumpToNearestKeyframe() 34 | { 35 | FixedCurve curve = new FixedCurve(FixedCurveMode.Step, 36 | new FixedCurveKey(0, 10), 37 | new FixedCurveKey(5, 50), 38 | new FixedCurveKey(10, 100)); 39 | 40 | Assert.Equal((Fixed64)10, curve.Evaluate(Fixed64.Zero)); 41 | Assert.Equal((Fixed64)10, curve.Evaluate((Fixed64)4.99)); // Should not interpolate 42 | Assert.Equal((Fixed64)50, curve.Evaluate((Fixed64)5)); 43 | Assert.Equal((Fixed64)50, curve.Evaluate((Fixed64)9.99)); 44 | Assert.Equal((Fixed64)100, curve.Evaluate((Fixed64)10)); 45 | } 46 | 47 | [Fact] 48 | public void Evaluate_SmoothInterpolation_ShouldSmoothlyTransition() 49 | { 50 | FixedCurve curve = new FixedCurve(FixedCurveMode.Smooth, 51 | new FixedCurveKey(0, 0), 52 | new FixedCurveKey(10, 100)); 53 | 54 | Fixed64 result = curve.Evaluate((Fixed64)5); 55 | Assert.True(result > (Fixed64)45 && result < (Fixed64)55, $"Unexpected smooth interpolation result: {result}"); 56 | } 57 | 58 | [Fact] 59 | public void Evaluate_CubicInterpolation_ShouldUseTangentsCorrectly() 60 | { 61 | FixedCurve curve = new FixedCurve(FixedCurveMode.Cubic, 62 | new FixedCurveKey(0, 0, 10, 10), 63 | new FixedCurveKey(10, 100, -10, -10)); 64 | 65 | Fixed64 result = curve.Evaluate((Fixed64)5); 66 | Assert.True(result > (Fixed64)45 && result < (Fixed64)55, $"Unexpected cubic interpolation result: {result}"); 67 | } 68 | 69 | [Fact] 70 | public void Evaluate_TimeBeforeFirstKeyframe_ShouldReturnFirstValue() 71 | { 72 | FixedCurve curve = new FixedCurve(FixedCurveMode.Linear, 73 | new FixedCurveKey(5, 50), 74 | new FixedCurveKey(10, 100)); 75 | 76 | Assert.Equal((Fixed64)50, curve.Evaluate(Fixed64.Zero)); 77 | } 78 | 79 | [Fact] 80 | public void Evaluate_TimeAfterLastKeyframe_ShouldReturnLastValue() 81 | { 82 | FixedCurve curve = new FixedCurve(FixedCurveMode.Linear, 83 | new FixedCurveKey(5, 50), 84 | new FixedCurveKey(10, 100)); 85 | 86 | Assert.Equal((Fixed64)100, curve.Evaluate((Fixed64)15)); 87 | } 88 | 89 | [Fact] 90 | public void Evaluate_SingleKeyframe_ShouldAlwaysReturnSameValue() 91 | { 92 | FixedCurve curve = new FixedCurve(FixedCurveMode.Linear, 93 | new FixedCurveKey(5, 50)); 94 | 95 | Assert.Equal((Fixed64)50, curve.Evaluate(Fixed64.Zero)); 96 | Assert.Equal((Fixed64)50, curve.Evaluate((Fixed64)10)); 97 | } 98 | 99 | [Fact] 100 | public void Evaluate_DuplicateKeyframes_ShouldHandleGracefully() 101 | { 102 | FixedCurve curve = new FixedCurve(FixedCurveMode.Linear, 103 | new FixedCurveKey(5, 50), 104 | new FixedCurveKey(5, 50), // Duplicate 105 | new FixedCurveKey(10, 100)); 106 | 107 | Assert.Equal((Fixed64)50, curve.Evaluate((Fixed64)5)); 108 | Assert.Equal((Fixed64)100, curve.Evaluate((Fixed64)10)); 109 | } 110 | 111 | [Fact] 112 | public void Evaluate_NegativeValues_ShouldInterpolateCorrectly() 113 | { 114 | FixedCurve curve = new FixedCurve(FixedCurveMode.Linear, 115 | new FixedCurveKey(-10, -100), 116 | new FixedCurveKey(0, 0), 117 | new FixedCurveKey(10, 100)); 118 | 119 | Assert.Equal((Fixed64)(-100), curve.Evaluate(-(Fixed64)10)); 120 | Assert.Equal((Fixed64)(0), curve.Evaluate(Fixed64.Zero)); 121 | Assert.Equal((Fixed64)(100), curve.Evaluate((Fixed64)10)); 122 | } 123 | 124 | [Fact] 125 | public void Evaluate_ExtremeValues_ShouldHandleCorrectly() 126 | { 127 | FixedCurve curve = new FixedCurve(FixedCurveMode.Linear, 128 | new FixedCurveKey(Fixed64.MIN_VALUE, -(Fixed64)10000), 129 | new FixedCurveKey(Fixed64.MAX_VALUE, (Fixed64)10000)); 130 | 131 | Assert.Equal((Fixed64)(-10000), curve.Evaluate(Fixed64.MIN_VALUE)); 132 | Assert.Equal((Fixed64)(10000), curve.Evaluate(Fixed64.MAX_VALUE)); 133 | } 134 | 135 | #region Test: Serialization 136 | 137 | [Fact] 138 | public void FixedCurve_NetSerialization_RoundTripMaintainsData() 139 | { 140 | var originalCurve = new FixedCurve( 141 | new FixedCurveKey(-10, -100), 142 | new FixedCurveKey(0, 0), 143 | new FixedCurveKey(10, 100)); 144 | 145 | // Serialize the Fixed3x3 object 146 | #if NET48_OR_GREATER 147 | var formatter = new BinaryFormatter(); 148 | using var stream = new MemoryStream(); 149 | formatter.Serialize(stream, originalCurve); 150 | 151 | // Reset stream position and deserialize 152 | stream.Seek(0, SeekOrigin.Begin); 153 | var deserializedCurve = (FixedCurve)formatter.Deserialize(stream); 154 | #endif 155 | 156 | #if NET8_0_OR_GREATER 157 | var jsonOptions = new JsonSerializerOptions 158 | { 159 | DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, 160 | ReferenceHandler = ReferenceHandler.IgnoreCycles, 161 | IncludeFields = true, 162 | IgnoreReadOnlyProperties = true 163 | }; 164 | var json = JsonSerializer.SerializeToUtf8Bytes(originalCurve, jsonOptions); 165 | var deserializedCurve = JsonSerializer.Deserialize(json, jsonOptions); 166 | #endif 167 | 168 | // Check that deserialized values match the original 169 | Assert.Equal(originalCurve, deserializedCurve); 170 | } 171 | 172 | [Fact] 173 | public void FixedCurve_MsgPackSerialization_RoundTripMaintainsData() 174 | { 175 | FixedCurve originalValue = new FixedCurve( 176 | new FixedCurveKey(-10, -100), 177 | new FixedCurveKey(0, 0), 178 | new FixedCurveKey(10, 100)); 179 | 180 | byte[] bytes = MessagePackSerializer.Serialize(originalValue); 181 | FixedCurve deserializedValue = MessagePackSerializer.Deserialize(bytes); 182 | 183 | // Check that deserialized values match the original 184 | Assert.Equal(originalValue, deserializedValue); 185 | } 186 | 187 | #endregion 188 | } 189 | } -------------------------------------------------------------------------------- /tests/FixedMathSharp.Tests/Fixed64.Tests.cs: -------------------------------------------------------------------------------- 1 | using MessagePack; 2 | using System; 3 | 4 | #if NET48_OR_GREATER 5 | using System.IO; 6 | using System.Runtime.Serialization.Formatters.Binary; 7 | #endif 8 | 9 | #if NET8_0_OR_GREATER 10 | using System.Text.Json; 11 | using System.Text.Json.Serialization; 12 | #endif 13 | 14 | using Xunit; 15 | 16 | namespace FixedMathSharp.Tests 17 | { 18 | public class Fixed64Tests 19 | { 20 | #region Test: Basic Arithmetic Operations (+, -, *, /) 21 | 22 | [Fact] 23 | public void Add_Fixed64Values_ReturnsCorrectSum() 24 | { 25 | var a = new Fixed64(2); 26 | var b = new Fixed64(3); 27 | var result = a + b; 28 | Assert.Equal(new Fixed64(5), result); 29 | } 30 | 31 | [Fact] 32 | public void Subtract_Fixed64Values_ReturnsCorrectDifference() 33 | { 34 | var a = new Fixed64(5); 35 | var b = new Fixed64(3); 36 | var result = a - b; 37 | Assert.Equal(new Fixed64(2), result); 38 | } 39 | 40 | [Fact] 41 | public void Multiply_Fixed64Values_ReturnsCorrectProduct() 42 | { 43 | var a = new Fixed64(2); 44 | var b = new Fixed64(3); 45 | var result = a * b; 46 | Assert.Equal(new Fixed64(6), result); 47 | } 48 | 49 | [Fact] 50 | public void Divide_Fixed64Values_ReturnsCorrectQuotient() 51 | { 52 | var a = new Fixed64(6); 53 | var b = new Fixed64(2); 54 | var result = a / b; 55 | Assert.Equal(new Fixed64(3), result); 56 | } 57 | 58 | [Fact] 59 | public void Divide_ByZero_ThrowsException() 60 | { 61 | var a = new Fixed64(6); 62 | Assert.Throws(() => { var result = a / Fixed64.Zero; }); 63 | } 64 | 65 | #endregion 66 | 67 | #region Test: Comparison Operators (<, <=, >, >=, ==, !=) 68 | 69 | [Fact] 70 | public void GreaterThan_Fixed64Values_ReturnsTrue() 71 | { 72 | var a = new Fixed64(5); 73 | var b = new Fixed64(3); 74 | Assert.True(a > b); 75 | } 76 | 77 | [Fact] 78 | public void LessThanOrEqual_Fixed64Values_ReturnsTrue() 79 | { 80 | var a = new Fixed64(3); 81 | var b = new Fixed64(5); 82 | Assert.True(a <= b); 83 | } 84 | 85 | [Fact] 86 | public void Equality_Fixed64Values_ReturnsTrue() 87 | { 88 | var a = new Fixed64(5); 89 | var b = new Fixed64(5); 90 | Assert.True(a == b); 91 | } 92 | 93 | [Fact] 94 | public void NotEquality_Fixed64Values_ReturnsTrue() 95 | { 96 | var a = new Fixed64(5); 97 | var b = new Fixed64(3); 98 | Assert.True(a != b); 99 | } 100 | 101 | #endregion 102 | 103 | #region Test: Implicit and Explicit Conversions 104 | 105 | [Fact] 106 | public void Convert_FromInteger_ReturnsCorrectFixed64() 107 | { 108 | Fixed64 result = (Fixed64)5; 109 | Assert.Equal(new Fixed64(5), result); 110 | } 111 | 112 | [Fact] 113 | public void Convert_FromFloat_ReturnsCorrectFixed64() 114 | { 115 | Fixed64 result = (Fixed64)5.5f; 116 | Assert.Equal(new Fixed64(5.5f), result); 117 | } 118 | 119 | [Fact] 120 | public void Convert_ToDouble_ReturnsCorrectDouble() 121 | { 122 | var fixedValue = new Fixed64(5.5f); 123 | double result = (double)fixedValue; 124 | Assert.Equal(5.5, result); 125 | } 126 | 127 | #endregion 128 | 129 | #region Test: Fraction Method 130 | 131 | [Fact] 132 | public void Fraction_CreatesCorrectFixed64Value() 133 | { 134 | var result = Fixed64.Fraction(1, 2); 135 | Assert.Equal(new Fixed64(0.5f), result); 136 | } 137 | 138 | #endregion 139 | 140 | #region Test: Arithmetic Overflow Protection 141 | 142 | [Fact] 143 | public void Add_OverflowProtection_ReturnsMaxValue() 144 | { 145 | var a = Fixed64.MAX_VALUE; 146 | var b = new Fixed64(1); 147 | var result = a + b; 148 | Assert.Equal(Fixed64.MAX_VALUE, result); 149 | } 150 | 151 | [Fact] 152 | public void Subtract_OverflowProtection_ReturnsMinValue() 153 | { 154 | var a = Fixed64.MIN_VALUE; 155 | var b = new Fixed64(1); 156 | var result = a - b; 157 | Assert.Equal(Fixed64.MIN_VALUE, result); 158 | } 159 | 160 | #endregion 161 | 162 | #region Test: Operations 163 | 164 | [Fact] 165 | public void IsInteger_PositiveInteger_ReturnsTrue() 166 | { 167 | var a = new Fixed64(42); 168 | Assert.True(a.IsInteger()); 169 | } 170 | 171 | [Fact] 172 | public void IsInteger_NegativeInteger_ReturnsTrue() 173 | { 174 | var a = new Fixed64(-42); 175 | Assert.True(a.IsInteger()); 176 | } 177 | 178 | [Fact] 179 | public void IsInteger_WhenZero_ReturnsTrue() 180 | { 181 | var a = Fixed64.Zero; 182 | Assert.True(a.IsInteger()); 183 | } 184 | 185 | [Fact] 186 | public void IsInteger_PositiveDecimal_ReturnsFalse() 187 | { 188 | var a = new Fixed64(4.2); 189 | Assert.False(a.IsInteger()); 190 | } 191 | 192 | [Fact] 193 | public void IsInteger_NegativeDecimal_ReturnsFalse() 194 | { 195 | var a = new Fixed64(-4.2); 196 | Assert.False(a.IsInteger()); 197 | } 198 | 199 | #endregion 200 | 201 | #region Test: Serialization 202 | 203 | [Fact] 204 | public void Fixed64_NetSerialization_RoundTripMaintainsData() 205 | { 206 | var originalValue = FixedMath.PI; 207 | 208 | // Serialize the Fixed64 object 209 | #if NET48_OR_GREATER 210 | var formatter = new BinaryFormatter(); 211 | using var stream = new MemoryStream(); 212 | formatter.Serialize(stream, originalValue); 213 | 214 | // Reset stream position and deserialize 215 | stream.Seek(0, SeekOrigin.Begin); 216 | var deserializedValue = (Fixed64)formatter.Deserialize(stream); 217 | #endif 218 | 219 | #if NET8_0_OR_GREATER 220 | var jsonOptions = new JsonSerializerOptions 221 | { 222 | DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, 223 | ReferenceHandler = ReferenceHandler.IgnoreCycles, 224 | IncludeFields = true, 225 | IgnoreReadOnlyProperties = true 226 | }; 227 | var json = JsonSerializer.SerializeToUtf8Bytes(originalValue, jsonOptions); 228 | var deserializedValue = JsonSerializer.Deserialize(json, jsonOptions); 229 | #endif 230 | 231 | // Check that deserialized values match the original 232 | Assert.Equal(originalValue, deserializedValue); 233 | } 234 | 235 | [Fact] 236 | public void Fixed64_MsgPackSerialization_RoundTripMaintainsData() 237 | { 238 | Fixed64 originalValue = FixedMath.PI; 239 | 240 | byte[] bytes = MessagePackSerializer.Serialize(originalValue); 241 | Fixed64 deserializedValue = MessagePackSerializer.Deserialize(bytes); 242 | 243 | // Check that deserialized values match the original 244 | Assert.Equal(originalValue, deserializedValue); 245 | } 246 | 247 | #endregion 248 | } 249 | } -------------------------------------------------------------------------------- /src/FixedMathSharp/Utility/DeterministicRandom.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | 4 | namespace FixedMathSharp.Utility 5 | { 6 | /// 7 | /// Fast, seedable, deterministic RNG suitable for lockstep sims and map gen. 8 | /// Uses xoroshiro128++ with splitmix64 seeding. No allocations, no time/GUID. 9 | /// 10 | public struct DeterministicRandom 11 | { 12 | // xoroshiro128++ state 13 | private ulong _s0; 14 | private ulong _s1; 15 | 16 | #region Construction / Seeding 17 | 18 | public DeterministicRandom(ulong seed) 19 | { 20 | // Expand a single seed into two 64-bit state words via splitmix64. 21 | _s0 = SplitMix64(ref seed); 22 | _s1 = SplitMix64(ref seed); 23 | 24 | // xoroshiro requires non-zero state; repair pathological seed. 25 | if (_s0 == 0UL && _s1 == 0UL) 26 | _s1 = 0x9E3779B97F4A7C15UL; 27 | } 28 | 29 | /// 30 | /// Create a stream deterministically 31 | /// Derived from (worldSeed, featureKey[,index]). 32 | /// 33 | public static DeterministicRandom FromWorldFeature(ulong worldSeed, ulong featureKey, ulong index = 0) 34 | { 35 | // Simple reversible mix (swap for a stronger mix if required). 36 | ulong seed = Mix64(worldSeed, featureKey); 37 | seed = Mix64(seed, index); 38 | return new DeterministicRandom(seed); 39 | } 40 | 41 | #endregion 42 | 43 | #region Core PRNG 44 | 45 | /// 46 | /// xoroshiro128++ next 64 bits. 47 | /// 48 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 49 | public ulong NextU64() 50 | { 51 | ulong s0 = _s0, s1 = _s1; 52 | ulong result = RotL(s0 + s1, 17) + s0; 53 | 54 | s1 ^= s0; 55 | _s0 = RotL(s0, 49) ^ s1 ^ (s1 << 21); // a,b 56 | _s1 = RotL(s1, 28); // c 57 | 58 | return result; 59 | } 60 | 61 | /// 62 | /// Next non-negative Int32 in [0, int.MaxValue]. 63 | /// 64 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 65 | public int Next() 66 | { 67 | // Take high bits for better quality; mask to 31 bits non-negative. 68 | return (int)(NextU64() >> 33); 69 | } 70 | 71 | /// 72 | /// Unbiased int in [0, maxExclusive). 73 | /// 74 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 75 | public int Next(int maxExclusive) 76 | { 77 | return maxExclusive <= 0 78 | ? throw new ArgumentOutOfRangeException(nameof(maxExclusive)) 79 | : (int)NextBounded((uint)maxExclusive); 80 | } 81 | 82 | /// 83 | /// Unbiased int in [min, maxExclusive). 84 | /// 85 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 86 | public int Next(int minInclusive, int maxExclusive) 87 | { 88 | if (minInclusive >= maxExclusive) 89 | throw new ArgumentException("min >= max"); 90 | uint range = (uint)(maxExclusive - minInclusive); 91 | return minInclusive + (int)NextBounded(range); 92 | } 93 | 94 | /// 95 | /// Double in [0,1). 96 | /// 97 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 98 | public double NextDouble() 99 | { 100 | // 53 random bits -> [0,1) 101 | return (NextU64() >> 11) * (1.0 / (1UL << 53)); 102 | } 103 | 104 | /// 105 | /// Fill span with random bytes. 106 | /// 107 | public void NextBytes(Span buffer) 108 | { 109 | int i = 0; 110 | while (i + 8 <= buffer.Length) 111 | { 112 | ulong v = NextU64(); 113 | Unsafe.WriteUnaligned(ref buffer[i], v); 114 | i += 8; 115 | } 116 | if (i < buffer.Length) 117 | { 118 | ulong v = NextU64(); 119 | while (i < buffer.Length) 120 | { 121 | buffer[i++] = (byte)v; 122 | v >>= 8; 123 | } 124 | } 125 | } 126 | 127 | #endregion 128 | 129 | #region Fixed64 helpers 130 | 131 | /// 132 | /// Random Fixed64 in [0,1). 133 | /// 134 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 135 | public Fixed64 NextFixed6401() 136 | { 137 | // Produce a raw value in [0, One.m_rawValue) 138 | ulong rawOne = (ulong)Fixed64.One.m_rawValue; 139 | ulong r = NextBounded(rawOne); 140 | return Fixed64.FromRaw((long)r); 141 | } 142 | 143 | /// 144 | /// Random Fixed64 in [0, maxExclusive). 145 | /// 146 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 147 | public Fixed64 NextFixed64(Fixed64 maxExclusive) 148 | { 149 | if (maxExclusive <= Fixed64.Zero) 150 | throw new ArgumentOutOfRangeException(nameof(maxExclusive), "max must be > 0"); 151 | ulong rawMax = (ulong)maxExclusive.m_rawValue; 152 | ulong r = NextBounded(rawMax); 153 | return Fixed64.FromRaw((long)r); 154 | } 155 | 156 | /// 157 | /// Random Fixed64 in [minInclusive, maxExclusive). 158 | /// 159 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 160 | public Fixed64 NextFixed64(Fixed64 minInclusive, Fixed64 maxExclusive) 161 | { 162 | if (minInclusive >= maxExclusive) 163 | throw new ArgumentException("min >= max"); 164 | ulong span = (ulong)(maxExclusive.m_rawValue - minInclusive.m_rawValue); 165 | ulong r = NextBounded(span); 166 | return Fixed64.FromRaw((long)r + minInclusive.m_rawValue); 167 | } 168 | 169 | #endregion 170 | 171 | #region Internals: unbiased range, splitmix64, mixing, rotations 172 | 173 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 174 | private ulong NextBounded(ulong bound) 175 | { 176 | // Rejection to avoid modulo bias. 177 | // threshold = 2^64 % bound, but expressed as (-bound) % bound 178 | ulong threshold = unchecked((ulong)-(long)bound) % bound; 179 | while (true) 180 | { 181 | ulong r = NextU64(); 182 | if (r >= threshold) 183 | return r % bound; 184 | } 185 | } 186 | 187 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 188 | private static ulong RotL(ulong x, int k) => (x << k) | (x >> (64 - k)); 189 | 190 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 191 | private static ulong SplitMix64(ref ulong state) 192 | { 193 | ulong z = (state += 0x9E3779B97F4A7C15UL); 194 | z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9UL; 195 | z = (z ^ (z >> 27)) * 0x94D049BB133111EBUL; 196 | return z ^ (z >> 31); 197 | } 198 | 199 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 200 | private static ulong Mix64(ulong a, ulong b) 201 | { 202 | // Simple reversible mix (variant of splitmix finalizer). 203 | ulong x = a ^ (b + 0x9E3779B97F4A7C15UL); 204 | x = (x ^ (x >> 30)) * 0xBF58476D1CE4E5B9UL; 205 | x = (x ^ (x >> 27)) * 0x94D049BB133111EBUL; 206 | return x ^ (x >> 31); 207 | } 208 | 209 | #endregion 210 | } 211 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | FixedMathSharp 2 | ============== 3 | 4 | ![FixedMathSharp Icon](https://raw.githubusercontent.com/mrdav30/fixedmathsharp/main/icon.png) 5 | 6 | [![.NET CI](https://github.com/mrdav30/FixedMathSharp/actions/workflows/dotnet.yml/badge.svg)](https://github.com/mrdav30/FixedMathSharp/actions/workflows/dotnet.yml) 7 | 8 | **A high-precision, deterministic fixed-point math library for .NET.** 9 | Ideal for simulations, games, and physics engines requiring reliable arithmetic without floating-point inaccuracies. 10 | 11 | --- 12 | 13 | ## 🛠️ Key Features 14 | 15 | - **Deterministic Calculations:** Ensures consistent results across different platforms. 16 | - **High Precision Arithmetic:** Uses fixed-point math to eliminate floating-point inaccuracies. 17 | - **Comprehensive Vector Support:** Includes 2D and 3D vector operations (`Vector2d`, `Vector3d`). 18 | - **Quaternion Rotations:** Leverage `FixedQuaternion` for smooth rotations without gimbal lock. 19 | - **Matrix Operations:** Supports transformations with `Fixed4x4` and `Fixed3x3` matrices. 20 | - **Bounding Shapes:** Includes `IBound` structs `BoundingBox`, `BoundingSphere`, and `BoundingArea` for lightweight spatial calculations. 21 | - **Advanced Math Functions:** Includes trigonometry and common math utilities. 22 | - **Framework Agnostic:** Works with **.NET, Unity, and other game engines**. 23 | - **Full Serialization Support:** Out-of-the-box round-trip serialization via BinaryFormatter (for .NET Framework 4.8+), System.Text.Json (for .NET 8+), and MessagePack across all serializable structs. 24 | 25 | --- 26 | 27 | ## 🚀 Installation 28 | 29 | 30 | Clone the repository and add it to your project: 31 | 32 | ### Non-Unity Projects 33 | 34 | 1. **Install via NuGet**: 35 | - Add FixedMathSharp to your project using the following command: 36 | 37 | ```bash 38 | dotnet add package FixedMathSharp 39 | ``` 40 | 41 | 2. **Or Download/Clone**: 42 | - Clone the repository or download the source code. 43 | 44 | ```bash 45 | git clone https://github.com/mrdav30/FixedMathSharp.git 46 | ``` 47 | 48 | 3. **Add to Project**: 49 | 50 | - Include the FixedMathSharp project or its DLLs in your build process. 51 | 52 | ### Unity Integration 53 | 54 | FixedMathSharp is now maintained as a separate Unity package.For Unity-specific implementations, refer to: 55 | 56 | 🔗 [FixedMathSharp-Unity Repository](https://github.com/mrdav30/FixedMathSharp-Unity). 57 | 58 | --- 59 | 60 | ## 📖 Usage Examples 61 | 62 | ### Basic Arithmetic with `Fixed64`: 63 | ```csharp 64 | Fixed64 a = new Fixed64(1.5); 65 | Fixed64 b = new Fixed64(2.5); 66 | Fixed64 result = a + b; 67 | Console.WriteLine(result); // Output: 4.0 68 | ``` 69 | 70 | ### Vector Operations: 71 | ```csharp 72 | Vector3d v1 = new Vector3d(1, 2, 3); 73 | Vector3d v2 = new Vector3d(4, 5, 6); 74 | Fixed64 dotProduct = Vector3d.Dot(v1, v2); 75 | Console.WriteLine(dotProduct); // Output: 32 76 | ``` 77 | 78 | ### Quaternion Rotation: 79 | ```csharp 80 | FixedQuaternion rotation = FixedQuaternion.FromAxisAngle(Vector3d.Up, FixedMath.PiOver2); // 90 degrees around Y-axis 81 | Vector3d point = new Vector3d(1, 0, 0); 82 | Vector3d rotatedPoint = rotation.Rotate(point); 83 | Console.WriteLine(rotatedPoint); // Output: (0, 0, -1) 84 | ``` 85 | 86 | ### Matrix Transformations: 87 | ```csharp 88 | Fixed4x4 matrix = Fixed4x4.Identity; 89 | Vector3d position = new Vector3d(1, 2, 3); 90 | matrix.SetTransform(position, Vector3d.One, FixedQuaternion.Identity); 91 | Console.WriteLine(matrix); 92 | ``` 93 | 94 | ### Bounding Shapes and Intersection 95 | ```csharp 96 | BoundingBox box = new BoundingBox(new Vector3d(0, 0, 0), new Vector3d(5, 5, 5)); 97 | BoundingSphere sphere = new BoundingSphere(new Vector3d(3, 3, 3), new Fixed64(1)); 98 | bool intersects = box.Intersects(sphere); 99 | Console.WriteLine(intersects); // Output: True 100 | ``` 101 | 102 | ### Trigonometry Example: 103 | ```csharp 104 | Fixed64 angle = FixedMath.PiOver4; // 45 degrees 105 | Fixed64 sinValue = FixedTrigonometry.Sin(angle); 106 | Console.WriteLine(sinValue); // Output: ~0.707 107 | ``` 108 | 109 | ### Deterministic Random Generation 110 | 111 | Use `DeterministicRandom` when you need reproducible random values across runs, worlds, or features. 112 | Streams are derived from a seed and remain deterministic regardless of threading or platform. 113 | 114 | ```csharp 115 | // Simple constructor-based stream: 116 | var rng = new DeterministicRandom(42UL); 117 | 118 | // Deterministic integer: 119 | int value = rng.Next(1, 10); // [1,10) 120 | 121 | // Deterministic Fixed64 in [0,1): 122 | Fixed64 ratio = rng.NextFixed6401(); 123 | 124 | // One stream per “feature” that’s stable for the same worldSeed + key: 125 | var rngOre = DeterministicRandom.FromWorldFeature(worldSeed: 123456789UL, featureKey: 0xORE); 126 | var rngRivers = DeterministicRandom.FromWorldFeature(123456789UL, 0xRIV, index: 0); 127 | 128 | // Deterministic Fixed64 draws: 129 | Fixed64 h = rngOre.NextFixed64(Fixed64.One); // [0, 1) 130 | Fixed64 size = rngOre.NextFixed64(Fixed64.Zero, 5 * Fixed64.One); // [0, 5) 131 | Fixed64 posX = rngRivers.NextFixed64(-Fixed64.One, Fixed64.One); // [-1, 1) 132 | 133 | // Deterministic integers: 134 | int loot = rngOre.Next(1, 5); // [1,5) 135 | ``` 136 | 137 | --- 138 | 139 | ## 📦 Library Structure 140 | 141 | - **`Fixed64` Struct:** Represents fixed-point numbers for precise arithmetic. 142 | - **`Vector2d` and `Vector3d` Structs:** Handle 2D and 3D vector operations. 143 | - **`FixedQuaternion` Struct:** Provides rotation handling without gimbal lock, enabling smooth rotations and quaternion-based transformations. 144 | - **`IBound` Interface:** Standard interface for bounding shapes `BoundingBox`, `BoundingArea`, and `BoundingSphere`, each offering intersection, containment, and projection logic. 145 | - **`FixedMath` Static Class:** Provides common math and trigonometric functions using fixed-point math. 146 | - **`Fixed4x4` and `Fixed3x3`:** Support matrix operations for transformations. 147 | - **`DeterministicRandom` Struct:** Seedable, allocation-free RNG for repeatable procedural generation. 148 | 149 | ### Fixed64 Struct 150 | 151 | **Fixed64** is the core data type representing fixed-point numbers. It provides various mathematical operations, including addition, subtraction, multiplication, division, and more. 152 | The struct guarantees deterministic behavior by using integer-based arithmetic with a configurable `SHIFT_AMOUNT`. 153 | 154 | --- 155 | 156 | ## ⚡ Performance Considerations 157 | 158 | FixedMathSharp is optimized for high-performance deterministic calculations: 159 | - **Inline methods and bit-shifting optimizations** ensure minimal overhead. 160 | - **Eliminates floating-point drift**, making it ideal for lockstep simulations. 161 | - **Supports fuzzy equality comparisons** for handling minor precision deviations. 162 | 163 | --- 164 | 165 | ## 🧪 Testing and Validation 166 | 167 | Unit tests are used extensively to validate the correctness of mathematical operations. 168 | Special **fuzzy comparisons** are employed where small precision discrepancies might occur, mimicking floating-point behavior. 169 | 170 | To run the tests: 171 | ```bash 172 | dotnet test --configuration debug 173 | ``` 174 | 175 | --- 176 | 177 | ## 🛠️ Compatibility 178 | 179 | - **.NET Framework** 4.7.2+ 180 | - **.NET Core / .NET** 6+ 181 | - **Unity 2020+** (via [FixedMathSharp-Unity](https://github.com/mrdav30/FixedMathSharp-Unity)) 182 | - **Cross-Platform Support** (Windows, Linux, macOS) 183 | 184 | --- 185 | 186 | ## 🤝 Contributing 187 | 188 | We welcome contributions! Please see our [CONTRIBUTING](https://github.com/mrdav30/FixedMathSharp/blob/main/CONTRIBUTING.md) guide for details on how to propose changes, report issues, and interact with the community. 189 | 190 | ## 📄 License 191 | 192 | This project is licensed under the MIT License - see the [LICENSE](https://github.com/mrdav30/FixedMathSharp/blob/main/LICENSE.md) for details. 193 | 194 | --- 195 | 196 | ## 👥 Contributors 197 | 198 | - **mrdav30** - Lead Developer 199 | - Contributions are welcome! Feel free to submit pull requests or report issues. 200 | 201 | --- 202 | 203 | ## 📧 Contact 204 | 205 | For questions or support, reach out to **mrdav30** via GitHub or open an issue in the repository. 206 | 207 | --- 208 | -------------------------------------------------------------------------------- /tests/FixedMathSharp.Tests/Bounds/BoundingArea.Tests.cs: -------------------------------------------------------------------------------- 1 | using MessagePack; 2 | 3 | #if NET48_OR_GREATER 4 | using System.IO; 5 | using System.Runtime.Serialization.Formatters.Binary; 6 | #endif 7 | 8 | #if NET8_0_OR_GREATER 9 | using System.Text.Json; 10 | using System.Text.Json.Serialization; 11 | #endif 12 | 13 | using Xunit; 14 | 15 | namespace FixedMathSharp.Tests.Bounds 16 | { 17 | public class BoundingAreaTests 18 | { 19 | #region Test: Constructor and Property 20 | 21 | [Fact] 22 | public void Constructor_AssignsCornersCorrectly() 23 | { 24 | var corner1 = new Vector3d(1, 2, 3); 25 | var corner2 = new Vector3d(4, 5, 6); 26 | 27 | var area = new BoundingArea(corner1, corner2); 28 | 29 | Assert.Equal(corner1, area.Corner1); 30 | Assert.Equal(corner2, area.Corner2); 31 | } 32 | 33 | [Fact] 34 | public void MinMaxProperties_AreCorrect() 35 | { 36 | var area = new BoundingArea( 37 | new Vector3d(1, 2, 3), 38 | new Vector3d(4, 5, 6) 39 | ); 40 | 41 | Assert.Equal(Fixed64.One, area.MinX); 42 | Assert.Equal(new Fixed64(4), area.MaxX); 43 | Assert.Equal(new Fixed64(2), area.MinY); 44 | Assert.Equal(new Fixed64(5), area.MaxY); 45 | Assert.Equal(new Fixed64(3), area.MinZ); 46 | Assert.Equal(new Fixed64(6), area.MaxZ); 47 | } 48 | 49 | #endregion 50 | 51 | #region Test: Containment 52 | 53 | [Fact] 54 | public void Contains_PointInside_ReturnsTrue() 55 | { 56 | var area = new BoundingArea(new Vector3d(1, 1, 1), new Vector3d(5, 5, 5)); 57 | var point = new Vector3d(3, 3, 3); 58 | 59 | Assert.True(area.Contains(point)); 60 | } 61 | 62 | [Fact] 63 | public void Contains_PointOutside_ReturnsFalse() 64 | { 65 | var area = new BoundingArea(new Vector3d(1, 1, 1), new Vector3d(5, 5, 5)); 66 | var point = new Vector3d(6, 6, 6); 67 | 68 | Assert.False(area.Contains(point)); 69 | } 70 | 71 | [Fact] 72 | public void Contains_PointOnBoundary_ReturnsTrue() 73 | { 74 | var area = new BoundingArea(new Vector3d(1, 1, 1), new Vector3d(5, 5, 5)); 75 | var point = new Vector3d(1, 3, 5); 76 | 77 | Assert.True(area.Contains(point)); 78 | } 79 | 80 | #endregion 81 | 82 | #region Test: Intersection 83 | 84 | [Fact] 85 | public void Intersects_WithOverlappingArea_ReturnsTrue() 86 | { 87 | var area1 = new BoundingArea(new Vector3d(1, 1, 1), new Vector3d(5, 5, 5)); 88 | var area2 = new BoundingArea(new Vector3d(3, 3, 3), new Vector3d(6, 6, 6)); 89 | 90 | Assert.True(area1.Intersects(area2)); 91 | 92 | var area3 = new BoundingArea(new Vector3d(-2, -2, 0), new Vector3d(2, 2, 0)); 93 | var area4 = new BoundingArea(new Vector3d(-1, -1, 0), new Vector3d(3, 3, 0)); 94 | Assert.True(area3.Intersects(area4)); 95 | } 96 | 97 | [Fact] 98 | public void Intersects_WithNonOverlappingArea_ReturnsFalse() 99 | { 100 | var area1 = new BoundingArea(new Vector3d(1, 1, 1), new Vector3d(2, 2, 2)); 101 | var area2 = new BoundingArea(new Vector3d(3, 3, 3), new Vector3d(4, 4, 4)); 102 | 103 | Assert.False(area1.Intersects(area2)); 104 | } 105 | 106 | [Fact] 107 | public void Intersects_WithBoundingBox_ReturnsTrue() 108 | { 109 | var area = new BoundingArea(new Vector3d(1, 1, 1), new Vector3d(5, 5, 5)); 110 | var box = new BoundingBox(new Vector3d(4, 4, 4), new Vector3d(6, 6, 6)); 111 | 112 | Assert.True(area.Intersects(box)); 113 | } 114 | 115 | [Fact] 116 | public void Intersects_WithBoundingSphere_ReturnsTrue() 117 | { 118 | var area = new BoundingArea(new Vector3d(1, 1, 1), new Vector3d(5, 5, 5)); 119 | var sphere = new BoundingSphere(new Vector3d(4, 4, 4), Fixed64.One); 120 | 121 | Assert.True(area.Intersects(sphere)); 122 | } 123 | 124 | #endregion 125 | 126 | #region Test: Equality 127 | 128 | [Fact] 129 | public void Equality_SameCorners_ReturnsTrue() 130 | { 131 | var area1 = new BoundingArea(new Vector3d(1, 2, 3), new Vector3d(4, 5, 6)); 132 | var area2 = new BoundingArea(new Vector3d(1, 2, 3), new Vector3d(4, 5, 6)); 133 | 134 | Assert.True(area1 == area2); 135 | } 136 | 137 | [Fact] 138 | public void Equality_DifferentCorners_ReturnsFalse() 139 | { 140 | var area1 = new BoundingArea(new Vector3d(1, 2, 3), new Vector3d(4, 5, 6)); 141 | var area2 = new BoundingArea(new Vector3d(0, 2, 3), new Vector3d(4, 5, 7)); 142 | 143 | Assert.False(area1 == area2); 144 | } 145 | 146 | [Fact] 147 | public void GetHashCode_SameArea_ReturnsSameHash() 148 | { 149 | var area1 = new BoundingArea(new Vector3d(1, 2, 3), new Vector3d(4, 5, 6)); 150 | var area2 = new BoundingArea(new Vector3d(1, 2, 3), new Vector3d(4, 5, 6)); 151 | 152 | Assert.Equal(area1.GetHashCode(), area2.GetHashCode()); 153 | } 154 | 155 | #endregion 156 | 157 | #region Test: Edge Cases 158 | 159 | [Fact] 160 | public void Contains_ZeroSizeArea_ContainsPoint() 161 | { 162 | var area = new BoundingArea(new Vector3d(1, 1, 1), new Vector3d(1, 1, 1)); 163 | var point = new Vector3d(1, 1, 1); 164 | 165 | Assert.True(area.Contains(point)); 166 | } 167 | 168 | [Fact] 169 | public void Intersects_ZeroSizeArea_ReturnsFalseForNonOverlapping() 170 | { 171 | var area1 = new BoundingArea(new Vector3d(1, 1, 1), new Vector3d(1, 1, 1)); 172 | var area2 = new BoundingArea(new Vector3d(2, 2, 2), new Vector3d(3, 3, 3)); 173 | 174 | Assert.False(area1.Intersects(area2)); 175 | } 176 | 177 | #endregion 178 | 179 | #region Test: Serialization 180 | 181 | [Fact] 182 | public void BoundingArea_NetSerialization_RoundTripMaintainsData() 183 | { 184 | BoundingArea originalValue = new( 185 | new Vector3d(1, 2, 3), 186 | new Vector3d(4, 5, 6) 187 | ); 188 | 189 | // Serialize the BoundingArea object 190 | #if NET48_OR_GREATER 191 | var formatter = new BinaryFormatter(); 192 | using var stream = new MemoryStream(); 193 | formatter.Serialize(stream, originalValue); 194 | 195 | // Reset stream position and deserialize 196 | stream.Seek(0, SeekOrigin.Begin); 197 | var deserializedValue = (BoundingArea)formatter.Deserialize(stream); 198 | #endif 199 | 200 | #if NET8_0_OR_GREATER 201 | var jsonOptions = new JsonSerializerOptions 202 | { 203 | DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, 204 | ReferenceHandler = ReferenceHandler.IgnoreCycles, 205 | IncludeFields = true, 206 | IgnoreReadOnlyProperties = true 207 | }; 208 | var json = JsonSerializer.SerializeToUtf8Bytes(originalValue, jsonOptions); 209 | var deserializedValue = JsonSerializer.Deserialize(json, jsonOptions); 210 | #endif 211 | 212 | // Check that deserialized values match the original 213 | Assert.Equal(originalValue, deserializedValue); 214 | } 215 | 216 | [Fact] 217 | public void BoundingArea_MsgPackSerialization_RoundTripMaintainsData() 218 | { 219 | BoundingArea originalValue = new( 220 | new Vector3d(1, 2, 3), 221 | new Vector3d(4, 5, 6) 222 | ); 223 | 224 | byte[] bytes = MessagePackSerializer.Serialize(originalValue); 225 | BoundingArea deserializedValue = MessagePackSerializer.Deserialize(bytes); 226 | 227 | // Check that deserialized values match the original 228 | Assert.Equal(originalValue, deserializedValue); 229 | } 230 | 231 | #endregion 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /tests/FixedMathSharp.Tests/Bounds/BoundingBox.Tests.cs: -------------------------------------------------------------------------------- 1 | using MessagePack; 2 | 3 | #if NET48_OR_GREATER 4 | using System.IO; 5 | using System.Runtime.Serialization.Formatters.Binary; 6 | #endif 7 | 8 | #if NET8_0_OR_GREATER 9 | using System.Text.Json; 10 | using System.Text.Json.Serialization; 11 | #endif 12 | 13 | using Xunit; 14 | 15 | namespace FixedMathSharp.Tests.Bounds 16 | { 17 | public class BoundingBoxTests 18 | { 19 | #region Test: Constructor and Property 20 | 21 | [Fact] 22 | public void Constructor_AssignsValuesCorrectly() 23 | { 24 | var center = new Vector3d(0, 0, 0); 25 | var size = new Vector3d(2, 2, 2); 26 | 27 | var box = new BoundingBox(center, size); 28 | 29 | Assert.Equal(center, box.Center); 30 | Assert.Equal(size, box.Proportions); 31 | Assert.Equal(new Vector3d(-1, -1, -1), box.Min); 32 | Assert.Equal(new Vector3d(1, 1, 1), box.Max); 33 | } 34 | 35 | #endregion 36 | 37 | #region Test: Containment 38 | 39 | [Fact] 40 | public void Contains_PointInside_ReturnsTrue() 41 | { 42 | var box = new BoundingBox(new Vector3d(0, 0, 0), new Vector3d(4, 4, 4)); 43 | var point = new Vector3d(1, 1, 1); 44 | 45 | Assert.True(box.Contains(point)); 46 | } 47 | 48 | [Fact] 49 | public void Contains_PointOutside_ReturnsFalse() 50 | { 51 | var box = new BoundingBox(new Vector3d(0, 0, 0), new Vector3d(4, 4, 4)); 52 | var point = new Vector3d(5, 5, 5); 53 | 54 | Assert.False(box.Contains(point)); 55 | } 56 | 57 | [Fact] 58 | public void Contains_PointOnBoundary_ReturnsTrue() 59 | { 60 | var box = new BoundingBox(new Vector3d(0, 0, 0), new Vector3d(4, 4, 4)); 61 | var point = new Vector3d(2, 0, 0); 62 | 63 | Assert.True(box.Contains(point)); 64 | } 65 | 66 | #endregion 67 | 68 | #region Test: Intersection 69 | 70 | [Fact] 71 | public void Intersects_WithOverlappingBox_ReturnsTrue() 72 | { 73 | var box1 = new BoundingBox(new Vector3d(0, 0, 0), new Vector3d(4, 4, 4)); 74 | var box2 = new BoundingBox(new Vector3d(1, 1, 1), new Vector3d(4, 4, 4)); 75 | 76 | Assert.True(box1.Intersects(box2)); 77 | 78 | var area3 = new BoundingBox(new Vector3d(-2, -2, 0), new Vector3d(2, 2, 0)); 79 | var area4 = new BoundingBox(new Vector3d(-1, -1, 0), new Vector3d(3, 3, 0)); 80 | Assert.False(area3.Intersects(area4)); 81 | } 82 | 83 | [Fact] 84 | public void Intersects_WithNonOverlappingBox_ReturnsFalse() 85 | { 86 | var box1 = new BoundingBox(new Vector3d(0, 0, 0), new Vector3d(2, 2, 2)); 87 | var box2 = new BoundingBox(new Vector3d(5, 5, 5), new Vector3d(2, 2, 2)); 88 | 89 | Assert.False(box1.Intersects(box2)); 90 | } 91 | 92 | [Fact] 93 | public void Intersects_WithBoundingArea_ReturnsTrue() 94 | { 95 | var box = new BoundingBox(new Vector3d(0, 0, 0), new Vector3d(4, 4, 4)); 96 | var area = new BoundingArea(new Vector3d(1, 1, 1), new Vector3d(3, 3, 3)); 97 | 98 | Assert.True(box.Intersects(area)); 99 | } 100 | 101 | [Fact] 102 | public void Intersects_WithBoundingSphere_ReturnsTrue() 103 | { 104 | var box = new BoundingBox(new Vector3d(0, 0, 0), new Vector3d(4, 4, 4)); 105 | var sphere = new BoundingSphere(new Vector3d(1, 1, 1), Fixed64.One); 106 | 107 | Assert.True(box.Intersects(sphere)); 108 | } 109 | 110 | #endregion 111 | 112 | #region Test: Surface Distance and Closest Point 113 | 114 | [Fact] 115 | public void DistanceToSurface_WorksCorrectly() 116 | { 117 | var box = new BoundingBox(new Vector3d(0, 0, 0), new Vector3d(4, 4, 4)); 118 | var point = new Vector3d(3, 0, 0); 119 | 120 | Assert.Equal(Fixed64.One, box.DistanceToSurface(point)); 121 | } 122 | 123 | [Fact] 124 | public void ClosestPointOnSurface_ReturnsCorrectPoint() 125 | { 126 | var box = new BoundingBox(new Vector3d(0, 0, 0), new Vector3d(4, 4, 4)); 127 | var point = new Vector3d(3, 3, 3); 128 | 129 | var closestPoint = box.ClosestPointOnSurface(point); 130 | Assert.Equal(new Vector3d(2, 2, 2), closestPoint); 131 | } 132 | 133 | #endregion 134 | 135 | #region Test: Equality 136 | 137 | [Fact] 138 | public void Equality_SameBox_ReturnsTrue() 139 | { 140 | var box1 = new BoundingBox(new Vector3d(0, 0, 0), new Vector3d(4, 4, 4)); 141 | var box2 = new BoundingBox(new Vector3d(0, 0, 0), new Vector3d(4, 4, 4)); 142 | 143 | Assert.True(box1 == box2); 144 | } 145 | 146 | [Fact] 147 | public void Equality_DifferentBox_ReturnsFalse() 148 | { 149 | var box1 = new BoundingBox(new Vector3d(0, 0, 0), new Vector3d(4, 4, 4)); 150 | var box2 = new BoundingBox(new Vector3d(1, 1, 1), new Vector3d(4, 4, 4)); 151 | 152 | Assert.False(box1 == box2); 153 | } 154 | 155 | [Fact] 156 | public void GetHashCode_SameBox_ReturnsSameHash() 157 | { 158 | var box1 = new BoundingBox(new Vector3d(0, 0, 0), new Vector3d(4, 4, 4)); 159 | var box2 = new BoundingBox(new Vector3d(0, 0, 0), new Vector3d(4, 4, 4)); 160 | 161 | Assert.Equal(box1.GetHashCode(), box2.GetHashCode()); 162 | } 163 | 164 | #endregion 165 | 166 | #region Test: Edge Cases 167 | 168 | [Fact] 169 | public void Contains_ZeroSizeBox_ContainsPoint() 170 | { 171 | var box = new BoundingBox(new Vector3d(0, 0, 0), new Vector3d(0, 0, 0)); 172 | var point = new Vector3d(0, 0, 0); 173 | 174 | Assert.True(box.Contains(point)); 175 | } 176 | 177 | [Fact] 178 | public void Intersects_ZeroSizeBox_ReturnsFalseForNonOverlapping() 179 | { 180 | var box1 = new BoundingBox(new Vector3d(0, 0, 0), new Vector3d(0, 0, 0)); 181 | var box2 = new BoundingBox(new Vector3d(1, 1, 1), new Vector3d(2, 2, 2)); 182 | 183 | Assert.False(box1.Intersects(box2)); 184 | } 185 | 186 | #endregion 187 | 188 | #region Test: Serialization 189 | 190 | [Fact] 191 | public void BoundingBox_NetSerialization_RoundTripMaintainsData() 192 | { 193 | BoundingBox originalValue = new(new Vector3d(0, 0, 0), new Vector3d(4, 4, 4)); 194 | 195 | // Serialize the BoundingArea object 196 | #if NET48_OR_GREATER 197 | var formatter = new BinaryFormatter(); 198 | using var stream = new MemoryStream(); 199 | formatter.Serialize(stream, originalValue); 200 | 201 | // Reset stream position and deserialize 202 | stream.Seek(0, SeekOrigin.Begin); 203 | var deserializedValue = (BoundingBox)formatter.Deserialize(stream); 204 | #endif 205 | 206 | #if NET8_0_OR_GREATER 207 | var jsonOptions = new JsonSerializerOptions 208 | { 209 | DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, 210 | ReferenceHandler = ReferenceHandler.IgnoreCycles, 211 | IncludeFields = true, 212 | IgnoreReadOnlyProperties = true 213 | }; 214 | var json = JsonSerializer.SerializeToUtf8Bytes(originalValue, jsonOptions); 215 | var deserializedValue = JsonSerializer.Deserialize(json, jsonOptions); 216 | #endif 217 | 218 | // Check that deserialized values match the original 219 | Assert.Equal(originalValue, deserializedValue); 220 | } 221 | 222 | [Fact] 223 | public void BoundingBox_MsgPackSerialization_RoundTripMaintainsData() 224 | { 225 | BoundingBox originalValue = new(new Vector3d(0, 0, 0), new Vector3d(4, 4, 4)); 226 | 227 | byte[] bytes = MessagePackSerializer.Serialize(originalValue); 228 | BoundingBox deserializedValue = MessagePackSerializer.Deserialize(bytes); 229 | 230 | // Check that deserialized values match the original 231 | Assert.Equal(originalValue, deserializedValue); 232 | } 233 | 234 | #endregion 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /tests/FixedMathSharp.Tests/FixedRange.Tests.cs: -------------------------------------------------------------------------------- 1 | using MessagePack; 2 | 3 | #if NET48_OR_GREATER 4 | using System.IO; 5 | using System.Runtime.Serialization.Formatters.Binary; 6 | #endif 7 | 8 | #if NET8_0_OR_GREATER 9 | using System.Text.Json; 10 | using System.Text.Json.Serialization; 11 | #endif 12 | 13 | using Xunit; 14 | 15 | namespace FixedMathSharp.Tests 16 | { 17 | public class FixedRangeTests 18 | { 19 | [Fact] 20 | public void FixedRange_InitializesCorrectly() 21 | { 22 | var min = new Fixed64(-10); 23 | var max = new Fixed64(10); 24 | var range = new FixedRange(min, max); 25 | 26 | Assert.Equal(min, range.Min); 27 | Assert.Equal(max, range.Max); 28 | } 29 | 30 | [Fact] 31 | public void FixedRange_Constructor_EnforcesOrder() 32 | { 33 | var range = new FixedRange(new Fixed64(10), new Fixed64(-10)); 34 | 35 | Assert.Equal(new Fixed64(-10), range.Min); 36 | Assert.Equal(new Fixed64(10), range.Max); 37 | } 38 | 39 | [Fact] 40 | public void FixedRange_Length_ComputesCorrectly() 41 | { 42 | var range = new FixedRange(new Fixed64(-5), new Fixed64(15)); 43 | Assert.Equal(new Fixed64(20), range.Length); // Length = 15 - (-5) = 20 44 | } 45 | 46 | [Fact] 47 | public void FixedRange_MidPoint_ComputesCorrectly() 48 | { 49 | var range = new FixedRange(new Fixed64(-5), new Fixed64(15)); 50 | Assert.Equal(new Fixed64(5), range.MidPoint); // Midpoint = (-5 + 15) / 2 = 5 51 | } 52 | 53 | [Fact] 54 | public void FixedRange_InRange_Fixed64Value_ReturnsTrue() 55 | { 56 | var range = new FixedRange(new Fixed64(0), new Fixed64(10)); 57 | Assert.True(range.InRange(new Fixed64(5))); // 5 is in [0, 10) 58 | } 59 | 60 | [Fact] 61 | public void FixedRange_InRange_Fixed64Value_ReturnsFalse() 62 | { 63 | var range = new FixedRange(new Fixed64(0), new Fixed64(10)); 64 | Assert.False(range.InRange(new Fixed64(10))); // 10 is not included in [0, 10) 65 | } 66 | 67 | [Fact] 68 | public void FixedRange_InRange_IntValue_ReturnsTrue() 69 | { 70 | var range = new FixedRange(new Fixed64(0), new Fixed64(10)); 71 | Assert.True(range.InRange((Fixed64)5)); // 5 is in [0, 10) 72 | } 73 | 74 | [Fact] 75 | public void FixedRange_InRange_FloatValue_ReturnsTrue() 76 | { 77 | var range = new FixedRange(new Fixed64(0), new Fixed64(10)); 78 | Assert.True(range.InRange((Fixed64)5.5f)); // 5.5 is in [0, 10) 79 | } 80 | 81 | [Fact] 82 | public void FixedRange_Overlaps_ReturnsTrue() 83 | { 84 | var range1 = new FixedRange(new Fixed64(0), new Fixed64(10)); 85 | var range2 = new FixedRange(new Fixed64(5), new Fixed64(15)); 86 | Assert.True(range1.Overlaps(range2)); // Ranges [0, 10) and [5, 15) overlap 87 | } 88 | 89 | [Fact] 90 | public void FixedRange_Overlaps_ReturnsFalse() 91 | { 92 | var range1 = new FixedRange(new Fixed64(0), new Fixed64(10)); 93 | var range2 = new FixedRange(new Fixed64(10), new Fixed64(20)); 94 | Assert.False(range1.Overlaps(range2)); // No overlap between [0, 10) and [10, 20) 95 | } 96 | 97 | [Fact] 98 | public void FixedRange_AddInPlace_AddsToRangeCorrectly() 99 | { 100 | var range = new FixedRange(new Fixed64(0), new Fixed64(10)); 101 | range.AddInPlace(new Fixed64(5)); 102 | 103 | Assert.Equal(new Fixed64(5), range.Min); 104 | Assert.Equal(new Fixed64(15), range.Max); // Range should now be [5, 15) 105 | } 106 | 107 | [Fact] 108 | public void FixedRange_GetDirection_ReturnsCorrectDirection() 109 | { 110 | var range1 = new FixedRange(new Fixed64(0), new Fixed64(5)); 111 | var range2 = new FixedRange(new Fixed64(10), new Fixed64(15)); 112 | 113 | Fixed64? sign; 114 | var result = FixedRange.GetDirection(range1, range2, out sign); 115 | Assert.True(result); 116 | Assert.Equal(-Fixed64.One, sign); // range1 is to the left of range2 117 | } 118 | 119 | [Fact] 120 | public void FixedRange_ComputeOverlapDepth_ComputesCorrectly() 121 | { 122 | var range1 = new FixedRange(new Fixed64(0), new Fixed64(10)); 123 | var range2 = new FixedRange(new Fixed64(5), new Fixed64(15)); 124 | 125 | var overlapDepth = FixedRange.ComputeOverlapDepth(range1, range2); 126 | Assert.Equal(new Fixed64(5), overlapDepth); // Overlap depth is 5 127 | } 128 | 129 | [Fact] 130 | public void FixedRange_EqualMinMax_DoesNotOverlapWithAnyOtherRange() 131 | { 132 | var pointRange = new FixedRange(new Fixed64(5), new Fixed64(5)); // Zero-length range at 5 133 | var range = new FixedRange(new Fixed64(0), new Fixed64(4)); // Does not contain 5 134 | 135 | Assert.False(pointRange.Overlaps(range)); // Point range (5-5) should not overlap with (0-4) 136 | } 137 | 138 | [Fact] 139 | public void FixedRange_EqualMinMax_ContainedInLargerRange() 140 | { 141 | var range1 = new FixedRange(new Fixed64(5), new Fixed64(5)); // Zero-length range 142 | var range2 = new FixedRange(new Fixed64(0), new Fixed64(10)); 143 | 144 | Assert.True(range2.Overlaps(range1)); // Zero-length range (5-5) is contained within (0-10) 145 | } 146 | 147 | 148 | [Fact] 149 | public void FixedRange_SmallRanges_OverlapCorrectly() 150 | { 151 | var smallRange1 = new FixedRange(new Fixed64(0.0001), new Fixed64(0.0002)); 152 | var smallRange2 = new FixedRange(new Fixed64(0.00015), new Fixed64(0.00025)); 153 | 154 | Assert.True(smallRange1.Overlaps(smallRange2)); 155 | } 156 | 157 | 158 | [Fact] 159 | public void FixedRange_LargeRanges_OverlapCorrectly() 160 | { 161 | var largeRange1 = new FixedRange(new Fixed64(long.MinValue), new Fixed64(0)); 162 | var largeRange2 = new FixedRange(new Fixed64(-1000), new Fixed64(long.MaxValue)); 163 | 164 | Assert.True(largeRange1.Overlaps(largeRange2)); 165 | } 166 | 167 | #region Test: Serialization 168 | 169 | 170 | [Fact] 171 | public void FixedRange_NetSerialization_RoundTripMaintainsData() 172 | { 173 | var originalRange = new FixedRange(new Fixed64(-10), new Fixed64(10)); 174 | 175 | // Serialize the FixedRange object 176 | #if NET48_OR_GREATER 177 | var formatter = new BinaryFormatter(); 178 | using var stream = new MemoryStream(); 179 | formatter.Serialize(stream, originalRange); 180 | 181 | // Reset stream position and deserialize 182 | stream.Seek(0, SeekOrigin.Begin); 183 | var deserializedRange = (FixedRange)formatter.Deserialize(stream); 184 | #endif 185 | 186 | #if NET8_0_OR_GREATER 187 | var jsonOptions = new JsonSerializerOptions 188 | { 189 | DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, 190 | ReferenceHandler = ReferenceHandler.IgnoreCycles, 191 | IncludeFields = true, 192 | IgnoreReadOnlyProperties = true 193 | }; 194 | var json = JsonSerializer.SerializeToUtf8Bytes(originalRange, jsonOptions); 195 | var deserializedRange = JsonSerializer.Deserialize(json, jsonOptions); 196 | #endif 197 | 198 | // Check that deserialized values match the original 199 | Assert.Equal(originalRange.Min, deserializedRange.Min); 200 | Assert.Equal(originalRange.Max, deserializedRange.Max); 201 | } 202 | 203 | [Fact] 204 | public void FixedRange_MsgPackSerialization_RoundTripMaintainsData() 205 | { 206 | FixedRange originalValue = new FixedRange(new Fixed64(-10), new Fixed64(10)); 207 | 208 | byte[] bytes = MessagePackSerializer.Serialize(originalValue); 209 | FixedRange deserializedValue = MessagePackSerializer.Deserialize(bytes); 210 | 211 | // Check that deserialized values match the original 212 | Assert.Equal(originalValue, deserializedValue); 213 | } 214 | 215 | #endregion 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /tests/FixedMathSharp.Tests/Bounds/BoundingSphere.Tests.cs: -------------------------------------------------------------------------------- 1 | using MessagePack; 2 | 3 | #if NET48_OR_GREATER 4 | using System.IO; 5 | using System.Runtime.Serialization.Formatters.Binary; 6 | #endif 7 | 8 | #if NET8_0_OR_GREATER 9 | using System.Text.Json; 10 | using System.Text.Json.Serialization; 11 | #endif 12 | 13 | using Xunit; 14 | 15 | namespace FixedMathSharp.Tests.Bounds 16 | { 17 | public class BoundingSphereTests 18 | { 19 | #region Test: Constructor and Property 20 | 21 | [Fact] 22 | public void Constructor_AssignsValuesCorrectly() 23 | { 24 | var center = new Vector3d(1, 2, 3); 25 | var radius = new Fixed64(5); 26 | 27 | var sphere = new BoundingSphere(center, radius); 28 | 29 | Assert.Equal(center, sphere.Center); 30 | Assert.Equal(radius, sphere.Radius); 31 | Assert.Equal(new Vector3d(-4, -3, -2), sphere.Min); 32 | Assert.Equal(new Vector3d(6, 7, 8), sphere.Max); 33 | } 34 | 35 | #endregion 36 | 37 | #region Test: Containment 38 | 39 | [Fact] 40 | public void Contains_PointInside_ReturnsTrue() 41 | { 42 | var sphere = new BoundingSphere(new Vector3d(0, 0, 0), new Fixed64(5)); 43 | var point = new Vector3d(2, 2, 2); 44 | 45 | Assert.True(sphere.Contains(point)); 46 | } 47 | 48 | [Fact] 49 | public void Contains_PointOutside_ReturnsFalse() 50 | { 51 | var sphere = new BoundingSphere(new Vector3d(0, 0, 0), new Fixed64(5)); 52 | var point = new Vector3d(6, 0, 0); 53 | 54 | Assert.False(sphere.Contains(point)); 55 | } 56 | 57 | [Fact] 58 | public void Contains_PointOnSurface_ReturnsTrue() 59 | { 60 | var sphere = new BoundingSphere(new Vector3d(0, 0, 0), new Fixed64(5)); 61 | var point = new Vector3d(5, 0, 0); 62 | 63 | Assert.True(sphere.Contains(point)); 64 | } 65 | 66 | #endregion 67 | 68 | #region Test: Intersection 69 | 70 | [Fact] 71 | public void Intersects_WithOverlappingSphere_ReturnsTrue() 72 | { 73 | var sphere1 = new BoundingSphere(new Vector3d(0, 0, 0), new Fixed64(5)); 74 | var sphere2 = new BoundingSphere(new Vector3d(3, 0, 0), new Fixed64(4)); 75 | 76 | Assert.True(sphere1.Intersects(sphere2)); 77 | } 78 | 79 | [Fact] 80 | public void Intersects_WithNonOverlappingSphere_ReturnsFalse() 81 | { 82 | var sphere1 = new BoundingSphere(new Vector3d(0, 0, 0), new Fixed64(5)); 83 | var sphere2 = new BoundingSphere(new Vector3d(11, 0, 0), new Fixed64(4)); 84 | 85 | Assert.False(sphere1.Intersects(sphere2)); 86 | } 87 | 88 | [Fact] 89 | public void Intersects_WithBoundingBox_ReturnsTrue() 90 | { 91 | var sphere = new BoundingSphere(new Vector3d(0, 0, 0), new Fixed64(5)); 92 | var box = new BoundingBox(new Vector3d(-3, -3, -3), new Vector3d(3, 3, 3)); 93 | 94 | Assert.True(sphere.Intersects(box)); 95 | } 96 | 97 | [Fact] 98 | public void Intersects_WithBoundingArea_ReturnsTrue() 99 | { 100 | var sphere = new BoundingSphere(new Vector3d(2, 2, 0), new Fixed64(3)); 101 | var area = new BoundingArea(new Vector3d(0, 0, 0), new Vector3d(4, 4, 0)); 102 | 103 | Assert.True(sphere.Intersects(area)); 104 | } 105 | 106 | #endregion 107 | 108 | #region Test: Distance to Surface 109 | 110 | [Fact] 111 | public void DistanceToSurface_PointOutside_ReturnsPositiveDistance() 112 | { 113 | var sphere = new BoundingSphere(new Vector3d(0, 0, 0), new Fixed64(5)); 114 | var point = new Vector3d(10, 0, 0); 115 | 116 | Assert.Equal(new Fixed64(5), sphere.DistanceToSurface(point)); 117 | } 118 | 119 | [Fact] 120 | public void DistanceToSurface_PointOnSurface_ReturnsZero() 121 | { 122 | var sphere = new BoundingSphere(new Vector3d(0, 0, 0), new Fixed64(5)); 123 | var point = new Vector3d(5, 0, 0); 124 | 125 | Assert.Equal(Fixed64.Zero, sphere.DistanceToSurface(point)); 126 | } 127 | 128 | [Fact] 129 | public void DistanceToSurface_PointInside_ReturnsNegativeDistance() 130 | { 131 | var sphere = new BoundingSphere(new Vector3d(0, 0, 0), new Fixed64(5)); 132 | var point = new Vector3d(3, 0, 0); 133 | 134 | Assert.Equal(new Fixed64(-2), sphere.DistanceToSurface(point)); 135 | } 136 | 137 | #endregion 138 | 139 | #region Test: Equality 140 | 141 | [Fact] 142 | public void Equality_SameValues_ReturnsTrue() 143 | { 144 | var sphere1 = new BoundingSphere(new Vector3d(0, 0, 0), new Fixed64(5)); 145 | var sphere2 = new BoundingSphere(new Vector3d(0, 0, 0), new Fixed64(5)); 146 | 147 | Assert.True(sphere1 == sphere2); 148 | } 149 | 150 | [Fact] 151 | public void Equality_DifferentValues_ReturnsFalse() 152 | { 153 | var sphere1 = new BoundingSphere(new Vector3d(0, 0, 0), new Fixed64(5)); 154 | var sphere2 = new BoundingSphere(new Vector3d(1, 1, 1), new Fixed64(5)); 155 | 156 | Assert.False(sphere1 == sphere2); 157 | } 158 | 159 | [Fact] 160 | public void GetHashCode_SameValues_ReturnsSameHash() 161 | { 162 | var sphere1 = new BoundingSphere(new Vector3d(0, 0, 0), new Fixed64(5)); 163 | var sphere2 = new BoundingSphere(new Vector3d(0, 0, 0), new Fixed64(5)); 164 | 165 | Assert.Equal(sphere1.GetHashCode(), sphere2.GetHashCode()); 166 | } 167 | 168 | #endregion 169 | 170 | #region Test: Edge Cases 171 | 172 | [Fact] 173 | public void Intersects_ZeroRadiusSphere_ReturnsTrueForOverlapping() 174 | { 175 | var sphere1 = new BoundingSphere(new Vector3d(0, 0, 0), Fixed64.Zero); 176 | var sphere2 = new BoundingSphere(new Vector3d(1, 1, 1), new Fixed64(2)); 177 | 178 | // The distance between centers is less than or equal to the sum of radii, so they intersect 179 | Assert.True(sphere1.Intersects(sphere2)); 180 | } 181 | 182 | [Fact] 183 | public void DistanceToSurface_ZeroRadiusSphere_ReturnsDistanceToCenter() 184 | { 185 | var sphere = new BoundingSphere(new Vector3d(0, 0, 0), Fixed64.Zero); 186 | var point = new Vector3d(3, 0, 0); 187 | 188 | Assert.Equal(new Fixed64(3), sphere.DistanceToSurface(point)); 189 | } 190 | 191 | #endregion 192 | 193 | #region Test: Serialization 194 | 195 | [Fact] 196 | public void BoundingSphere_NetSerialization_RoundTripMaintainsData() 197 | { 198 | BoundingSphere originalValue = new(new Vector3d(1, 1, 1), new Fixed64(2)); 199 | 200 | // Serialize the BoundingArea object 201 | #if NET48_OR_GREATER 202 | var formatter = new BinaryFormatter(); 203 | using var stream = new MemoryStream(); 204 | formatter.Serialize(stream, originalValue); 205 | 206 | // Reset stream position and deserialize 207 | stream.Seek(0, SeekOrigin.Begin); 208 | var deserializedValue = (BoundingSphere)formatter.Deserialize(stream); 209 | #endif 210 | 211 | #if NET8_0_OR_GREATER 212 | var jsonOptions = new JsonSerializerOptions 213 | { 214 | DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, 215 | ReferenceHandler = ReferenceHandler.IgnoreCycles, 216 | IncludeFields = true, 217 | IgnoreReadOnlyProperties = true 218 | }; 219 | var json = JsonSerializer.SerializeToUtf8Bytes(originalValue, jsonOptions); 220 | var deserializedValue = JsonSerializer.Deserialize(json, jsonOptions); 221 | #endif 222 | 223 | // Check that deserialized values match the original 224 | Assert.Equal(originalValue, deserializedValue); 225 | } 226 | 227 | [Fact] 228 | public void BoundingSphere_MsgPackSerialization_RoundTripMaintainsData() 229 | { 230 | BoundingSphere originalValue = new(new Vector3d(1, 1, 1), new Fixed64(2)); 231 | 232 | byte[] bytes = MessagePackSerializer.Serialize(originalValue); 233 | BoundingSphere deserializedValue = MessagePackSerializer.Deserialize(bytes); 234 | 235 | // Check that deserialized values match the original 236 | Assert.Equal(originalValue, deserializedValue); 237 | } 238 | 239 | #endregion 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /src/FixedMathSharp/Numerics/Extensions/Fixed64.Extensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | using System.Threading.Tasks; 4 | using static System.Net.Mime.MediaTypeNames; 5 | 6 | namespace FixedMathSharp 7 | { 8 | public static class Fixed64Extensions 9 | { 10 | #region Fixed64 Operations 11 | 12 | /// 13 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 14 | public static int Sign(this Fixed64 value) 15 | { 16 | return Fixed64.Sign(value); 17 | } 18 | 19 | /// 20 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 21 | public static bool IsInteger(this Fixed64 value) 22 | { 23 | return Fixed64.IsInteger(value); 24 | } 25 | 26 | /// 27 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 28 | public static Fixed64 Squared(this Fixed64 value) 29 | { 30 | return FixedMath.Squared(value); 31 | } 32 | 33 | /// 34 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 35 | public static Fixed64 Round(this Fixed64 value, MidpointRounding mode = MidpointRounding.ToEven) 36 | { 37 | return FixedMath.Round(value, mode); 38 | } 39 | 40 | /// 41 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 42 | public static Fixed64 RoundToPrecision(this Fixed64 value, int places, MidpointRounding mode = MidpointRounding.ToEven) 43 | { 44 | return FixedMath.RoundToPrecision(value, places, mode); 45 | } 46 | 47 | /// 48 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 49 | public static Fixed64 ClampOne(this Fixed64 f1) 50 | { 51 | return FixedMath.ClampOne(f1); 52 | } 53 | 54 | /// 55 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 56 | public static Fixed64 Clamp01(this Fixed64 f1) 57 | { 58 | return FixedMath.Clamp01(f1); 59 | } 60 | 61 | /// 62 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 63 | public static Fixed64 Abs(this Fixed64 value) 64 | { 65 | return FixedMath.Abs(value); 66 | } 67 | 68 | /// 69 | /// Checks if the absolute value of x is less than y. 70 | /// 71 | /// The value to compare. 72 | /// The comparison threshold. 73 | /// True if |x| < y; otherwise false. 74 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 75 | public static bool AbsLessThan(this Fixed64 x, Fixed64 y) 76 | { 77 | return Abs(x) < y; 78 | } 79 | 80 | /// 81 | public static Fixed64 FastAdd(this Fixed64 a, Fixed64 b) 82 | { 83 | return FixedMath.FastAdd(a, b); 84 | } 85 | 86 | /// 87 | public static Fixed64 FastSub(this Fixed64 a, Fixed64 b) 88 | { 89 | return FixedMath.FastSub(a, b); 90 | } 91 | 92 | /// 93 | public static Fixed64 FastMul(this Fixed64 a, Fixed64 b) 94 | { 95 | return FixedMath.FastMul(a, b); 96 | } 97 | 98 | /// 99 | public static Fixed64 FastMod(this Fixed64 a, Fixed64 b) 100 | { 101 | return FixedMath.FastMod(a, b); 102 | } 103 | 104 | /// 105 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 106 | public static Fixed64 Floor(this Fixed64 value) 107 | { 108 | return FixedMath.Floor(value); 109 | } 110 | 111 | /// 112 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 113 | public static Fixed64 Ceiling(this Fixed64 value) 114 | { 115 | return FixedMath.Ceiling(value); 116 | } 117 | 118 | /// 119 | /// Rounds the Fixed64 value to the nearest integer. 120 | /// 121 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 122 | public static int RoundToInt(this Fixed64 x) 123 | { 124 | return (int)FixedMath.Round(x); 125 | } 126 | 127 | /// 128 | /// Rounds up the Fixed64 value to the nearest integer. 129 | /// 130 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 131 | public static int CeilToInt(this Fixed64 x) 132 | { 133 | return (int)FixedMath.Ceiling(x); 134 | } 135 | 136 | /// 137 | /// Rounds down the Fixed64 value to the nearest integer. 138 | /// 139 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 140 | public static int FloorToInt(this Fixed64 x) 141 | { 142 | return (int)Floor(x); 143 | } 144 | 145 | #endregion 146 | 147 | #region Conversion 148 | 149 | /// 150 | /// Converts the Fixed64 value to a string formatted to 2 decimal places. 151 | /// 152 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 153 | public static string ToFormattedString(this Fixed64 f1) 154 | { 155 | return f1.ToPreciseFloat().ToString("0.##"); 156 | } 157 | 158 | /// 159 | /// Converts the Fixed64 value to a double with specified decimal precision. 160 | /// 161 | /// The Fixed64 value to convert. 162 | /// The number of decimal places to round to. 163 | /// The formatted double value. 164 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 165 | public static double ToFormattedDouble(this Fixed64 f1, int precision = 2) 166 | { 167 | return Math.Round((double)f1, precision, MidpointRounding.AwayFromZero); 168 | } 169 | 170 | /// 171 | /// Converts the Fixed64 value to a float with 2 decimal points of precision. 172 | /// 173 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 174 | public static float ToFormattedFloat(this Fixed64 f1) 175 | { 176 | return (float)ToFormattedDouble(f1); 177 | } 178 | 179 | /// 180 | /// Converts the Fixed64 value to a precise float representation (without rounding). 181 | /// 182 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 183 | public static float ToPreciseFloat(this Fixed64 f1) 184 | { 185 | return (float)(double)f1; 186 | } 187 | 188 | /// 189 | /// Converts the angle in degrees to radians. 190 | /// 191 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 192 | public static Fixed64 ToRadians(this Fixed64 angleInDegrees) 193 | { 194 | return FixedMath.DegToRad(angleInDegrees); 195 | } 196 | 197 | /// 198 | /// Converts the angle in radians to degree. 199 | /// 200 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 201 | public static Fixed64 ToDegree(this Fixed64 angleInRadians) 202 | { 203 | return FixedMath.RadToDeg(angleInRadians); 204 | } 205 | 206 | #endregion 207 | 208 | #region Equality 209 | 210 | /// 211 | /// Checks if the value is greater than epsilon (positive or negative). 212 | /// Useful for determining if a value is effectively non-zero with a given precision. 213 | /// 214 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 215 | public static bool MoreThanEpsilon(this Fixed64 d) 216 | { 217 | return d.Abs() > Fixed64.Epsilon; 218 | } 219 | 220 | /// 221 | /// Checks if the value is less than epsilon (i.e., effectively zero). 222 | /// Useful for determining if a value is close enough to zero with a given precision. 223 | /// 224 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 225 | public static bool LessThanEpsilon(this Fixed64 d) 226 | { 227 | return d.Abs() < Fixed64.Epsilon; 228 | } 229 | 230 | /// 231 | /// Helper method to compare individual vector components for approximate equality, allowing a fractional difference. 232 | /// Handles zero components by only using the allowed percentage difference. 233 | /// 234 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 235 | public static bool FuzzyComponentEqual(this Fixed64 a, Fixed64 b, Fixed64 percentage) 236 | { 237 | var diff = (a - b).Abs(); 238 | var allowedErr = a.Abs() * percentage; 239 | // Compare directly to percentage if a is zero 240 | // Otherwise, use percentage of a's magnitude 241 | return a == Fixed64.Zero ? diff <= percentage : diff <= allowedErr; 242 | } 243 | 244 | #endregion 245 | } 246 | } -------------------------------------------------------------------------------- /src/FixedMathSharp/Bounds/BoundingArea.cs: -------------------------------------------------------------------------------- 1 | using MessagePack; 2 | using System; 3 | using System.Runtime.CompilerServices; 4 | 5 | namespace FixedMathSharp 6 | { 7 | /// 8 | /// Represents a lightweight, axis-aligned bounding area with fixed-point precision, optimized for 2D or simplified 3D use cases. 9 | /// 10 | /// 11 | /// The BoundingArea is designed for performance-critical scenarios where only a minimal bounding volume is required. 12 | /// It offers fast containment and intersection checks with other bounds but lacks the full feature set of BoundingBox. 13 | /// 14 | /// Use Cases: 15 | /// - Efficient spatial queries in 2D or constrained 3D spaces (e.g., terrain maps or collision grids). 16 | /// - Simplified bounding volume checks where rotation or complex shape fitting is not needed. 17 | /// - Can be used as a broad-phase bounding volume to cull objects before more precise checks with BoundingBox or BoundingSphere. 18 | /// 19 | 20 | [Serializable] 21 | [MessagePackObject] 22 | public struct BoundingArea : IBound, IEquatable 23 | { 24 | #region Fields 25 | 26 | /// 27 | /// One of the corner points of the bounding area. 28 | /// 29 | [Key(0)] 30 | public Vector3d Corner1; 31 | 32 | /// 33 | /// The opposite corner point of the bounding area. 34 | /// 35 | [Key(1)] 36 | public Vector3d Corner2; 37 | 38 | #endregion 39 | 40 | #region Constructors 41 | 42 | /// 43 | /// Initializes a new instance of the BoundingArea struct with corner coordinates. 44 | /// 45 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 46 | public BoundingArea(Fixed64 c1x, Fixed64 c1y, Fixed64 c1z, Fixed64 c2x, Fixed64 c2y, Fixed64 c2z) 47 | { 48 | Corner1 = new Vector3d(c1x, c1y, c1z); 49 | Corner2 = new Vector3d(c2x, c2y, c2z); 50 | } 51 | 52 | /// 53 | /// Initializes a new instance of the BoundingArea struct with two corner points. 54 | /// 55 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 56 | public BoundingArea(Vector3d corner1, Vector3d corner2) 57 | { 58 | Corner1 = corner1; 59 | Corner2 = corner2; 60 | } 61 | 62 | #endregion 63 | 64 | #region Properties and Methods (Instance) 65 | 66 | // Min/Max properties for easy access to boundaries 67 | 68 | /// 69 | /// The minimum corner of the bounding box. 70 | /// 71 | [IgnoreMember] 72 | public Vector3d Min 73 | { 74 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 75 | get => new(MinX, MinY, MinZ); 76 | } 77 | 78 | /// 79 | /// The maximum corner of the bounding box. 80 | /// 81 | [IgnoreMember] 82 | public Vector3d Max 83 | { 84 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 85 | get => new(MaxX, MaxY, MaxZ); 86 | } 87 | 88 | [IgnoreMember] 89 | public Fixed64 MinX 90 | { 91 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 92 | get => Corner1.x < Corner2.x ? Corner1.x : Corner2.x; 93 | } 94 | 95 | [IgnoreMember] 96 | public Fixed64 MaxX 97 | { 98 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 99 | get => Corner1.x > Corner2.x ? Corner1.x : Corner2.x; 100 | } 101 | 102 | [IgnoreMember] 103 | public Fixed64 MinY 104 | { 105 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 106 | get => Corner1.y < Corner2.y ? Corner1.y : Corner2.y; 107 | } 108 | 109 | [IgnoreMember] 110 | public Fixed64 MaxY 111 | { 112 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 113 | get => Corner1.y > Corner2.y ? Corner1.y : Corner2.y; 114 | } 115 | 116 | [IgnoreMember] 117 | public Fixed64 MinZ 118 | { 119 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 120 | get => Corner1.z < Corner2.z ? Corner1.z : Corner2.z; 121 | } 122 | 123 | [IgnoreMember] 124 | public Fixed64 MaxZ 125 | { 126 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 127 | get => Corner1.z > Corner2.z ? Corner1.z : Corner2.z; 128 | } 129 | 130 | /// 131 | /// Calculates the width (X-axis) of the bounding area. 132 | /// 133 | [IgnoreMember] 134 | public Fixed64 Width 135 | { 136 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 137 | get => MaxX - MinX; 138 | } 139 | 140 | /// 141 | /// Calculates the height (Y-axis) of the bounding area. 142 | /// 143 | [IgnoreMember] 144 | public Fixed64 Height 145 | { 146 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 147 | get => MaxY - MinY; 148 | } 149 | 150 | /// 151 | /// Calculates the depth (Z-axis) of the bounding area. 152 | /// 153 | [IgnoreMember] 154 | public Fixed64 Depth 155 | { 156 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 157 | get => MaxZ - MinZ; 158 | } 159 | 160 | /// 161 | /// Determines if a point is inside the bounding area (including boundaries). 162 | /// 163 | public bool Contains(Vector3d point) 164 | { 165 | // Check if the point is within the bounds of the area (including boundaries) 166 | return point.x >= MinX && point.x <= MaxX 167 | && point.y >= MinY && point.y <= MaxY 168 | && point.z >= MinZ && point.z <= MaxZ; 169 | } 170 | 171 | /// 172 | /// Checks if another IBound intersects with this bounding area. 173 | /// 174 | /// 175 | /// It checks for overlap on all axes. If there is no overlap on any axis, they do not intersect. 176 | /// 177 | public bool Intersects(IBound other) 178 | { 179 | switch (other) 180 | { 181 | case BoundingBox or BoundingArea: 182 | { 183 | if (Contains(other.Min) && Contains(other.Max)) 184 | return true; // Full containment 185 | 186 | // Determine which axis is "flat" (thickness zero) 187 | bool flatX = Min.x == Max.x && other.Min.x == other.Max.x; 188 | bool flatY = Min.y == Max.y && other.Min.y == other.Max.y; 189 | bool flatZ = Min.z == Max.z && other.Min.z == other.Max.z; 190 | 191 | if (flatZ) // Rectangle in XY 192 | return !(Max.x < other.Min.x || Min.x > other.Max.x || 193 | Max.y < other.Min.y || Min.y > other.Max.y); 194 | else if (flatY) // Rectangle in XZ 195 | return !(Max.x < other.Min.x || Min.x > other.Max.x || 196 | Max.z < other.Min.z || Min.z > other.Max.z); 197 | else if (flatX) // Rectangle in YZ 198 | return !(Max.y < other.Min.y || Min.y > other.Max.y || 199 | Max.z < other.Min.z || Min.z > other.Max.z); 200 | else // fallback to 3D volume logic 201 | return !(Max.x < other.Min.x || Min.x > other.Max.x || 202 | Max.y < other.Min.y || Min.y > other.Max.y || 203 | Max.z < other.Min.z || Min.z > other.Max.z); 204 | } 205 | case BoundingSphere sphere: 206 | // Find the closest point on the area to the sphere's center 207 | // Intersection occurs if the distance from the closest point to the sphere’s center is within the radius. 208 | return Vector3d.SqrDistance(sphere.Center, this.ProjectPointWithinBounds(sphere.Center)) <= sphere.SqrRadius; 209 | 210 | default: return false; // Default case for unknown or unsupported types 211 | }; 212 | } 213 | 214 | /// 215 | /// Projects a point onto the bounding box. If the point is outside the box, it returns the closest point on the surface. 216 | /// 217 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 218 | public Vector3d ProjectPoint(Vector3d point) 219 | { 220 | return this.ProjectPointWithinBounds(point); 221 | } 222 | 223 | #endregion 224 | 225 | #region Operators 226 | 227 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 228 | public static bool operator ==(BoundingArea left, BoundingArea right) => left.Equals(right); 229 | 230 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 231 | public static bool operator !=(BoundingArea left, BoundingArea right) => !left.Equals(right); 232 | 233 | #endregion 234 | 235 | #region Equality and HashCode Overrides 236 | 237 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 238 | public override bool Equals(object? obj) => obj is BoundingArea other && Equals(other); 239 | 240 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 241 | public bool Equals(BoundingArea other) => Corner1.Equals(other.Corner1) && Corner2.Equals(other.Corner2); 242 | 243 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 244 | public override int GetHashCode() 245 | { 246 | unchecked 247 | { 248 | int hash = 17; 249 | hash = hash * 23 + Corner1.GetHashCode(); 250 | hash = hash * 23 + Corner2.GetHashCode(); 251 | return hash; 252 | } 253 | } 254 | 255 | #endregion 256 | } 257 | } -------------------------------------------------------------------------------- /src/FixedMathSharp/Numerics/FixedRange.cs: -------------------------------------------------------------------------------- 1 | using MessagePack; 2 | using System; 3 | using System.Runtime.CompilerServices; 4 | 5 | namespace FixedMathSharp 6 | { 7 | /// 8 | /// Represents a range of values with fixed precision. 9 | /// 10 | [Serializable] 11 | [MessagePackObject] 12 | public struct FixedRange : IEquatable 13 | { 14 | #region Constants 15 | 16 | /// 17 | /// The smallest possible range. 18 | /// 19 | public static readonly FixedRange MinRange = new FixedRange(Fixed64.MIN_VALUE, Fixed64.MIN_VALUE); 20 | 21 | /// 22 | /// The largest possible range. 23 | /// 24 | public static readonly FixedRange MaxRange = new FixedRange(Fixed64.MAX_VALUE, Fixed64.MAX_VALUE); 25 | 26 | #endregion 27 | 28 | #region Fields 29 | 30 | /// 31 | /// Gets the minimum value of the range. 32 | /// 33 | [Key(0)] 34 | public Fixed64 Min; 35 | 36 | /// 37 | /// Gets the maximum value of the range. 38 | /// 39 | [Key(1)] 40 | public Fixed64 Max; 41 | 42 | #endregion 43 | 44 | #region Constructors 45 | 46 | /// 47 | /// Initializes a new instance of the FixedRange structure with the specified minimum and maximum values. 48 | /// 49 | /// The minimum value of the range. 50 | /// The maximum value of the range. 51 | /// If true, ensures that Min is less than or equal to Max. 52 | 53 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 54 | public FixedRange(Fixed64 min, Fixed64 max, bool enforceOrder = true) 55 | { 56 | if (enforceOrder) 57 | { 58 | Min = min < max ? min : max; 59 | Max = min < max ? max : min; 60 | } 61 | else 62 | { 63 | Min = min; 64 | Max = max; 65 | } 66 | } 67 | 68 | #endregion 69 | 70 | #region Properties and Methods (Instance) 71 | 72 | /// 73 | /// The length of the range, computed as Max - Min. 74 | /// 75 | [IgnoreMember] 76 | public Fixed64 Length 77 | { 78 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 79 | get => Max - Min; 80 | } 81 | 82 | /// 83 | /// The midpoint of the range. 84 | /// 85 | [IgnoreMember] 86 | public Fixed64 MidPoint 87 | { 88 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 89 | get => (Min + Max) * Fixed64.Half; 90 | } 91 | 92 | /// 93 | /// Sets the minimum and maximum values for the range. 94 | /// 95 | /// The new minimum value. 96 | /// The new maximum value. 97 | public void SetMinMax(Fixed64 min, Fixed64 max) 98 | { 99 | Min = min; 100 | Max = max; 101 | } 102 | 103 | /// 104 | /// Adds a value to both the minimum and maximum of the range. 105 | /// 106 | /// The value to add. 107 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 108 | public void AddInPlace(Fixed64 val) 109 | { 110 | Min += val; 111 | Max += val; 112 | } 113 | 114 | /// 115 | /// Determines whether the specified value is within the range, with an option to include or exclude the upper bound. 116 | /// 117 | /// The value to check. 118 | /// If true, the upper bound (Max) is included in the range check; otherwise, the upper bound is exclusive. Default is false (exclusive). 119 | /// True if the value is within the range; otherwise, false. 120 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 121 | public bool InRange(Fixed64 x, bool includeMax = false) 122 | { 123 | return includeMax ? x >= Min && x <= Max : x >= Min && x < Max; 124 | } 125 | 126 | /// 127 | public bool InRange(double x, bool includeMax = false) 128 | { 129 | long xL = (long)Math.Round((double)x * FixedMath.ONE_L); 130 | return includeMax ? xL >= Min.m_rawValue && xL <= Max.m_rawValue : xL >= Min.m_rawValue && xL < Max.m_rawValue; 131 | } 132 | 133 | /// 134 | /// Checks whether this range overlaps with the specified range, ensuring no adjacent edges are considered overlaps. 135 | /// 136 | /// The range to compare. 137 | /// True if the ranges overlap; otherwise, false. 138 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 139 | public bool Overlaps(FixedRange other) 140 | { 141 | return Min < other.Max && Max > other.Min; 142 | } 143 | 144 | #endregion 145 | 146 | #region Range Operations 147 | 148 | /// 149 | /// Determines the direction from one range to another. 150 | /// If they don't overlap, returns -1 or 1 depending on the relative position. 151 | /// 152 | /// The first range. 153 | /// The second range. 154 | /// The direction between ranges (-1 or 1). 155 | /// True if the ranges don't overlap, false if they do. 156 | public static bool GetDirection(FixedRange range1, FixedRange range2, out Fixed64? sign) 157 | { 158 | sign = null; 159 | if (!range1.Overlaps(range2)) 160 | { 161 | if (range1.Max < range2.Min) sign = -Fixed64.One; 162 | else sign = Fixed64.One; 163 | return true; 164 | } 165 | return false; 166 | } 167 | 168 | /// 169 | /// Calculates the overlap depth between two ranges. 170 | /// Assumes the ranges are sorted (min and max are correctly assigned). 171 | /// 172 | /// The first range. 173 | /// The second range. 174 | /// The depth of the overlap between the ranges. 175 | public static Fixed64 ComputeOverlapDepth(FixedRange rangeA, FixedRange rangeB) 176 | { 177 | // Check if one range is completely within the other 178 | bool isRangeAInsideB = rangeA.Min >= rangeB.Min && rangeA.Max <= rangeB.Max; 179 | bool isRangeBInsideA = rangeB.Min >= rangeA.Min && rangeB.Max <= rangeA.Max; 180 | if (isRangeAInsideB) 181 | return rangeA.Max - rangeB.Min; // The size of rangeA 182 | else if (isRangeBInsideA) 183 | return rangeB.Max - rangeA.Min; // The size of rangeB 184 | 185 | // Calculate overlap between the two ranges 186 | Fixed64 overlapEnd = FixedMath.Min(rangeA.Max, rangeB.Max); 187 | Fixed64 overlapStart = FixedMath.Max(rangeA.Min, rangeB.Min); 188 | Fixed64 overlap = overlapEnd - overlapStart; 189 | 190 | return overlap > Fixed64.Zero ? overlap : Fixed64.Zero; 191 | } 192 | 193 | /// 194 | /// Checks for overlap between two ranges and calculates the vector of overlap depth. 195 | /// 196 | /// The origin vector. 197 | /// The first range. 198 | /// The second range. 199 | /// The overlap limit to check. 200 | /// The direction sign to consider. 201 | /// The overlap vector and depth, if any. 202 | /// True if overlap occurs and is below the limit, otherwise false. 203 | public static bool CheckOverlap(Vector3d origin, FixedRange range1, FixedRange range2, Fixed64 limit, Fixed64 sign, out (Vector3d Vector, Fixed64 Depth)? output) 204 | { 205 | output = null; 206 | Fixed64 overlap = ComputeOverlapDepth(range1, range2); 207 | 208 | // If the overlap is smaller than the current minimum, update the minimum 209 | if (overlap < limit) 210 | { 211 | output = (origin * overlap * sign, overlap); 212 | return true; 213 | } 214 | return false; 215 | } 216 | 217 | #endregion 218 | 219 | #region Operators 220 | 221 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 222 | public static FixedRange operator +(FixedRange left, FixedRange right) 223 | { 224 | return new FixedRange(left.Min + right.Min, left.Max + right.Max); 225 | } 226 | 227 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 228 | public static FixedRange operator -(FixedRange left, FixedRange right) 229 | { 230 | return new FixedRange(left.Min - right.Min, left.Max - right.Max); 231 | } 232 | 233 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 234 | public static bool operator ==(FixedRange left, FixedRange right) 235 | { 236 | return left.Equals(right); 237 | } 238 | 239 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 240 | public static bool operator !=(FixedRange left, FixedRange right) 241 | { 242 | return !left.Equals(right); 243 | } 244 | 245 | #endregion 246 | 247 | #region Conversion 248 | 249 | /// 250 | /// Returns a string that represents the FixedRange instance, formatted as "Min - Max". 251 | /// 252 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 253 | public override string ToString() 254 | { 255 | return $"{Min.ToFormattedDouble()} - {Max.ToFormattedDouble()}"; 256 | } 257 | 258 | #endregion 259 | 260 | #region Equality and HashCode Overrides 261 | 262 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 263 | public override bool Equals(object? obj) 264 | { 265 | return obj is FixedRange other && Equals(other); 266 | } 267 | 268 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 269 | public bool Equals(FixedRange other) 270 | { 271 | return other.Min == Min && other.Max == Max; 272 | } 273 | 274 | /// 275 | /// Computes the hash code for the FixedRange instance. 276 | /// 277 | /// The hash code of the range. 278 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 279 | public override int GetHashCode() 280 | { 281 | return Min.GetHashCode() ^ Max.GetHashCode(); 282 | } 283 | 284 | #endregion 285 | } 286 | } -------------------------------------------------------------------------------- /tests/FixedMathSharp.Tests/DeterministicRandom.Tests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Runtime.CompilerServices; 4 | using FixedMathSharp.Utility; 5 | using Xunit; 6 | 7 | namespace FixedMathSharp.Tests 8 | { 9 | public class DeterministicRandomTests 10 | { 11 | // Helper: pull a sequence from NextU64 to compare streams 12 | private static ulong[] U64Seq(DeterministicRandom rng, int count) 13 | { 14 | var arr = new ulong[count]; 15 | for (int i = 0; i < count; i++) arr[i] = rng.NextU64(); 16 | return arr; 17 | } 18 | 19 | // Helper: pull a sequence from Next(int) to compare streams 20 | private static int[] IntSeq(DeterministicRandom rng, int count, int min, int max) 21 | { 22 | var arr = new int[count]; 23 | for (int i = 0; i < count; i++) arr[i] = rng.Next(min, max); 24 | return arr; 25 | } 26 | 27 | [Fact] 28 | public void SameSeed_Yields_IdenticalSequences() 29 | { 30 | var a = new DeterministicRandom(123456789UL); 31 | var b = new DeterministicRandom(123456789UL); 32 | 33 | // Interleave various calls to ensure internal state advances identically 34 | Assert.Equal(a.NextU64(), b.NextU64()); 35 | Assert.Equal(a.Next(), b.Next()); 36 | Assert.Equal(a.Next(1000), b.Next(1000)); 37 | Assert.Equal(a.Next(-50, 50), b.Next(-50, 50)); 38 | Assert.Equal(a.NextDouble(), b.NextDouble(), 14); 39 | 40 | // Then compare a longer run of NextU64 to be extra sure 41 | var seqA = U64Seq(a, 32); 42 | var seqB = U64Seq(b, 32); 43 | Assert.Equal(seqA, seqB); 44 | } 45 | 46 | [Fact] 47 | public void DifferentSeeds_Yield_DifferentSequences() 48 | { 49 | var a = new DeterministicRandom(1UL); 50 | var b = new DeterministicRandom(2UL); 51 | 52 | // It's possible (but astronomically unlikely) that first value matches; check a window 53 | var seqA = U64Seq(a, 16); 54 | var seqB = U64Seq(b, 16); 55 | 56 | // Require at least one difference in the first 16 draws 57 | Assert.NotEqual(seqA, seqB); 58 | } 59 | 60 | [Fact] 61 | public void FromWorldFeature_IsStable_AndSeparatesByFeature_AndIndex() 62 | { 63 | ulong world = 0xDEADBEEFCAFEBABEUL; 64 | ulong featureOre = 0x4F5245UL; // 'ORE' 65 | ulong featureRiver = 0x524956UL; // 'RIV' 66 | 67 | // Stability: repeated construction yields same sequence 68 | var a1 = DeterministicRandom.FromWorldFeature(world, featureOre, index: 0); 69 | var a2 = DeterministicRandom.FromWorldFeature(world, featureOre, index: 0); 70 | Assert.Equal(U64Seq(a1, 8), U64Seq(a2, 8)); 71 | 72 | // Different featureKey -> different stream 73 | var b = DeterministicRandom.FromWorldFeature(world, featureRiver, index: 0); 74 | Assert.NotEqual(U64Seq(a1, 8), U64Seq(b, 8)); 75 | 76 | // Different index -> different stream 77 | var c = DeterministicRandom.FromWorldFeature(world, featureOre, index: 1); 78 | Assert.NotEqual(U64Seq(a1, 8), U64Seq(c, 8)); 79 | } 80 | 81 | [Fact] 82 | public void Next_NoArg_IsNonNegative_AndWithinRange() 83 | { 84 | var rng = new DeterministicRandom(42UL); 85 | for (int i = 0; i < 1000; i++) 86 | { 87 | int v = rng.Next(); 88 | Assert.InRange(v, 0, int.MaxValue); 89 | } 90 | } 91 | 92 | [Theory] 93 | [InlineData(1)] 94 | [InlineData(2)] 95 | [InlineData(7)] 96 | [InlineData(97)] 97 | [InlineData(int.MaxValue)] 98 | public void Next_MaxExclusive_RespectsBounds_AndThrows_OnInvalid(int maxExclusive) 99 | { 100 | var rng = new DeterministicRandom(9001UL); 101 | for (int i = 0; i < 4096; i++) 102 | { 103 | int v = rng.Next(maxExclusive); 104 | Assert.InRange(v, 0, maxExclusive - 1); 105 | } 106 | } 107 | 108 | [Fact] 109 | public void Next_MaxExclusive_Throws_WhenNonPositive() 110 | { 111 | var rng = new DeterministicRandom(1UL); 112 | Assert.Throws(() => rng.Next(0)); 113 | Assert.Throws(() => rng.Next(-5)); 114 | } 115 | 116 | [Fact] 117 | public void Next_MinMax_RespectsBounds_AndThrows_OnInvalidRange() 118 | { 119 | var rng = new DeterministicRandom(2024UL); 120 | 121 | for (int i = 0; i < 4096; i++) 122 | { 123 | int v = rng.Next(-10, 10); 124 | Assert.InRange(v, -10, 9); 125 | } 126 | 127 | Assert.Throws(() => rng.Next(5, 5)); 128 | Assert.Throws(() => rng.Next(10, -10)); 129 | Assert.Throws(() => rng.Next(10, 10)); 130 | } 131 | 132 | [Fact] 133 | public void NextDouble_IsInUnitInterval() 134 | { 135 | var rng = new DeterministicRandom(123UL); 136 | for (int i = 0; i < 4096; i++) 137 | { 138 | double d = rng.NextDouble(); 139 | Assert.True(d >= 0.0 && d < 1.0, "NextDouble() must be in [0,1)"); 140 | } 141 | } 142 | 143 | [Fact] 144 | public void NextBytes_FillsEntireBuffer_AndAdvancesState() 145 | { 146 | var rng = new DeterministicRandom(55555UL); 147 | 148 | // Exercise both the fast 8-byte path and the tail path 149 | var sizes = new[] { 1, 7, 8, 9, 10, 15, 16, 17, 31, 32, 33 }; 150 | byte[][] results = new byte[sizes.Length][]; 151 | 152 | for (int i = 0; i < sizes.Length; i++) 153 | { 154 | var buf = new byte[sizes[i]]; 155 | rng.NextBytes(buf); 156 | // Ensure something was written (not all zeros) and length correct 157 | Assert.Equal(sizes[i], buf.Length); 158 | Assert.True(buf.Any(b => b != 0) || sizes[i] == 0); 159 | results[i] = buf; 160 | } 161 | 162 | // Make sure successive calls produce different buffers (very high probability) 163 | for (int i = 1; i < sizes.Length; i++) 164 | { 165 | Assert.NotEqual(results[i - 1], results[i]); 166 | } 167 | } 168 | 169 | [Fact] 170 | public void Fixed64_ZeroToOne_IsWithinRange_AndReproducible() 171 | { 172 | var rng1 = new DeterministicRandom(777UL); 173 | var rng2 = new DeterministicRandom(777UL); 174 | 175 | for (int i = 0; i < 2048; i++) 176 | { 177 | var a = rng1.NextFixed6401(); // [0,1) 178 | var b = rng2.NextFixed6401(); 179 | Assert.True(a >= Fixed64.Zero && a < Fixed64.One); 180 | Assert.Equal(a, b); // same seed, same draw order → identical 181 | } 182 | } 183 | 184 | [Fact] 185 | public void Fixed64_MaxExclusive_IsWithinRange_AndThrowsOnInvalid() 186 | { 187 | var rng = new DeterministicRandom(888UL); 188 | 189 | // Positive max 190 | var max = 5 * Fixed64.One; 191 | for (int i = 0; i < 2048; i++) 192 | { 193 | var v = rng.NextFixed64(max); 194 | Assert.True(v >= Fixed64.Zero && v < max); 195 | } 196 | 197 | // Invalid: max <= 0 198 | Assert.Throws(() => rng.NextFixed64(Fixed64.Zero)); 199 | Assert.Throws(() => rng.NextFixed64(-Fixed64.One)); 200 | } 201 | 202 | [Fact] 203 | public void Fixed64_MinMax_RespectsBounds_AndThrowsOnInvalidRange() 204 | { 205 | var rng = new DeterministicRandom(999UL); 206 | 207 | var min = -Fixed64.One; 208 | var max = 2 * Fixed64.One; 209 | 210 | for (int i = 0; i < 2048; i++) 211 | { 212 | var v = rng.NextFixed64(min, max); 213 | Assert.True(v >= min && v < max); 214 | } 215 | 216 | Assert.Throws(() => rng.NextFixed64(Fixed64.One, Fixed64.One)); 217 | Assert.Throws(() => rng.NextFixed64(Fixed64.One, Fixed64.Zero)); 218 | } 219 | 220 | [Fact] 221 | public void Interleaved_APIs_Stay_Deterministic() 222 | { 223 | // Ensure mixing different API calls doesn't desynchronize deterministic equality 224 | var a1 = new DeterministicRandom(1234UL); 225 | var a2 = new DeterministicRandom(1234UL); 226 | 227 | // Apply the same interleaving sequence on both instances 228 | int i1 = a1.Next(100); 229 | int i2 = a2.Next(100); 230 | Assert.Equal(i1, i2); 231 | 232 | double d1 = a1.NextDouble(); 233 | double d2 = a2.NextDouble(); 234 | Assert.Equal(d1, d2, 14); 235 | 236 | var buf1 = new byte[13]; 237 | var buf2 = new byte[13]; 238 | a1.NextBytes(buf1); 239 | a2.NextBytes(buf2); 240 | Assert.Equal(buf1, buf2); 241 | 242 | var f1 = a1.NextFixed64(-Fixed64.One, Fixed64.One); 243 | var f2 = a2.NextFixed64(-Fixed64.One, Fixed64.One); 244 | Assert.Equal(f1, f2); 245 | 246 | // And the streams continue to match: 247 | Assert.Equal(a1.NextU64(), a2.NextU64()); 248 | Assert.Equal(a1.Next(), a2.Next()); 249 | } 250 | 251 | [Fact] 252 | public void Next_Int_Bounds_Cover_CommonAndEdgeRanges() 253 | { 254 | var rng = new DeterministicRandom(3141592653UL); 255 | 256 | // Small bounds including 1 (degenerate but valid) 257 | foreach (int bound in new[] { 1, 2, 3, 4, 5, 7, 16, 31, 32, 33, 64, 127, 128, 129, 255, 256, 257 }) 258 | { 259 | for (int i = 0; i < 512; i++) 260 | { 261 | int v = rng.Next(bound); 262 | Assert.InRange(v, 0, bound - 1); 263 | } 264 | } 265 | 266 | // Wide range near int.MaxValue to exercise rejection logic frequently 267 | for (int i = 0; i < 1024; i++) 268 | { 269 | int v = rng.Next(int.MaxValue); 270 | Assert.InRange(v, 0, int.MaxValue - 1); 271 | } 272 | } 273 | 274 | [Fact] 275 | public void Next_Fixed64_Covers_TypicalGameRanges() 276 | { 277 | var rng = new DeterministicRandom(0xFEEDFACEUL); 278 | 279 | // [0, 10) 280 | var ten = 10 * Fixed64.One; 281 | for (int i = 0; i < 2048; i++) 282 | { 283 | var v = rng.NextFixed64(ten); 284 | Assert.True(v >= Fixed64.Zero && v < ten); 285 | } 286 | 287 | // [-5, 5) 288 | var neg5 = -5 * Fixed64.One; 289 | var pos5 = 5 * Fixed64.One; 290 | for (int i = 0; i < 2048; i++) 291 | { 292 | var v = rng.NextFixed64(neg5, pos5); 293 | Assert.True(v >= neg5 && v < pos5); 294 | } 295 | } 296 | } 297 | } -------------------------------------------------------------------------------- /tests/FixedMathSharp.Tests/Vector2d.Tests.cs: -------------------------------------------------------------------------------- 1 | using MessagePack; 2 | 3 | #if NET48_OR_GREATER 4 | using System.IO; 5 | using System.Runtime.Serialization.Formatters.Binary; 6 | #endif 7 | 8 | #if NET8_0_OR_GREATER 9 | using System.Text.Json; 10 | using System.Text.Json.Serialization; 11 | #endif 12 | 13 | using Xunit; 14 | 15 | namespace FixedMathSharp.Tests 16 | { 17 | public class Vector2dTests 18 | { 19 | [Fact] 20 | public void RotatedRight_Rotates90DegreesClockwise() 21 | { 22 | var vector = new Vector2d(1, 0); 23 | var result = vector.RotatedRight; 24 | 25 | Assert.Equal(new Vector2d(0, -1), result); // (1, 0) rotated 90° clockwise becomes (0, -1) 26 | } 27 | 28 | [Fact] 29 | public void RotatedLeft_Rotates90DegreesCounterclockwise() 30 | { 31 | var vector = new Vector2d(1, 0); 32 | var result = vector.RotatedLeft; 33 | 34 | Assert.Equal(new Vector2d(0, 1), result); // (1, 0) rotated 90° counterclockwise becomes (0, 1) 35 | } 36 | 37 | [Fact] 38 | public void RightHandNormal_ReturnsCorrectNormalVector() 39 | { 40 | var vector = new Vector2d(1, 0); 41 | var result = vector.RightHandNormal; 42 | 43 | Assert.Equal(new Vector2d(0, 1), result); // The right-hand normal of (1, 0) is (0, 1) 44 | } 45 | 46 | [Fact] 47 | public void LeftHandNormal_ReturnsCorrectNormalVector() 48 | { 49 | var vector = new Vector2d(1, 0); 50 | var result = vector.LeftHandNormal; 51 | 52 | Assert.Equal(new Vector2d(0, -1), result); // The left-hand normal of (1, 0) is (0, -1) 53 | } 54 | 55 | [Fact] 56 | public void MyMagnitude_CalculatesCorrectMagnitude() 57 | { 58 | var vector = new Vector2d(3, 4); 59 | var result = vector.Magnitude; 60 | 61 | Assert.Equal(new Fixed64(5), result); // The magnitude of (3, 4) is 5 (3-4-5 triangle) 62 | } 63 | 64 | [Fact] 65 | public void SqrMagnitude_CalculatesCorrectSquareMagnitude() 66 | { 67 | var vector = new Vector2d(3, 4); 68 | var result = vector.SqrMagnitude; 69 | 70 | Assert.Equal(new Fixed64(25), result); // The squared magnitude of (3, 4) is 25 (3^2 + 4^2) 71 | } 72 | 73 | 74 | [Fact] 75 | public void NormalizeInPlace_NormalizesVectorCorrectly() 76 | { 77 | var vector = new Vector2d(3, 4); 78 | vector.Normalize(); 79 | 80 | var expected = new Vector2d(new Fixed64(0.6), new Fixed64(0.8)); // Normalized vector (0.6, 0.8) 81 | Assert.True(vector.FuzzyEqual(expected, new Fixed64(0.0001))); 82 | } 83 | 84 | [Fact] 85 | public void LerpInPlace_InterpolatesBetweenVectorsCorrectly() 86 | { 87 | var start = new Vector2d(0, 0); 88 | var end = new Vector2d(10, 10); 89 | var amount = new Fixed64(0.5); // 50% interpolation 90 | 91 | start.LerpInPlace(end, amount); 92 | Assert.Equal(new Vector2d(5, 5), start); // Should be halfway between (0, 0) and (10, 10) 93 | } 94 | 95 | [Fact] 96 | public void RotateInPlace_RotatesVectorCorrectly() 97 | { 98 | var vector = new Vector2d(1, 0); 99 | var cos = FixedMath.Cos(FixedMath.PiOver2); // 90° cosine 100 | var sin = FixedMath.Sin(FixedMath.PiOver2); // 90° sine 101 | 102 | vector.RotateInPlace(cos, sin); 103 | Assert.True(vector.FuzzyEqual(new Vector2d(0, 1), new Fixed64(0.0001))); // (1, 0) rotated 90° becomes (0, 1) 104 | } 105 | 106 | [Fact] 107 | public void RotateInverse_RotatesVectorInOppositeDirection() 108 | { 109 | var vector = new Vector2d(1, 0); 110 | var cos = FixedMath.Cos(FixedMath.PiOver2); // 90° cosine 111 | var sin = FixedMath.Sin(FixedMath.PiOver2); // 90° sine 112 | 113 | vector.RotateInverse(cos, sin); 114 | Assert.True(vector.FuzzyEqual(new Vector2d(0, -1), new Fixed64(0.0001))); // Should rotate -90° to (0, -1) 115 | } 116 | 117 | [Fact] 118 | public void RotateRightInPlace_RotatesVector90DegreesClockwise() 119 | { 120 | var vector = new Vector2d(1, 0); 121 | vector.RotateRightInPlace(); 122 | 123 | Assert.Equal(new Vector2d(0, -1), vector); // (1, 0) rotated 90° clockwise becomes (0, -1) 124 | } 125 | 126 | [Fact] 127 | public void RotateLeftInPlace_RotatesVector90DegreesCounterclockwise() 128 | { 129 | var vector = new Vector2d(1, 0); 130 | vector.RotateLeftInPlace(); 131 | 132 | Assert.Equal(new Vector2d(0, 1), vector); // (1, 0) rotated 90° counterclockwise becomes (0, 1) 133 | } 134 | 135 | [Fact] 136 | public void ScaleInPlace_ScalesVectorCorrectly() 137 | { 138 | var vector = new Vector2d(2, 3); 139 | var scaleFactor = new Fixed64(2); 140 | vector.ScaleInPlace(scaleFactor); 141 | 142 | Assert.Equal(new Vector2d(4, 6), vector); // (2, 3) scaled by 2 becomes (4, 6) 143 | } 144 | 145 | [Fact] 146 | public void Dot_ComputesDotProductCorrectly() 147 | { 148 | var vector1 = new Vector2d(1, 2); 149 | var vector2 = new Vector2d(3, 4); 150 | var result = vector1.Dot(vector2); 151 | 152 | Assert.Equal(new Fixed64(11), result); // Dot product of (1, 2) and (3, 4) is 1*3 + 2*4 = 11 153 | } 154 | 155 | [Fact] 156 | public void Cross_ComputesCrossProductCorrectly() 157 | { 158 | var vector1 = new Vector2d(1, 2); 159 | var vector2 = new Vector2d(3, 4); 160 | var result = vector1.CrossProduct(vector2); 161 | 162 | Assert.Equal(new Fixed64(-2), result); // Cross product of (1, 2) and (3, 4) is 1*4 - 2*3 = -2 163 | } 164 | 165 | [Fact] 166 | public void Distance_ComputesDistanceCorrectly() 167 | { 168 | var vector1 = new Vector2d(1, 1); 169 | var vector2 = new Vector2d(4, 5); 170 | var result = vector1.Distance(vector2); 171 | 172 | Assert.Equal(new Fixed64(5), result); // Distance between (1, 1) and (4, 5) is 5 (3-4-5 triangle) 173 | } 174 | 175 | [Fact] 176 | public void SqrDistance_ComputesSquareDistanceCorrectly() 177 | { 178 | var vector1 = new Vector2d(1, 1); 179 | var vector2 = new Vector2d(4, 5); 180 | var result = vector1.SqrDistance(vector2); 181 | 182 | Assert.Equal(new Fixed64(25), result); // Squared distance between (1, 1) and (4, 5) is 25 183 | } 184 | 185 | [Fact] 186 | public void ReflectInPlace_ReflectsVectorCorrectly() 187 | { 188 | var vector = new Vector2d(1, 1); 189 | var axisX = new Fixed64(0); 190 | var axisY = new Fixed64(1); // Reflect across the Y-axis 191 | 192 | vector.ReflectInPlace(axisX, axisY); 193 | Assert.Equal(new Vector2d(-1, 1), vector); // Reflecting (1, 1) across Y-axis becomes (-1, 1) 194 | } 195 | 196 | [Fact] 197 | public void AddInPlace_AddsToVectorCorrectly() 198 | { 199 | var vector = new Vector2d(1, 1); 200 | var amount = new Fixed64(2); 201 | vector.AddInPlace(amount); 202 | 203 | Assert.Equal(new Vector2d(3, 3), vector); // (1, 1) + 2 becomes (3, 3) 204 | } 205 | 206 | [Fact] 207 | public void SubtractInPlace_SubtractsFromVectorCorrectly() 208 | { 209 | var vector = new Vector2d(3, 3); 210 | var amount = new Fixed64(1); 211 | vector.SubtractInPlace(amount); 212 | 213 | Assert.Equal(new Vector2d(2, 2), vector); // (3, 3) - 1 becomes (2, 2) 214 | } 215 | 216 | [Fact] 217 | public void NormalizeInPlace_DoesNothingForZeroVector() 218 | { 219 | var vector = new Vector2d(0, 0); 220 | vector.Normalize(); 221 | 222 | Assert.Equal(new Vector2d(0, 0), vector); // A zero vector remains zero after normalization 223 | } 224 | 225 | [Fact] 226 | public void V2ClampOneInPlace_ClampsCorrectly() 227 | { 228 | var vector = new Vector2d(2, -3); 229 | var result = vector.ClampOneInPlace(); 230 | 231 | Assert.Equal(new Vector2d(1, -1), result); // Clamps x and y to [-1, 1] 232 | } 233 | 234 | [Fact] 235 | public void V2ToDegrees_ConvertsCorrectly() 236 | { 237 | var radians = new Vector2d(FixedMath.PiOver2, FixedMath.PI); // (90°, 180°) 238 | var result = radians.ToDegrees(); 239 | 240 | Assert.True(result.FuzzyEqual(new Vector2d(90, 180))); // Converts radians to degrees 241 | } 242 | 243 | [Fact] 244 | public void V2ToRadians_ConvertsCorrectly() 245 | { 246 | var degrees = new Vector2d(90, 180); 247 | var result = degrees.ToRadians(); 248 | 249 | Assert.True(result.FuzzyEqual(new Vector2d(FixedMath.PiOver2, FixedMath.PI))); // Converts degrees to radians 250 | } 251 | 252 | [Fact] 253 | public void V2FuzzyEqualAbsolute_ComparesCorrectly_WithAllowedDifference() 254 | { 255 | var vector1 = new Vector2d(2, 2); 256 | var vector2 = new Vector2d(2.1, 2.1); 257 | var allowedDifference = new Fixed64(0.15); 258 | 259 | Assert.True(vector1.FuzzyEqualAbsolute(vector2, allowedDifference)); // Approximate equality with a 0.15 difference 260 | } 261 | 262 | [Fact] 263 | public void V2FuzzyEqual_ComparesCorrectly_WithDefaultTolerance() 264 | { 265 | var vector1 = new Vector2d(100, 100); 266 | var vector2 = new Vector2d(100.0000008537, 100.0000008537); // Small difference 267 | 268 | // Use FuzzyEqual with the default tolerance, which is small (e.g., 0.01% difference) 269 | Assert.True(vector1.FuzzyEqual(vector2)); // The difference should be within the default tolerance 270 | 271 | vector2 = new Vector2d(100.0001, 100.0001); // big difference 272 | Assert.False(vector1.FuzzyEqual(vector2)); // The difference should be outside the default tolerance 273 | } 274 | 275 | 276 | [Fact] 277 | public void V2FuzzyEqual_ComparesCorrectly_WithCustomPercentage() 278 | { 279 | var vector1 = new Vector2d(100, 100); 280 | var vector2 = new Vector2d(102, 102); 281 | var percentage = new Fixed64(0.02); // Allow a 2% difference 282 | 283 | Assert.True(vector1.FuzzyEqual(vector2, percentage)); // Should be approximately equal within 2% difference 284 | } 285 | 286 | [Fact] 287 | public void V2CheckDistance_VerifiesDistanceCorrectly() 288 | { 289 | var vector1 = new Vector2d(0, 0); 290 | var vector2 = new Vector2d(3, 4); // Distance is 5 (3-4-5 triangle) 291 | var factor = new Fixed64(5); 292 | 293 | Assert.True(vector1.CheckDistance(vector2, factor)); // Distance is 5, so should return true 294 | } 295 | 296 | [Fact] 297 | public void V2SqrDistance_CalculatesCorrectly() 298 | { 299 | var vector1 = new Vector2d(0, 0); 300 | var vector2 = new Vector2d(3, 4); // Squared distance should be 25 301 | var result = vector1.SqrDistance(vector2); 302 | 303 | Assert.Equal(new Fixed64(25), result); // 3^2 + 4^2 = 25 304 | } 305 | 306 | [Fact] 307 | public void V2Rotate_RotatesVectorCorrectly() 308 | { 309 | var vector = new Vector2d(1, 0); 310 | var angle = FixedMath.PiOver2; // Rotate by 90° (π/2 radians) 311 | var result = vector.Rotate(angle); 312 | 313 | Assert.True(result.FuzzyEqual(new Vector2d(0, 1), new Fixed64(0.0001))); // Should rotate to (0, 1) 314 | } 315 | 316 | #region Test: Serialization 317 | 318 | [Fact] 319 | public void Vector2d_NetSerialization_RoundTripMaintainsData() 320 | { 321 | var originalValue = new Vector2d(FixedMath.PI, FixedMath.PiOver2); 322 | 323 | // Serialize the Vector3d object 324 | #if NET48_OR_GREATER 325 | var formatter = new BinaryFormatter(); 326 | using var stream = new MemoryStream(); 327 | formatter.Serialize(stream, originalValue); 328 | 329 | // Reset stream position and deserialize 330 | stream.Seek(0, SeekOrigin.Begin); 331 | var deserializedValue = (Vector2d)formatter.Deserialize(stream); 332 | #endif 333 | 334 | #if NET8_0_OR_GREATER 335 | var jsonOptions = new JsonSerializerOptions 336 | { 337 | DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, 338 | ReferenceHandler = ReferenceHandler.IgnoreCycles, 339 | IncludeFields = true, 340 | IgnoreReadOnlyProperties = true 341 | }; 342 | var json = JsonSerializer.SerializeToUtf8Bytes(originalValue, jsonOptions); 343 | var deserializedValue = JsonSerializer.Deserialize(json, jsonOptions); 344 | #endif 345 | 346 | // Check that deserialized values match the original 347 | Assert.Equal(originalValue, deserializedValue); 348 | } 349 | 350 | [Fact] 351 | public void Vector2d_MsgPackSerialization_RoundTripMaintainsData() 352 | { 353 | Vector2d originalValue = new Vector2d(FixedMath.PI, FixedMath.PiOver2); 354 | 355 | byte[] bytes = MessagePackSerializer.Serialize(originalValue); 356 | Vector2d deserializedValue = MessagePackSerializer.Deserialize(bytes); 357 | 358 | // Check that deserialized values match the original 359 | Assert.Equal(originalValue, deserializedValue); 360 | } 361 | 362 | #endregion 363 | } 364 | } 365 | -------------------------------------------------------------------------------- /tests/FixedMathSharp.Tests/Fixed3x3.Tests.cs: -------------------------------------------------------------------------------- 1 | using MessagePack; 2 | 3 | #if NET48_OR_GREATER 4 | using System.IO; 5 | using System.Runtime.Serialization.Formatters.Binary; 6 | #endif 7 | 8 | #if NET8_0_OR_GREATER 9 | using System.Text.Json; 10 | using System.Text.Json.Serialization; 11 | #endif 12 | 13 | using Xunit; 14 | 15 | namespace FixedMathSharp.Tests 16 | { 17 | public class Fixed3x3Tests 18 | { 19 | [Fact] 20 | public void CreateRotationX_WorksCorrectly() 21 | { 22 | var rotationMatrix = Fixed3x3.CreateRotationX(FixedMath.PiOver2); // 90 degrees 23 | var expectedMatrix = new Fixed3x3( 24 | Fixed64.One, Fixed64.Zero, Fixed64.Zero, 25 | Fixed64.Zero, Fixed64.Zero, -Fixed64.One, 26 | Fixed64.Zero, Fixed64.One, Fixed64.Zero 27 | ); 28 | 29 | Assert.Equal(expectedMatrix, rotationMatrix); 30 | } 31 | 32 | [Fact] 33 | public void CreateRotationY_WorksCorrectly() 34 | { 35 | var rotationMatrix = Fixed3x3.CreateRotationY(FixedMath.PiOver2); // 90 degrees 36 | var expectedMatrix = new Fixed3x3( 37 | Fixed64.Zero, Fixed64.Zero, Fixed64.One, 38 | Fixed64.Zero, Fixed64.One, Fixed64.Zero, 39 | -Fixed64.One, Fixed64.Zero, Fixed64.Zero 40 | ); 41 | 42 | Assert.Equal(expectedMatrix, rotationMatrix); 43 | } 44 | 45 | [Fact] 46 | public void CreateRotationZ_WorksCorrectly() 47 | { 48 | var rotationMatrix = Fixed3x3.CreateRotationZ(FixedMath.PiOver2); // 90 degrees 49 | var expectedMatrix = new Fixed3x3( 50 | Fixed64.Zero, -Fixed64.One, Fixed64.Zero, 51 | Fixed64.One, Fixed64.Zero, Fixed64.Zero, 52 | Fixed64.Zero, Fixed64.Zero, Fixed64.One 53 | ); 54 | 55 | Assert.Equal(expectedMatrix, rotationMatrix); 56 | } 57 | 58 | [Fact] 59 | public void CreateShear_WorksCorrectly() 60 | { 61 | var shearMatrix = Fixed3x3.CreateShear(new Fixed64(1.0f), new Fixed64(0.5f), new Fixed64(0.2f)); 62 | var expectedMatrix = new Fixed3x3( 63 | Fixed64.One, new Fixed64(1.0f), new Fixed64(0.5f), 64 | new Fixed64(1.0f), Fixed64.One, new Fixed64(0.2f), 65 | new Fixed64(0.5f), new Fixed64(0.2f), Fixed64.One 66 | ); 67 | 68 | Assert.Equal(expectedMatrix, shearMatrix); 69 | } 70 | 71 | [Fact] 72 | public void InvertDiagonal_WorksCorrectly() 73 | { 74 | var matrix = new Fixed3x3( 75 | new Fixed64(2.0f), Fixed64.Zero, Fixed64.Zero, 76 | Fixed64.Zero, new Fixed64(3.0f), Fixed64.Zero, 77 | Fixed64.Zero, Fixed64.Zero, new Fixed64(4.0f) 78 | ); 79 | 80 | var inverted = matrix.InvertDiagonal(); 81 | var expected = new Fixed3x3( 82 | Fixed64.One / new Fixed64(2.0f), Fixed64.Zero, Fixed64.Zero, 83 | Fixed64.Zero, Fixed64.One / new Fixed64(3.0f), Fixed64.Zero, 84 | Fixed64.Zero, Fixed64.Zero, Fixed64.One / new Fixed64(4.0f) 85 | ); 86 | 87 | Assert.Equal(expected, inverted); 88 | } 89 | 90 | [Fact] 91 | public void Lerp_WorksCorrectly() 92 | { 93 | var matrixA = Fixed3x3.Identity; 94 | var matrixB = new Fixed3x3( 95 | Fixed64.One, Fixed64.One, Fixed64.One, 96 | Fixed64.One, Fixed64.One, Fixed64.One, 97 | Fixed64.One, Fixed64.One, Fixed64.One 98 | ); 99 | 100 | var result = Fixed3x3.Lerp(matrixA, matrixB, new Fixed64(0.5f)); 101 | var expected = new Fixed3x3( 102 | Fixed64.One, new Fixed64(0.5f), new Fixed64(0.5f), 103 | new Fixed64(0.5f), Fixed64.One, new Fixed64(0.5f), 104 | new Fixed64(0.5f), new Fixed64(0.5f), Fixed64.One 105 | ); 106 | 107 | Assert.Equal(expected, result); 108 | } 109 | 110 | [Fact] 111 | public void Transpose_WorksCorrectly() 112 | { 113 | var matrix = new Fixed3x3( 114 | Fixed64.One, new Fixed64(2.0f), new Fixed64(3.0f), 115 | new Fixed64(4.0f), Fixed64.One, new Fixed64(5.0f), 116 | new Fixed64(6.0f), new Fixed64(7.0f), Fixed64.One 117 | ); 118 | 119 | var transposed = Fixed3x3.Transpose(matrix); 120 | var expected = new Fixed3x3( 121 | Fixed64.One, new Fixed64(4.0f), new Fixed64(6.0f), 122 | new Fixed64(2.0f), Fixed64.One, new Fixed64(7.0f), 123 | new Fixed64(3.0f), new Fixed64(5.0f), Fixed64.One 124 | ); 125 | 126 | Assert.Equal(expected, transposed); 127 | } 128 | 129 | [Fact] 130 | public void Invert_WorksCorrectly() 131 | { 132 | var matrix = new Fixed3x3( 133 | new Fixed64(2.0f), Fixed64.Zero, Fixed64.Zero, 134 | Fixed64.Zero, new Fixed64(3.0f), Fixed64.Zero, 135 | Fixed64.Zero, Fixed64.Zero, new Fixed64(4.0f) 136 | ); 137 | 138 | var success = Fixed3x3.Invert(matrix, out var result); 139 | Assert.True(success); 140 | 141 | var expected = new Fixed3x3( 142 | Fixed64.One / new Fixed64(2.0f), Fixed64.Zero, Fixed64.Zero, 143 | Fixed64.Zero, Fixed64.One / new Fixed64(3.0f), Fixed64.Zero, 144 | Fixed64.Zero, Fixed64.Zero, Fixed64.One / new Fixed64(4.0f) 145 | ); 146 | 147 | Assert.True(result?.FuzzyEqual(expected), $"Expected: {expected}, Actual: {result}"); 148 | } 149 | 150 | [Fact] 151 | public void Invert_SingularMatrix_ReturnsFalse() 152 | { 153 | var matrix = new Fixed3x3( 154 | Fixed64.One, Fixed64.Zero, Fixed64.Zero, 155 | Fixed64.Zero, Fixed64.Zero, Fixed64.Zero, // Singular row 156 | Fixed64.Zero, Fixed64.Zero, Fixed64.One 157 | ); 158 | 159 | var success = Fixed3x3.Invert(matrix, out var _); 160 | Assert.False(success); 161 | } 162 | 163 | [Fact] 164 | public void Fixed3x3_SetGlobalScale_WorksWithoutRotation() 165 | { 166 | var initialScale = new Vector3d(2, 2, 2); 167 | var globalScale = new Vector3d(4, 4, 4); 168 | 169 | var matrix = Fixed3x3.Identity; 170 | matrix.SetScale(initialScale); 171 | 172 | matrix.SetGlobalScale(globalScale); 173 | 174 | var extractedScale = Fixed3x3.ExtractScale(matrix); 175 | Assert.Equal(globalScale, extractedScale); 176 | } 177 | 178 | [Fact] 179 | public void Fixed3x3_SetGlobalScale_WorksWithRotation() 180 | { 181 | var rotationMatrix = new Fixed3x3( 182 | new Vector3d(0, 1, 0), // Rotated X-axis 183 | new Vector3d(-1, 0, 0), // Rotated Y-axis 184 | new Vector3d(0, 0, 1) // Z-axis unchanged 185 | ); 186 | var initialScale = new Vector3d(2, 2, 2); 187 | var globalScale = new Vector3d(4, 4, 4); 188 | 189 | // Apply initial scale 190 | rotationMatrix.SetScale(initialScale); 191 | 192 | // Set global scale 193 | rotationMatrix.SetGlobalScale(globalScale); 194 | 195 | // Extract final scale 196 | var extractedScale = Fixed3x3.ExtractLossyScale(rotationMatrix); 197 | 198 | Assert.True( 199 | extractedScale.FuzzyEqual(globalScale, new Fixed64(0.01)), 200 | $"Extracted scale {extractedScale} does not match expected {globalScale}." 201 | ); 202 | } 203 | 204 | [Fact] 205 | public void Fixed3x3_Normalize_WorksCorrectly() 206 | { 207 | var matrix = new Fixed3x3( 208 | new Vector3d(2, 0, 0), 209 | new Vector3d(0, 3, 0), 210 | new Vector3d(0, 0, 4) 211 | ); 212 | 213 | matrix.Normalize(); 214 | 215 | var xAxis = new Vector3d(matrix.m00, matrix.m01, matrix.m02); 216 | var yAxis = new Vector3d(matrix.m10, matrix.m11, matrix.m12); 217 | var zAxis = new Vector3d(matrix.m20, matrix.m21, matrix.m22); 218 | 219 | Assert.Equal(Fixed64.One, xAxis.Magnitude); 220 | Assert.Equal(Fixed64.One, yAxis.Magnitude); 221 | Assert.Equal(Fixed64.One, zAxis.Magnitude); 222 | } 223 | 224 | [Fact] 225 | public void TransformDirection_IdentityMatrix_ReturnsSameVector() 226 | { 227 | var matrix = Fixed3x3.Identity; 228 | var direction = new Vector3d(1, 2, 3); 229 | 230 | var transformed = Fixed3x3.TransformDirection(matrix, direction); 231 | 232 | Assert.Equal(direction, transformed); 233 | } 234 | 235 | [Fact] 236 | public void TransformDirection_90DegreeRotationX_WorksCorrectly() 237 | { 238 | var matrix = Fixed3x3.CreateRotationX(FixedMath.PiOver2); // 90-degree rotation around X-axis 239 | var direction = new Vector3d(0, 1, 0); // Pointing along Y-axis 240 | 241 | var transformed = Fixed3x3.TransformDirection(matrix, direction); 242 | 243 | // Expecting the direction to be rotated into the Z-axis 244 | var expected = new Vector3d(0, 0, 1); 245 | Assert.Equal(expected, transformed); 246 | } 247 | 248 | [Fact] 249 | public void TransformDirection_90DegreeRotationY_WorksCorrectly() 250 | { 251 | var matrix = Fixed3x3.CreateRotationY(FixedMath.PiOver2); // 90-degree rotation around Y-axis 252 | var direction = new Vector3d(1, 0, 0); // Pointing along X-axis 253 | 254 | var transformed = Fixed3x3.TransformDirection(matrix, direction); 255 | 256 | // Expecting the direction to be rotated into the negative Z-axis 257 | var expected = new Vector3d(0, 0, -1); 258 | Assert.Equal(expected, transformed); 259 | } 260 | 261 | [Fact] 262 | public void TransformDirection_90DegreeRotationZ_WorksCorrectly() 263 | { 264 | var matrix = Fixed3x3.CreateRotationZ(FixedMath.PiOver2); // 90-degree rotation around Z-axis 265 | var direction = new Vector3d(1, 0, 0); // Pointing along X-axis 266 | 267 | var transformed = Fixed3x3.TransformDirection(matrix, direction); 268 | 269 | // Expecting the direction to be rotated into the Y-axis 270 | var expected = new Vector3d(0, 1, 0); 271 | Assert.Equal(expected, transformed); 272 | } 273 | 274 | [Fact] 275 | public void TransformDirection_WithScaling_DirectionRemainsNormalized() 276 | { 277 | var matrix = Fixed3x3.CreateScale(new Vector3d(2, 3, 4)); 278 | var direction = new Vector3d(1, 1, 1).Normal; 279 | 280 | var transformed = Fixed3x3.TransformDirection(matrix, direction).Normal; 281 | 282 | // Direction should still be normalized (scaling affects positions, not directions) 283 | Assert.Equal(Fixed64.One, transformed.Magnitude, new Fixed64(0.0001)); 284 | } 285 | 286 | [Fact] 287 | public void InverseTransformDirection_IdentityMatrix_ReturnsSameVector() 288 | { 289 | var matrix = Fixed3x3.Identity; 290 | var direction = new Vector3d(1, 2, 3); 291 | 292 | var inverseTransformed = Fixed3x3.InverseTransformDirection(matrix, direction); 293 | 294 | Assert.Equal(direction, inverseTransformed); 295 | } 296 | 297 | [Fact] 298 | public void InverseTransformDirection_InvertsTransformDirection() 299 | { 300 | var matrix = Fixed3x3.CreateRotationY(FixedMath.PiOver2); // 90-degree Y-axis rotation 301 | var direction = new Vector3d(1, 0, 0); 302 | 303 | var transformed = Fixed3x3.TransformDirection(matrix, direction); 304 | var inverseTransformed = Fixed3x3.InverseTransformDirection(matrix, transformed); 305 | 306 | // The inverse should return the original direction 307 | Assert.Equal(direction, inverseTransformed); 308 | } 309 | 310 | [Fact] 311 | public void InverseTransformDirection_NonInvertibleMatrix_ThrowsException() 312 | { 313 | var singularMatrix = new Fixed3x3( 314 | Fixed64.One, Fixed64.Zero, Fixed64.Zero, 315 | Fixed64.Zero, Fixed64.Zero, Fixed64.Zero, // Singular row 316 | Fixed64.Zero, Fixed64.Zero, Fixed64.One 317 | ); 318 | 319 | var direction = new Vector3d(1, 1, 1); 320 | 321 | Assert.Throws(() => 322 | Fixed3x3.InverseTransformDirection(singularMatrix, direction)); 323 | } 324 | 325 | #region Test: Serialization 326 | 327 | [Fact] 328 | public void Fixed3x3_NetSerialization_RoundTripMaintainsData() 329 | { 330 | var original3x3 = Fixed3x3.CreateRotationX(FixedMath.PiOver2); // 90 degrees 331 | 332 | // Serialize the Fixed3x3 object 333 | #if NET48_OR_GREATER 334 | var formatter = new BinaryFormatter(); 335 | using var stream = new MemoryStream(); 336 | formatter.Serialize(stream, original3x3); 337 | 338 | // Reset stream position and deserialize 339 | stream.Seek(0, SeekOrigin.Begin); 340 | var deserialized3x3 = (Fixed3x3)formatter.Deserialize(stream); 341 | #endif 342 | 343 | #if NET8_0_OR_GREATER 344 | var jsonOptions = new JsonSerializerOptions 345 | { 346 | DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, 347 | ReferenceHandler = ReferenceHandler.IgnoreCycles, 348 | IncludeFields = true, 349 | IgnoreReadOnlyProperties = true 350 | }; 351 | var json = JsonSerializer.SerializeToUtf8Bytes(original3x3, jsonOptions); 352 | var deserialized3x3 = JsonSerializer.Deserialize(json, jsonOptions); 353 | #endif 354 | 355 | // Check that deserialized values match the original 356 | Assert.Equal(original3x3, deserialized3x3); 357 | } 358 | 359 | [Fact] 360 | public void Fixed3x3_MsgPackSerialization_RoundTripMaintainsData() 361 | { 362 | Fixed3x3 originalValue = Fixed3x3.CreateRotationX(FixedMath.PiOver2); // 90 degrees 363 | 364 | byte[] bytes = MessagePackSerializer.Serialize(originalValue); 365 | Fixed3x3 deserializedValue = MessagePackSerializer.Deserialize(bytes); 366 | 367 | // Check that deserialized values match the original 368 | Assert.Equal(originalValue, deserializedValue); 369 | } 370 | 371 | #endregion 372 | } 373 | } 374 | -------------------------------------------------------------------------------- /tests/FixedMathSharp.Tests/FixedMath.Tests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | 4 | namespace FixedMathSharp.Tests 5 | { 6 | public class FixedMathTests 7 | { 8 | #region Test: CopySign Method 9 | 10 | [Fact] 11 | public void CopySign_PositiveToPositive_ReturnsPositive() 12 | { 13 | var result = FixedMath.CopySign(new Fixed64(5), new Fixed64(3)); 14 | Assert.Equal(new Fixed64(5), result); 15 | } 16 | 17 | [Fact] 18 | public void CopySign_PositiveToNegative_ReturnsNegative() 19 | { 20 | var result = FixedMath.CopySign(new Fixed64(5), new Fixed64(-3)); 21 | Assert.Equal(new Fixed64(-5), result); 22 | } 23 | 24 | #endregion 25 | 26 | #region Test: Clamp01 Method 27 | 28 | [Fact] 29 | public void Clamp01_ValueLessThanZero_ReturnsZero() 30 | { 31 | var result = FixedMath.Clamp01(new Fixed64(-1)); 32 | Assert.Equal(Fixed64.Zero, result); 33 | } 34 | 35 | [Fact] 36 | public void Clamp01_ValueGreaterThanOne_ReturnsOne() 37 | { 38 | var result = FixedMath.Clamp01(new Fixed64(2)); 39 | Assert.Equal(Fixed64.One, result); 40 | } 41 | 42 | [Fact] 43 | public void Clamp01_ValueInRange_ReturnsValue() 44 | { 45 | var result = FixedMath.Clamp01(new Fixed64(0.5f)); 46 | Assert.Equal(new Fixed64(0.5f), result); 47 | } 48 | 49 | #endregion 50 | 51 | #region Test: FastAbs Method 52 | 53 | [Fact] 54 | public void FastAbs_PositiveValue_ReturnsSameValue() 55 | { 56 | var result = FixedMath.Abs(new Fixed64(10)); 57 | Assert.Equal(new Fixed64(10), result); 58 | } 59 | 60 | [Fact] 61 | public void FastAbs_NegativeValue_ReturnsPositiveValue() 62 | { 63 | var result = FixedMath.Abs(new Fixed64(-10)); 64 | Assert.Equal(new Fixed64(10), result); 65 | } 66 | 67 | #endregion 68 | 69 | #region Test: Ceiling Method 70 | 71 | [Fact] 72 | public void Ceiling_WithFraction_ReturnsNextInteger() 73 | { 74 | var result = FixedMath.Ceiling(new Fixed64(1.5)); 75 | Assert.Equal(new Fixed64(2), result); 76 | } 77 | 78 | [Fact] 79 | public void Ceiling_ExactInteger_ReturnsSameInteger() 80 | { 81 | var result = FixedMath.Ceiling(new Fixed64(3.0)); 82 | var test = new Fixed64(3); 83 | Assert.Equal(test, result); 84 | } 85 | 86 | #endregion 87 | 88 | #region Test: Max Method 89 | 90 | [Fact] 91 | public void Max_FirstValueLarger_ReturnsFirstValue() 92 | { 93 | var result = FixedMath.Max(new Fixed64(5), new Fixed64(3)); 94 | Assert.Equal(new Fixed64(5), result); 95 | } 96 | 97 | [Fact] 98 | public void Max_SecondValueLarger_ReturnsSecondValue() 99 | { 100 | var result = FixedMath.Max(new Fixed64(3), new Fixed64(5)); 101 | Assert.Equal(new Fixed64(5), result); 102 | } 103 | 104 | [Fact] 105 | public void Max_EqualValues_ReturnsEitherValue() 106 | { 107 | var result = FixedMath.Max(new Fixed64(5), new Fixed64(5)); 108 | Assert.Equal(new Fixed64(5), result); 109 | } 110 | 111 | #endregion 112 | 113 | #region Test: Min Method 114 | 115 | [Fact] 116 | public void Min_FirstValueSmaller_ReturnsFirstValue() 117 | { 118 | var result = FixedMath.Min(new Fixed64(3), new Fixed64(5)); 119 | Assert.Equal(new Fixed64(3), result); 120 | } 121 | 122 | [Fact] 123 | public void Min_SecondValueSmaller_ReturnsSecondValue() 124 | { 125 | var result = FixedMath.Min(new Fixed64(5), new Fixed64(3)); 126 | Assert.Equal(new Fixed64(3), result); 127 | } 128 | 129 | [Fact] 130 | public void Min_EqualValues_ReturnsEitherValue() 131 | { 132 | var result = FixedMath.Min(new Fixed64(5), new Fixed64(5)); 133 | Assert.Equal(new Fixed64(5), result); 134 | } 135 | 136 | #endregion 137 | 138 | #region Test: Round Method (Without Decimal Places) 139 | 140 | [Fact] 141 | public void Round_ToEven_RoundsToNearestEven() 142 | { 143 | var result = FixedMath.Round(new Fixed64(2.5)); 144 | Assert.Equal(new Fixed64(2), result); 145 | } 146 | 147 | [Fact] 148 | public void Round_AwayFromZero_RoundsUp() 149 | { 150 | var result = FixedMath.Round(new Fixed64(2.5), System.MidpointRounding.AwayFromZero); 151 | Assert.Equal(new Fixed64(3), result); 152 | } 153 | 154 | [Fact] 155 | public void Round_ToEven_NegativeNumber_RoundsToNearestEven() 156 | { 157 | var result = FixedMath.Round(new Fixed64(-2.5)); 158 | Assert.Equal(new Fixed64(-2), result); 159 | } 160 | 161 | #endregion 162 | 163 | #region Test: Round Method (With Decimal Places) 164 | 165 | [Fact] 166 | public void Round_WithDecimalPlaces_RoundsToTwoDecimalPlaces() 167 | { 168 | var result = FixedMath.RoundToPrecision(new Fixed64(2.556f), 2, MidpointRounding.AwayFromZero); 169 | Assert.Equal(2.56f, result.ToFormattedFloat()); 170 | } 171 | 172 | [Fact] 173 | public void Round_WithDecimalPlaces_RoundsToZeroDecimalPlaces_ToEven() 174 | { 175 | var result = FixedMath.RoundToPrecision(new Fixed64(2.5), 0); 176 | Assert.Equal(new Fixed64(2), result); 177 | } 178 | 179 | [Fact] 180 | public void Round_WithDecimalPlaces_RoundsToZeroDecimalPlaces_AwayFromZero() 181 | { 182 | var result = FixedMath.RoundToPrecision(new Fixed64(2.5), 0, MidpointRounding.AwayFromZero); 183 | Assert.Equal(new Fixed64(3), result); 184 | } 185 | 186 | #endregion 187 | 188 | #region Test: FastAdd Method 189 | 190 | [Fact] 191 | public void FastAdd_AddsTwoPositiveValues() 192 | { 193 | var a = new Fixed64(2); 194 | var b = new Fixed64(3); 195 | var result = FixedMath.FastAdd(a, b); 196 | Assert.Equal(new Fixed64(5), result); 197 | } 198 | 199 | [Fact] 200 | public void FastAdd_AddsNegativeAndPositiveValue() 201 | { 202 | var a = new Fixed64(-2); 203 | var b = new Fixed64(3); 204 | var result = FixedMath.FastAdd(a, b); 205 | Assert.Equal(new Fixed64(1), result); 206 | } 207 | 208 | [Fact] 209 | public void FastAdd_AddsTwoNegativeValues() 210 | { 211 | var a = new Fixed64(-5); 212 | var b = new Fixed64(-3); 213 | var result = FixedMath.FastAdd(a, b); 214 | Assert.Equal(new Fixed64(-8), result); 215 | } 216 | 217 | #endregion 218 | 219 | #region Test: FastSub Method 220 | 221 | [Fact] 222 | public void FastSub_SubtractsTwoPositiveValues() 223 | { 224 | var a = new Fixed64(5); 225 | var b = new Fixed64(3); 226 | var result = FixedMath.FastSub(a, b); 227 | Assert.Equal(new Fixed64(2), result); 228 | } 229 | 230 | [Fact] 231 | public void FastSub_SubtractsNegativeFromPositive() 232 | { 233 | var a = new Fixed64(5); 234 | var b = new Fixed64(-3); 235 | var result = FixedMath.FastSub(a, b); 236 | Assert.Equal(new Fixed64(8), result); 237 | } 238 | 239 | [Fact] 240 | public void FastSub_SubtractsPositiveFromNegative() 241 | { 242 | var a = new Fixed64(-5); 243 | var b = new Fixed64(3); 244 | var result = FixedMath.FastSub(a, b); 245 | Assert.Equal(new Fixed64(-8), result); 246 | } 247 | 248 | #endregion 249 | 250 | #region Test: FastMul Method 251 | 252 | [Fact] 253 | public void FastMul_MultipliesTwoPositiveValues() 254 | { 255 | var a = new Fixed64(2); 256 | var b = new Fixed64(3); 257 | var result = FixedMath.FastMul(a, b); 258 | Assert.Equal(new Fixed64(6), result); 259 | } 260 | 261 | [Fact] 262 | public void FastMul_MultipliesPositiveAndNegativeValue() 263 | { 264 | var a = new Fixed64(2); 265 | var b = new Fixed64(-3); 266 | var result = FixedMath.FastMul(a, b); 267 | Assert.Equal(new Fixed64(-6), result); 268 | } 269 | 270 | [Fact] 271 | public void FastMul_MultipliesWithZero() 272 | { 273 | var a = new Fixed64(0); 274 | var b = new Fixed64(3); 275 | var result = FixedMath.FastMul(a, b); 276 | Assert.Equal(Fixed64.Zero, result); 277 | } 278 | 279 | #endregion 280 | 281 | #region Test: Interpolation Methods 282 | 283 | [Fact] 284 | public void LinearInterpolate_TAtZero_ReturnsFromValue() 285 | { 286 | var result = FixedMath.LinearInterpolate(new Fixed64(3), new Fixed64(5), new Fixed64(0)); 287 | Assert.Equal(new Fixed64(3), result); 288 | } 289 | 290 | [Fact] 291 | public void LinearInterpolate_TAtOne_ReturnsToValue() 292 | { 293 | var result = FixedMath.LinearInterpolate(new Fixed64(3), new Fixed64(5), new Fixed64(1)); 294 | Assert.Equal(new Fixed64(5), result); 295 | } 296 | 297 | [Fact] 298 | public void LinearInterpolate_TAtHalf_ReturnsMidpoint() 299 | { 300 | var result = FixedMath.LinearInterpolate(new Fixed64(3), new Fixed64(5), new Fixed64(0.5)); 301 | Assert.Equal(new Fixed64(4), result); // Midpoint should be 4 302 | } 303 | 304 | [Fact] 305 | public void SmoothStep_TAtZero_ReturnsFromValue() 306 | { 307 | var result = FixedMath.SmoothStep(new Fixed64(3), new Fixed64(5), new Fixed64(0)); 308 | Assert.Equal(new Fixed64(3), result); 309 | } 310 | 311 | [Fact] 312 | public void SmoothStep_TAtOne_ReturnsToValue() 313 | { 314 | var result = FixedMath.SmoothStep(new Fixed64(3), new Fixed64(5), new Fixed64(1)); 315 | Assert.Equal(new Fixed64(5), result); 316 | } 317 | 318 | [Fact] 319 | public void SmoothStep_TAtHalf_ReturnsSmoothedMidpoint() 320 | { 321 | var result = FixedMath.SmoothStep(new Fixed64(0), new Fixed64(10), new Fixed64(0.5)); 322 | Assert.Equal(new Fixed64(5), result); // Should be near 5 with smoothing effect 323 | } 324 | 325 | [Fact] 326 | public void CubicInterpolate_TAtZero_ReturnsP0() 327 | { 328 | var result = FixedMath.CubicInterpolate(new Fixed64(3), new Fixed64(5), new Fixed64(1), new Fixed64(1), new Fixed64(0)); 329 | Assert.Equal(new Fixed64(3), result); 330 | } 331 | 332 | [Fact] 333 | public void CubicInterpolate_TAtOne_ReturnsP1() 334 | { 335 | var result = FixedMath.CubicInterpolate(new Fixed64(3), new Fixed64(5), new Fixed64(1), new Fixed64(1), new Fixed64(1)); 336 | Assert.Equal(new Fixed64(5), result); 337 | } 338 | 339 | [Fact] 340 | public void CubicInterpolate_TAtHalf_ReturnsSmoothCurveValue() 341 | { 342 | var result = FixedMath.CubicInterpolate(new Fixed64(0), new Fixed64(10), new Fixed64(2), new Fixed64(2), new Fixed64(0.5)); 343 | Assert.Equal(new Fixed64(5), result); // Expected to be near midpoint but with cubic smoothing 344 | } 345 | 346 | #endregion 347 | 348 | #region Test: Clamp Method 349 | 350 | [Fact] 351 | public void Clamp_ValueWithinRange_ReturnsSameValue() 352 | { 353 | var value = new Fixed64(5); 354 | var min = new Fixed64(3); 355 | var result = FixedMath.Clamp(value, min, Fixed64.MAX_VALUE); 356 | Assert.Equal(new Fixed64(5), result); 357 | } 358 | 359 | [Fact] 360 | public void Clamp_ValueWithinRange_ReturnsValue() 361 | { 362 | var result = FixedMath.Clamp(new Fixed64(3), new Fixed64(2), new Fixed64(5)); 363 | Assert.Equal(new Fixed64(3), result); 364 | } 365 | 366 | [Fact] 367 | public void Clamp_ValueBelowMin_ReturnsMin() 368 | { 369 | var result = FixedMath.Clamp(new Fixed64(1), new Fixed64(2), new Fixed64(5)); 370 | Assert.Equal(new Fixed64(2), result); 371 | } 372 | 373 | [Fact] 374 | public void Clamp_ValueAboveMax_ReturnsMax() 375 | { 376 | var result = FixedMath.Clamp(new Fixed64(6), new Fixed64(2), new Fixed64(5)); 377 | Assert.Equal(new Fixed64(5), result); 378 | } 379 | 380 | #endregion 381 | 382 | #region Test: MoveTowards Method 383 | 384 | [Fact] 385 | public void MoveTowards_ValueMovesUpWithoutOvershoot() 386 | { 387 | var result = FixedMath.MoveTowards(new Fixed64(3), new Fixed64(5), new Fixed64(1)); 388 | Assert.Equal(new Fixed64(4), result); 389 | } 390 | 391 | [Fact] 392 | public void MoveTowards_ValueMovesDownWithoutOvershoot() 393 | { 394 | var result = FixedMath.MoveTowards(new Fixed64(5), new Fixed64(3), new Fixed64(1)); 395 | Assert.Equal(new Fixed64(4), result); 396 | } 397 | 398 | [Fact] 399 | public void MoveTowards_ValueOvershoot_ReturnsTarget() 400 | { 401 | var result = FixedMath.MoveTowards(new Fixed64(3), new Fixed64(5), new Fixed64(3)); 402 | Assert.Equal(new Fixed64(5), result); // It overshoots, so it should return 5 403 | } 404 | 405 | #endregion 406 | 407 | #region Test: FastMod Method (Edge Case) 408 | 409 | [Fact] 410 | public void FastMod_PositiveValues_ReturnsCorrectRemainder() 411 | { 412 | var result = FixedMath.FastMod(new Fixed64(10), new Fixed64(3)); 413 | Assert.Equal(new Fixed64(1), result); 414 | } 415 | 416 | [Fact] 417 | public void FastMod_NegativeDividend_ReturnsCorrectRemainder() 418 | { 419 | var result = FixedMath.FastMod(new Fixed64(-10), new Fixed64(3)); 420 | Assert.Equal(new Fixed64(-1), result); // Check for correct handling of negative numbers 421 | } 422 | 423 | [Fact] 424 | public void FastMod_ZeroDivisor_ThrowsException() 425 | { 426 | Assert.Throws(() => FixedMath.FastMod(new Fixed64(10), Fixed64.Zero)); 427 | } 428 | 429 | #endregion 430 | 431 | #region Test: AddOverflowHelper Method 432 | 433 | [Fact] 434 | public void AddOverflowHelper_NoOverflow_ReturnsCorrectSum() 435 | { 436 | bool overflow = false; 437 | long x = 10; 438 | long y = 20; 439 | var result = FixedMath.AddOverflowHelper(x, y, ref overflow); 440 | Assert.Equal(30, result); 441 | Assert.False(overflow); 442 | } 443 | 444 | [Fact] 445 | public void AddOverflowHelper_PositiveOverflow_SetsOverflowFlag() 446 | { 447 | bool overflow = false; 448 | long x = long.MaxValue; 449 | long y = 1; 450 | _ = FixedMath.AddOverflowHelper(x, y, ref overflow); 451 | Assert.True(overflow); 452 | } 453 | 454 | [Fact] 455 | public void AddOverflowHelper_NegativeOverflow_SetsOverflowFlag() 456 | { 457 | bool overflow = false; 458 | long x = long.MinValue; 459 | long y = -1; 460 | _ = FixedMath.AddOverflowHelper(x, y, ref overflow); 461 | Assert.True(overflow); 462 | } 463 | 464 | #endregion 465 | } 466 | } --------------------------------------------------------------------------------