├── 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 | 
5 |
6 | [](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 | }
--------------------------------------------------------------------------------