diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..27a4c24 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,52 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 4 + +[*.{csproj,props,targets,slnx,xml,axaml,resx,json,yml,yaml}] +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false + +[*.cs] +# Namespaces and usings +csharp_style_namespace_declarations = file_scoped:warning +dotnet_diagnostic.IDE0161.severity = warning +dotnet_sort_system_directives_first = true +dotnet_diagnostic.IDE0005.severity = warning + +# Braces and layout (Allman; braces optional for single-line statements) +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_prefer_braces = when_multiline:suggestion + +# var and expression bodies +csharp_style_var_for_built_in_types = true:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion +csharp_style_var_elsewhere = true:suggestion +csharp_style_expression_bodied_methods = when_on_single_line:suggestion +csharp_style_expression_bodied_properties = true:suggestion +csharp_style_expression_bodied_operators = when_on_single_line:suggestion + +# Modern language features +csharp_style_prefer_primary_constructors = true:suggestion +dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion +csharp_style_prefer_pattern_matching = true:suggestion +csharp_style_prefer_switch_expression = true:suggestion + +# Naming: private fields are _camelCase +dotnet_naming_rule.private_fields_underscore.symbols = private_fields +dotnet_naming_rule.private_fields_underscore.style = underscore_camel +dotnet_naming_rule.private_fields_underscore.severity = suggestion +dotnet_naming_symbols.private_fields.applicable_kinds = field +dotnet_naming_symbols.private_fields.applicable_accessibilities = private +dotnet_naming_style.underscore_camel.required_prefix = _ +dotnet_naming_style.underscore_camel.capitalization = camel_case diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4fee963 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,57 @@ +name: CI + +on: + push: + branches: [master, "refactor/**"] + pull_request: + +permissions: + contents: read + +jobs: + build: + name: Build and test (${{ matrix.os }}) + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + runs-on: ${{ matrix.os }} + env: + DOTNET_NOLOGO: true + DOTNET_CLI_TELEMETRY_OPTOUT: true + + steps: + - uses: actions/checkout@v7 + + - uses: actions/setup-dotnet@v6 + with: + global-json-file: global.json + + - name: Restore + run: dotnet restore SharpMath.slnx + + # Builds everything, including the WinForms projects (EnableWindowsTargeting), samples and benchmarks. + - name: Build + run: dotnet build SharpMath.slnx --configuration Release --no-restore + + # The WinForms tests only run on Windows; elsewhere their project is not a test project. + - name: Test + run: dotnet test --solution SharpMath.slnx --configuration Release --no-build + + - name: Collect coverage + if: runner.os == 'Linux' + run: | + dotnet test --project tests/SharpMath.Tests --configuration Release --no-build --coverage --coverage-output-format cobertura --coverage-output core.cobertura.xml --results-directory coverage + dotnet test --project tests/SharpMath.Canvas.Tests --configuration Release --no-build --coverage --coverage-output-format cobertura --coverage-output canvas.cobertura.xml --results-directory coverage + + - name: Check coverage (at least 90 % line coverage) + if: runner.os == 'Linux' + run: dotnet run eng/CheckCoverage.cs -- 90 coverage/core.cobertura.xml=SharpMath coverage/canvas.cobertura.xml=SharpMath.Canvas + + - name: Upload coverage + if: runner.os == 'Linux' && always() + uses: actions/upload-artifact@v7 + with: + name: coverage + path: coverage/*.cobertura.xml + if-no-files-found: ignore diff --git a/.gitignore b/.gitignore index 186a0dd..b854af4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,24 @@ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. +## +## Get latest from `dotnet new gitignore` + +# dotenv files +.env # User-specific files +*.rsuser *.suo *.user *.userosscache *.sln.docstates +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + # Build results [Dd]ebug/ [Dd]ebugPublic/ @@ -14,44 +26,79 @@ [Rr]eleases/ x64/ x86/ -build/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ bld/ [Bb]in/ [Oo]bj/ +[Ll]og/ +[Ll]ogs/ -# Roslyn cache directories -*.ide/ +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* -#NUNIT +# NUnit *.VisualState.xml TestResult.xml +nunit-*.xml # Build Results of an ATL Project [Dd]ebugPS/ [Rr]eleasePS/ dlldata.c +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET +project.lock.json +project.fragment.lock.json +artifacts/ + +# Tye +.tye/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio *_i.c *_p.c -*_i.h +*_h.h *.ilk *.meta *.obj +*.iobj *.pch +*.pdb +*.ipdb *.pgc *.pgd *.rsp +# but not Directory.Build.rsp, as it configures directory-level build defaults +!Directory.Build.rsp *.sbr *.tlb *.tli *.tlh *.tmp *.tmp_proj +*_wpftmp.csproj *.log +*.tlog *.vspscc *.vssscc .builds @@ -66,14 +113,21 @@ _Chutzpah* ipch/ *.aps *.ncb +*.opendb *.opensdf *.sdf *.cachefile +*.VC.db +*.VC.VC.opendb # Visual Studio profiler *.psess *.vsp *.vspx +*.sap + +# Visual Studio Trace Files +*.e2e # TFS 2012 Local Workspace $tf/ @@ -86,18 +140,29 @@ _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user -# JustCode is a .NET coding addin-in -.JustCode - # TeamCity is a build add-in _TeamCity* # DotCover is a Code Coverage Tool *.dotCover +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + # NCrunch _NCrunch_* .*crunch*.local.xml +nCrunchTemp_* # MightyMoose *.mm.* @@ -125,39 +190,71 @@ publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml -# TODO: Comment the next line if you want to checkin your web deploy settings +# Note: Comment the next line if you want to checkin your web deploy settings, # but database connection strings (with potential passwords) will be unencrypted *.pubxml *.publishproj +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + # NuGet Packages *.nupkg +# NuGet Symbol Packages +*.snupkg # The packages folder can be ignored because of Package Restore -**/packages/* +**/[Pp]ackages/* # except build/, which is used as an MSBuild target. -!**/packages/build/ +!**/[Pp]ackages/build/ # Uncomment if necessary however generally it will be regenerated when needed -#!**/packages/repositories.config +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets -# Windows Azure Build Output +# Microsoft Azure Build Output csx/ *.build.csdef -# Windows Store app package directory +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ # Others -*.[Cc]ache ClientBin/ -[Ss]tyle[Cc]op.* ~$* *~ *.dbmdl *.dbproj.schemaview +*.jfm *.pfx *.publishsettings -node_modules/ -bower_components/ +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ # RIA/Silverlight projects Generated_Code/ @@ -169,24 +266,222 @@ _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak # SQL Server files *.mdf *.ldf +*.ndf # Business Intelligence projects *.rdl.data *.bim.layout *.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl # Microsoft Fakes FakesAssemblies/ +# GhostDoc plugin setting file +*.GhostDoc.xml + # Node.js Tools for Visual Studio .ntvs_analysis.dat +node_modules/ # Visual Studio 6 build log *.plg # Visual Studio 6 workspace options file -*.opt \ No newline at end of file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml +.idea/ + +## +## Visual studio for Mac +## + + +# globs +Makefile.in +*.userprefs +*.usertasks +config.make +config.status +aclocal.m4 +install-sh +autom4te.cache/ +*.tar.gz +tarballs/ +test-results/ + +# content below from: https://github.com/github/gitignore/blob/main/Global/macOS.gitignore +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# content below from: https://github.com/github/gitignore/blob/main/Global/Windows.gitignore +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# Vim temporary swap files +*.swp + +# SharpMath +/coverage/ +/output/ +/samples/SharpMath.Samples.Export/output/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..cc03814 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,255 @@ +# Changelog + +All notable changes to SharpMath are documented in this file. The format is based on +[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project follows [Semantic Versioning](https://semver.org/). + +## [2.0.0] - Unreleased + +SharpMath 2.0 is a rewrite for .NET 10. Almost every public type changed. The sections below double as a migration +guide from 1.x. + +### Breaking changes + +#### Platform and packaging + +- Targets `net10.0` (C# 14) instead of .NET Framework 4.0. The WinForms parts target `net10.0-windows`. +- The library is split into five assemblies: `SharpMath`, `SharpMath.Canvas`, `SharpMath.Canvas.Skia`, + `SharpMath.Canvas.WinForms` and `SharpMath.Canvas.Avalonia`. The core no longer references WinForms or System.Drawing. +- Removed `[Serializable]`, `[assembly: ComVisible(true)]` and the GUID. `BinaryFormatter` no longer exists in .NET. +- Namespaces: + - `SharpMath.Presentation` → `SharpMath.Canvas` + - `SharpMath.Equations.Exceptions` → `SharpMath.Equations` + - `SharpMath.Expressions.Exceptions` → `SharpMath.Expressions` + +#### Value types are immutable + +- `Vector2/3/4`, `Point2D/3D`, `Matrix2x2/3x3/4x4`, `Line2D`, `Line3D`, `LineSegment2D`, `BoundingBox2D` and + `ProjectionData` are `readonly struct`s. Settable properties and indexers are gone. Use the constructors or factory + methods instead of `new Matrix2x2 { [0, 0] = 8, … }`. Vectors, points, matrices and segments support `with` + (`v with { X = 1 }`, `m with { M14 = 5 }`); lines, bounding boxes and projection data validate or normalize their + values and are created through their constructors or factories only. +- Indexers take `int` instead of `uint` and are read-only. They throw `ArgumentOutOfRangeException` (was + `IndexOutOfRangeException`), and the 2D indexers of matrices now check row and column separately. +- Vectors, points and matrices no longer implement `IEnumerable`. Use the indexers or the properties. +- `Polygon` is immutable and no longer implements `IEnumerable`. `Points` is a `ReadOnlyCollection`. +- `LinearEquation` and `LinearEquationSystem` are immutable. `LinearEquation` has a single constructor, + `new LinearEquation([a₁, a₂, …], result)`. The parameterless constructors and `LinearEquation(double result, params + double[] coefficients)` were removed. +- `Polygon`, `LinearEquation` and `LinearEquationSystem` are `sealed`. +- `Dimension`, `RowCount` and `ColumnCount` are `int`. +- `Equals(object)` of a vector no longer accepts other `IVector` types. + +#### Renamed and reshaped members + +| 1.x | 2.0 | +|---|---| +| `Magnitude`, `SquareMagnitude` | `Length`, `LengthSquared` | +| `DotProduct` | `Dot` | +| `VectorProduct` | `Cross` (`Vector3` → `Vector3`, `Vector2` → `double`) | +| `CheckForOrthogonality`, `CheckForOrthonormality`, `CheckForParallelism` | `IsOrthogonalTo`, `IsOrthonormalTo`, `IsParallelTo` | +| `Vector2.AreOrthogonal/AreOrthonormal/AreParallel(a, b)` | `a.IsOrthogonalTo(b)` etc. | +| `Vector2.Area`, `Vector3.Area` | `ParallelogramArea` | +| `Vector2(Point2D bottom, Point2D tip)` | `tip - bottom` | +| `Point2D.PositionVector`, `new Point2D(Vector2)` | `point.ToVector()`, `vector.ToPoint()` | +| `Vector2.Divide(vector, scalar)` | `vector / scalar` | +| `vector.Transform(matrix)`, `Vector2.Transform(matrix, vector)` | `matrix * vector` or `Vector2.Transform(vector, matrix)` | +| `Vector3.FromVector(IVector)`, `Convert()` | constructors, e.g. `new Vector3(v2, z)`, `new Vector4(v3, w)`, `Vector3.FromHomogeneous(v4)` | +| `vector.Negate()` | `-vector` | +| `Point2D + Point2D` | `Point2D + Vector2` (point − point is a vector) | +| `Vector2 * Vector2` (dot product) | `Dot` | +| `Matrix3x3/4x4.Scalation` | `Scaling` | +| `Inverse`, `Transpose`, `Adjugate`, `CofactorMatrix` (properties) | `Inverse()`, `Transpose()`, `Adjugate()`, `Cofactors()`; plus `TryInvert(out m)` | +| `Negate` (matrix property) | unary `-` | +| `IsTriangle` | `IsTriangular` | +| `IsAntiSymmetric` | `IsSkewSymmetric` (they were identical) | +| `Matrix4x4.View` | `Matrix4x4.LookAt` | +| `vector * matrix` | `matrix * vector` (column vectors, see below) | +| `new Line2D(slope, offset)`, `Offset` | `Line2D.FromSlopeIntercept(slope, yIntercept)`, `YIntercept` | +| `new Line2D(pointA, pointB)` | `Line2D.FromPoints(pointA, pointB)` | +| `Line2D.GetPoint(x)` | `SolveForY(x)` | +| `IsPointOnLine` | `Contains` | +| `IntersectsWith`, `AreIntersecting`, `GetIntersectionPoint` | `Intersect` returning `LineIntersection2D`/`LineIntersection3D` with a `Kind` | +| `Line2D.AreParallel(a, b)` | `a.IsParallelTo(b)` | +| `new Line3D(point, direction)` | `Line3D.FromPointAndDirection(point, direction)` | +| `Line3D.GetPoint(t)` | `PointAt(distance)` (the direction is now a unit vector) | +| `Polygon.ContainsPoint` | `Polygon.Contains` | +| `Polygon.Center` | `Polygon.Centroid` | +| `Polygon.BoundingBox` (`RectangleF`) | `BoundingBox2D` | +| `ProjectionData(Size, float aspect, float fov, float near, float far)` | `ProjectionData(double width, double height, double aspectRatio, double fieldOfView, double nearPlane, double farPlane)` | +| `new Parser(text).Evaluate()` | `MathExpression.Parse(text).Evaluate()` | +| `FloatingNumber`, `FloatingNumberExtensions` | `Tolerance` | +| `MathHelper.Pi`, `E`, `TwoPi`, `DegreesToRadians`, … | `Math.PI`, `Math.E`, `Math.Tau`, `double.DegreesToRadians`, `double.RadiansToDegrees` | +| `EquationNotSolvableException` for singular `Inverse` | `InvalidOperationException` | +| `DimensionException` | `ArgumentException` | + +#### Removed + +- The unfinished `SharpMath.Analysis` namespace (it was never compiled), all commented-out code, and unused helpers. +- `Matrix1x1`, `Matrix1x3`, `Matrix3x1`, `Matrix1x4`, `Matrix4x1`, `ISquareMatrix`, `MatrixUtils`, `VectorUtils`, + `MatrixEnumerator`, `AsHorizontalMatrix`, `AsVerticalMatrix`. `IVector`, `IMatrix` and `ISquareMatrix` remain as + read-only interfaces. +- `Algorithms` (shunting yard), `Token`, `Token`, `TokenType`, `TokenEx`, `CharEx` and the `protected static` + operator dictionaries. Extend the parser with an `ExpressionContext` instead. +- `MathHelper`. + +#### Canvas + +- The canvas is no longer a WinForms control. `SharpMath.Canvas.Canvas2D` is a platform-neutral model, and + `Canvas2DControl` (WinForms) and `Canvas2DView` (Avalonia) display it through their `Canvas` property. +- `Functions` (`BindingList`) and `Vectors` (`BindingList`) became `Items` + (`ObservableCollection`) with `AddFunction`, `AddVector`, `AddPoint`, `AddLine`, `AddSegment` and + `AddPolygon`. `FunctionWrapper` was removed. +- `GridOrigin`, `GridSize` and `GridScale` became `Viewport` (`Origin`, `ScaleX`, `ScaleY`, `SetOrigin`, `SetScale`, + `Pan`, `ZoomAt`). +- `TrackingLines` and `ValueIndicator` became `ShowTrackingLines` and `ShowValueIndicator`. +- Colors, fonts and line widths are no longer hard-coded but configured through `CanvasStyle` and `ItemStyle`. + +### Behaviour changes + +These follow the rule "mathematical definition first, then `System.Numerics`, then least surprise". + +- **Matrix convention:** column vectors everywhere (`v' = M · v`). `Matrix4x4.LookAt`, `World` and + `PerspectiveFieldOfView` used a row-vector (Direct3D) layout and are now transposed. The `Matrix3x3`/`Matrix4x4` + constructors that take vectors place them as rows, as documented (they used to become columns). The projection + stays right-handed with a depth range of [0, 1], and its `M44` is now 0. +- `Vector3.Forward` is (0, 0, −1) and `Vector3.Back` is (0, 0, 1), matching the right-handed convention. +- Equality is exact on every type and follows `System.Numerics`: `==` compares components with IEEE semantics (NaN is + not equal to NaN), `Equals` treats NaN as equal and is consistent with `GetHashCode`. Use `ApproximatelyEquals` + (new on vectors, points, matrices, lines, segments, polygons and bounding boxes) for tolerance-based comparisons. +- Predicates no longer use one absolute epsilon of `1.6e-9`: + - `IsIdentity`, `IsZero`, `IsNormalized` and the `Contains` checks use a combined tolerance (absolute `1e-12`, + relative `1e-9`). + - `IsDiagonal`, `IsTriangular`, `IsSymmetric` and `IsSkewSymmetric` compare entries relative to the largest entry, + and `IsOrthogonal` compares `M · Mᵀ` with the identity. + - `IsParallelTo` and `IsOrthogonalTo` compare the sine or cosine of the angle with `1e-9`, independent of the vector + lengths. + - `IsSingular` uses an LU decomposition with partial pivoting: a matrix is singular if a pivot is at most + `n · ε · s` (`s` = largest absolute entry), the same rule `MatrixMxN` uses. Scaled identities such as + `0.001 · I` and transformations with large translations are regular; matrices whose rows differ by many orders of + magnitude can be numerically singular. +- `Inverse()` throws for singular matrices; `TryInvert` does not throw. +- `IsDiagonal` and `IsTriangular` follow the mathematical definitions: a zero on the diagonal does not matter. +- The zero vector counts as parallel and orthogonal to every vector. +- `Line3D` is stored canonically: `Point` is the point of the line closest to the origin and `Direction` is a unit + vector whose first non-zero component is positive, so `Line3D.FromPoints(p, q).Point` is generally not `p`. +- `ToString()` uses the invariant culture everywhere; matrices print as `Matrix2x2 [[1, 2], [3, 4]]`. +- `Matrix3x3 * Vector2` divides by the resulting W component if it is not 1 (projective transformations). +- `ProjectionData` requires a field of view below π and a positive aspect ratio and throws + `ArgumentOutOfRangeException`. +- `Vector3.ScalarTripleProduct` returns the signed value `a · (b × c)`; it used to return the absolute value. +- `Normalize()` of the zero vector throws `InvalidOperationException` (it returned NaN). It also normalizes very small + and very large vectors correctly. +- `MoveTowards` stops at the target instead of overshooting. +- `LinearEquationSystem.Solve` requires exactly one coefficient per equation and throws `ArgumentException` otherwise. + Too many coefficients used to throw `IndexOutOfRangeException`, and too few were silently padded with zeros. +- Expressions: + - `!` is the factorial; it used to be the parser's internal negation marker. + - Functions require parentheses. + - Letters that are not functions or constants are variables. + - Multiplication may be implicit. + - Every syntax error is a `ParserException` with a `Position`, including empty input (it was `System.Exception`) + and unbalanced brackets (they were `InvalidOperationException` or silently ignored). + - Two or more letters directly in front of `(` must name a function (or end with a known constant or declared + variable), so `cot(x)` reports an unknown function instead of multiplying `c·o·t·x`. Write `x*y*(2)` or `x y(2)` + for products of single-letter variables. + - Input nested more than 256 levels deep or with more than 4096 nested operations is rejected with a + `ParserException` instead of overflowing the stack. +- Canvas: axis labels use the invariant culture on both axes; ticks use "nice" steps (1, 2, 5 × 10ⁿ) and labels are + always placed on grid lines; labels that would be cut off at the edges are skipped; the value indicator shows as many + decimals as the zoom level needs; zooming is limited to 10⁻⁶–10⁹ pixels per unit. + +### Fixed + +- `Matrix2x2/3x3/4x4.Inverse` returned the identity matrix, because the Gauss-Jordan helpers mutated boxed copies. + Singular matrices also returned the identity. +- Gauss-Jordan searched for the pivot in the wrong cell and did not pivot partially, so some regular systems were + reported as unsolvable. +- `Matrix2x2.IsIdentity` checked `M22 == 0`. `Matrix3x3/4x4.IsIdentity` ignored off-diagonal entries (and `M44`). +- `MatrixEnumerator` enumerated in the wrong order and threw for 1x3/1x4 matrices. +- `Matrix1x1` equality was approximate while its hash code was exact. +- `IsOrthogonal`, `IsSymmetric` and `IsSkewSymmetric` compared exactly, so real rotation matrices were "not orthogonal". +- `Matrix1x1.CofactorMatrix` and `Adjugate` threw. +- `Vector3.Transform` divided Z by W twice. +- `Matrix4x4.View` lost the camera translation and did not normalize its basis. `World` lost the position and ignored + the results of `Normalize`. +- `vector * matrix` silently computed `matrix * vector`. +- `ProjectionData` did not validate the aspect ratio, and the field-of-view error message mentioned 360°. +- `AsHorizontalMatrix`/`AsVerticalMatrix` accepted wrong target types. +- `Vector4.UnitW` was (0, 0, 1, 0). +- `CheckForParallelism` returned false whenever a component was zero. +- `Angle` returned NaN for (nearly) parallel vectors. +- `MoveTowards` returned NaN for equal vectors. +- `Line3D.IsPointOnLine` was wrong for directions with zero components. +- `Line2D` could not represent vertical lines, and coincident lines reported no intersection. Refs #9: lines can now + be created from the general form `Ax + By + C = 0`. +- Comparing two different polygons with `==` crashed with a stack overflow. +- `Polygon.GetHashCode` differed for equal polygons. +- `Polygon.Area`, `Center` and `BoundingBox` were wrong for polygons that do not contain the origin. +- `LinearEquationSystem.Solve` validated the number of equations instead of coefficients. +- Expressions: + - Unary minus replaced the first `-` in the whole string. + - Unary `+` was ignored incorrectly. + - Signs after operators (`2*-3`, `2^-1`) failed. + - `%` and `log(x, base)` could not be used. + - `.5` and `1e5` could not be parsed. + - Unbalanced brackets were ignored or crashed. + - Names with a common prefix (e.g. `sinh`) could not be added. +- Canvas (Fixes #3): + - Zooming moved the origin by 1.25 times the distance to the cursor, so the point under the cursor drifted until + coordinates overflowed. + - Coordinates passed to GDI+ were not clipped. + - The tick algorithm normalized large and small values incorrectly and threw for a height of 0. + - A grid size of 0 made the drawing loops hang. + - Function graphs were connected across poles. + - `FunctionWrapper` cached every sampled value forever. + - Pens and brushes were not disposed. + - An origin of (0, 0) could not be set. + +### Added + +- `Tolerance` with `AreEqual`, `IsZero` and `IsNegligible`, and `ApproximatelyEquals` (with optional tolerances) on + vectors, points, matrices, lines, segments, polygons and bounding boxes. +- `MatrixMxN`: an immutable matrix of any size with LU-based determinant, inverse, rank and `Solve`. +- `LineSegment2D`, `BoundingBox2D`, `IntersectionKind`, `LineIntersection2D`, `LineIntersection3D` and + `SegmentIntersection2D`. +- `Line2D`: + - factories `FromGeneralForm`, `FromSlopeIntercept`, `FromPoints`, `FromPointAndDirection` + - `A`, `B`, `C`, `Normal`, `Direction`, `IsVertical`, `IsHorizontal` + - `SolveForX`, `SignedDistanceTo`, `DistanceTo`, `ClosestPoint`, `PointAt` +- `Line3D`: `DistanceTo` (point and line), `ClosestPoint`, `ClosestPoints`, `IsParallelTo`, and `Intersect` + including skew lines. +- Uniform operators on all vectors (`Vector4` had none): `+`, `-`, unary `-`, `v * s`, `s * v`, `v / s`. +- `Vector3(Vector2, z)`, `Vector4(Vector3, w)`, `Vector3.FromHomogeneous`, `ToPoint`, `Point2D/3D.ToVector`, + `Point2D/3D.Distance` and `DistanceSquared` (instance and static). +- `Matrix4x4.Translation(Vector3)`, `Matrix2x2.Rotation/Scaling`, `Matrix3x3.Scaling(scale)`, and `ToString()` for + all matrices. +- Expressions: + - `MathExpression` with variables, `Evaluate(name, value)`, `Evaluate(dictionary)` and an allocation-free `ToFunc` + - `ExpressionContext` for custom functions, constants and multi-letter variables, with `Clone()` + - more built-ins: `atan2 sinh cosh tanh log2 exp floor ceil round sign pow min max tau` +- Canvas: + - the platform-neutral `Canvas2D` model with `Viewport`, `CanvasStyle`, `ItemStyle` and the `Changed` event + - items for points, infinite lines, segments, polygons and vectors with any tail + - functions from expression strings, optionally with an `ExpressionContext` + - the renderer interface `IDrawingContext`, with an SVG renderer (`RenderSvg`), a SkiaSharp renderer (`RenderPng`, + `RenderImage`) and a GDI+ renderer + - an Avalonia control (`Canvas2DView`) for Windows, Linux and macOS +- Samples for WinForms, Avalonia and headless export; BenchmarkDotNet benchmarks (`doc/benchmarks.md`); xUnit tests + with property-based tests (CsCheck), zero-allocation tests and a coverage gate; GitHub Actions on Windows, Linux and + macOS. Refs #5: the test suite now runs on all three platforms (see "Platform support and determinism" in the + README). + +### Performance + +- Closed-form determinants, adjugates and inverses for 2x2 to 4x4 matrices instead of recursive Laplace expansion. +- Unrolled, non-boxing operators. Equality and hash codes no longer go through enumerators and LINQ. +- Common geometry and expression operations no longer allocate. + +See [doc/benchmarks.md](doc/benchmarks.md) for measurements. + +## [1.14.7] and earlier + +Releases before 2.0 targeted .NET Framework 4.0 and are not documented in this file. + +[2.0.0]: https://github.com/dbforge/SharpMath/compare/66339fd...refactor/v2 +[1.14.7]: https://github.com/dbforge/SharpMath/tree/66339fd diff --git a/Canvas2D.png b/Canvas2D.png deleted file mode 100644 index b21aca0..0000000 Binary files a/Canvas2D.png and /dev/null differ diff --git a/Canvas2DFunctions.png b/Canvas2DFunctions.png deleted file mode 100644 index 4b0cc33..0000000 Binary files a/Canvas2DFunctions.png and /dev/null differ diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..c240c55 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,23 @@ + + + net10.0 + 14 + enable + enable + true + latest-recommended + true + true + true + false + + 2.0.0 + Dominic Beger + Copyright © Dominic Beger 2015-2026 + MIT + https://github.com/dbforge/SharpMath + https://github.com/dbforge/SharpMath + git + math;vector;matrix;geometry;linear-equations;expression-parser;plotting;canvas + + diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 0000000..f34a449 --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,21 @@ + + + true + + + + + + + + + + + + + + + + + + diff --git a/README.md b/README.md index 94a0368..dddb1b6 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,225 @@ # SharpMath -SharpMath is a C# math library supporting vectors, matrices, linear equation systems, expression parsing and soon more advanced analysis. -Specials thanks go to [NikxDa](https://github.com/nikxda) who wrote the Canvas2D control for representing vectors and functions graphically. -## Overview -![Class Diagramm](https://github.com/ProgTrade/SharpMath/blob/master/SharpMath.png) +SharpMath is a .NET math library: vectors, points, matrices, lines, segments and polygons, linear equation systems and +an expression parser. It also has a 2D canvas that plots functions and geometry and renders them through GDI+, +SkiaSharp or SVG, or shows them interactively in WinForms and Avalonia. -## 2D and 3D-Geometry -SharpMath offers many structs, such as points, vectors, lines and matrices. These are basically also used in games/computer graphics, except that they're a bit more extensive and sophisticated. Thus, the game engines provide some more flexibility and possibilites than SharpMath. -Nevertheless, you can perform simple operations (such as rotating, scaling or translating objects defined through vertices) in space and project everything onto a two-dimensional surface. +![Functions plotted with SharpMath](doc/images/canvas-functions.png) -![3D-Sample](https://trade-programming.de/pixelkram/sharpmath_projection.png) +> **SharpMath 2.0** is a rewrite for .NET 10 with many breaking changes. See [CHANGELOG.md](CHANGELOG.md) for a +> migration guide from 1.x. + +## Packages + +| Assembly | What it contains | Dependencies | Platforms | +|---|---|---|---| +| `SharpMath` | Geometry, equations, expressions | none | all | +| `SharpMath.Canvas` | Canvas model, styles, renderer interface, SVG renderer | `SharpMath` | all | +| `SharpMath.Canvas.Skia` | SkiaSharp renderer, PNG export | `SkiaSharp` | Windows, Linux, macOS | +| `SharpMath.Canvas.WinForms` | WinForms control, GDI+ renderer | Windows Desktop | Windows | +| `SharpMath.Canvas.Avalonia` | Avalonia control | `Avalonia`, `SharpMath.Canvas.Skia` | Windows, Linux, macOS | + +```mermaid +graph LR + Core[SharpMath] + Canvas[SharpMath.Canvas
model + SVG] + Skia[SharpMath.Canvas.Skia
SkiaSharp + PNG] + WinForms[SharpMath.Canvas.WinForms
GDI+ control] + Avalonia[SharpMath.Canvas.Avalonia
Avalonia control] + Canvas --> Core + Skia --> Canvas + WinForms --> Canvas + Avalonia --> Skia +``` + +The packages are not published on NuGet yet. Reference the projects from source or build them with `dotnet pack`. + +## Geometry + +All vectors, points, matrices and lines are immutable `readonly struct`s with `System.Numerics`-style names. Equality +(`==`, `Equals`, `GetHashCode`) is exact; `ApproximatelyEquals` and all predicates such as `IsOrthogonal` or +`IsParallelTo` use the tolerances from `SharpMath.Tolerance` (absolute `1e-12`, relative `1e-9`). + +```csharp +using SharpMath.Geometry; + +var a = new Vector3(1, 2, 3); +var b = new Vector3(-2, 0.5, 4); +var normal = a.Cross(b).Normalize(); +var angle = a.Angle(b); // radians +var moved = a with { Z = 0 }; // modify a copy + +var p = new Point2D(1, 2); +var q = p + new Vector2(3, 4); // point + vector = point +var direction = q - p; // point − point = vector +``` + +> **Column vectors.** SharpMath uses the textbook convention `v' = M · v`: write `matrix * vector`, and transformations +> compose from right to left. `T * R * S * v` scales first, then rotates, then translates. `System.Numerics` uses row +> vectors and composes the other way round. + +```csharp +var model = Matrix4x4.Translation(0, 0, -5) * Matrix4x4.RotationY(Math.PI / 4) * Matrix4x4.Scaling(2); +var view = Matrix4x4.LookAt(eye: new Vector3(0, 2, 5), target: Vector3.Zero, up: Vector3.Up); +var projection = Matrix4x4.PerspectiveFieldOfView(Math.PI / 3, aspectRatio: 16.0 / 9, nearPlane: 0.1, farPlane: 100); +var clip = projection * view * model * new Vector3(1, 1, 1); // includes the perspective divide + +var m = new Matrix3x3(2, 5, 2, 3, -3, 1, 1, 4, -4); // row-major +double det = m.Determinant; // 111, closed form +if (m.TryInvert(out var inverse)) + Console.WriteLine(m * inverse); // ≈ identity +``` + +The projection is right-handed with a depth range of [0, 1]; the camera looks along −Z (`Vector3.Forward`). + +For larger matrices, `MatrixMxN` offers products, transpose, determinant, inverse, rank and `Solve` via an LU +decomposition with partial pivoting. `IsSingular`, `Inverse()` and `Solve` all use the same rule: a matrix is singular +if a pivot of the LU decomposition is at most `n · ε` times its largest entry. + +### Lines, segments and polygons + +```csharp +// Ax + By + C = 0, also for vertical lines +var line = Line2D.FromGeneralForm(4.0 / 3, -1, 1.0 / 3); +double y = line.SolveForY(2); +var other = Line2D.FromPoints(new Point2D(1, 0), new Point2D(1, 5)); // vertical + +var intersection = line.Intersect(other); +if (intersection.Kind == IntersectionKind.Point) + Console.WriteLine(intersection.Point); + +var segment = new LineSegment2D(new Point2D(0, 0), new Point2D(4, 4)); +var hit = segment.Intersect(new LineSegment2D(new Point2D(0, 4), new Point2D(4, 0))); // Point (2, 2) + +var line3 = Line3D.FromPoints(new Point3D(0, 0, 0), new Point3D(1, 1, 0)); +var kind = line3.Intersect(Line3D.FromPointAndDirection(new Point3D(0, 0, 3), Vector3.UnitY)).Kind; // Skew + +var polygon = new Polygon(new Point2D(0, 0), new Point2D(4, 0), new Point2D(4, 4), new Point2D(2, 1), new Point2D(0, 4)); +Console.WriteLine($"{polygon.Area} {polygon.Perimeter} {polygon.Centroid} {polygon.Contains(new Point2D(1, 1))}"); +``` ## Linear equation systems -SharpMath can solve linear equation systems using the Gauss-Jordan algorithm internally by representing the equations as matrices. -## Expression Parsing -SharpMath knows most of the functions, constants and operators used in computations. You can provide the parser with an input string containing your expression and it will return the result when calling the corresponding function. The flexibility of the parser allows you to add further operations. This feature may be used for calculators. +```csharp +using SharpMath.Equations; + +// x − y + 2z = 6, 2x + 3y + 2z = 11, 3x + 2y + z = 8 +var system = new LinearEquationSystem( + new LinearEquation([1.0, -1, 2], 6), + new LinearEquation([2.0, 3, 2], 11), + new LinearEquation([3.0, 2, 1], 8)); +double[] solution = system.Solve(); // [1, 1, 3] +``` + +`Solve` throws an `EquationNotSolvableException` if the system has no unique solution. + +## Expressions + +`MathExpression` parses an expression once and evaluates it as often as needed, without allocating. + +```csharp +using SharpMath.Expressions; + +double value = MathExpression.Parse("3*4^2 + sin(pi/2)").Evaluate(); // 49 + +var expression = MathExpression.Parse("2x^2 - 3x + 1"); // implicit multiplication +Func f = expression.ToFunc("x"); +double y = f(1.5); + +var context = new ExpressionContext() + .AddFunction("sec", x => 1 / Math.Cos(x)) + .AddConstant("g", 9.81) + .DeclareVariable("t"); +var fall = MathExpression.Parse("g t^2 / 2", context); +double distance = fall.Evaluate("t", 3); +``` + +The parser supports: +- numbers like `1.5`, `.5` and `1e5` +- the operators `+ - * / % ^` and postfix `!`; `^` is right-associative, and `-2^2 = -4` +- parentheses and implicit multiplication: `2x`, `3(x+1)`, `(x+1)(x-1)` +- the functions `sin cos tan asin acos atan atan2 sinh cosh tanh sqrt abs ln lg log(x, base) log2 exp floor ceil round sign pow min max` +- the constants `pi`, `e` and `tau` + +Names are case-insensitive. A sequence of letters is split by longest match against the known names, so `pix` is +`pi·x`. Two or more letters directly in front of `(` must name a function: `cot(x)` is an error unless `cot` was added +to the context. Write `x*y*(2)` or `x y(2)` for products of single-letter variables. Every syntax error is a +`ParserException` with the `Position` of the problem. Use `context.Clone()` to derive variants of a configured +context. + +## Canvas + +![Geometry on the canvas](doc/images/canvas-geometry.png) + +`Canvas2D` is a platform-neutral model. It holds the viewport (pan and zoom), the grid with labelled ticks, the style +and the items: functions (delegates or expression strings), vectors, points, infinite lines, segments and polygons. +Renderers implement `IDrawingContext`; hosts forward mouse input to the model and redraw when it raises `Changed`. + +```csharp +using SharpMath.Canvas; +using SharpMath.Expressions; +using SharpMath.Geometry; + +var canvas = new Canvas2D { ShowTrackingLines = true }; +canvas.AddFunction("x^3 / 10 - x"); +canvas.AddFunction("sec(x)", context: new ExpressionContext().AddFunction("sec", x => 1 / Math.Cos(x))); +canvas.AddFunction(Math.Sin, new ItemStyle(new Stroke(Color.FromRgb(200, 0, 0), Width: 2, DashStyle.Dashed))); +canvas.AddVector(new Vector2(4, 3)); +canvas.AddPolygon(new Polygon(new Point2D(2, -2), new Point2D(6, -2), new Point2D(4, -5))); +canvas.Style = canvas.Style with { AxisStroke = new Stroke(Color.Black) }; +``` + +Ways to display or export a canvas: +- **SVG**, without dependencies: `string svg = canvas.RenderSvg(800, 600);` +- **PNG** via SkiaSharp: `canvas.RenderPng(stream, 800, 600);`, or `canvas.RenderImage(...)` for an `SKImage` +- **WinForms**: place a `Canvas2DControl` on a form and use its `Canvas` property. It renders with GDI+. +- **Avalonia**: place a `Canvas2DView` in a window and use its `Canvas` property. It renders with Skia. + +In both controls, drag with the left mouse button to pan and use the mouse wheel to zoom around the pointer. + +## Samples + +| Sample | Description | +|---|---| +| `samples/SharpMath.Samples.Canvas.WinForms` | Interactive canvas with an expression input (Windows) | +| `samples/SharpMath.Samples.Canvas.Avalonia` | The same, cross-platform | +| `samples/SharpMath.Samples.Perspective.WinForms` | A rotating wireframe cube using `LookAt` and the perspective projection (Windows) | +| `samples/SharpMath.Samples.Polygon.WinForms` | `Polygon.Contains` following the mouse pointer (Windows) | +| `samples/SharpMath.Samples.Export` | Renders PNG and SVG without UI; it generates the images in this README | + +## Platform support and determinism + +SharpMath targets .NET 10 and is tested on Windows, Linux and macOS. The maths uses plain `double` arithmetic without +platform-specific SIMD code. Results are therefore as reproducible as .NET's floating-point and `System.Math` +implementations: elementary arithmetic is IEEE 754 and identical everywhere, but functions such as `Math.Sin` may +differ in the last bits between platforms and runtime versions. If you need bit-exact lockstep results (e.g. for +networked games), avoid transcendental functions in the simulated state. + +## Building and testing + +You need the .NET 10 SDK. + +```shell +dotnet build SharpMath.slnx +dotnet test --solution SharpMath.slnx + +# Coverage gate (at least 90 % line coverage for SharpMath and SharpMath.Canvas), as in CI +dotnet test --project tests/SharpMath.Tests --coverage --coverage-output-format cobertura --coverage-output core.cobertura.xml --results-directory coverage +dotnet test --project tests/SharpMath.Canvas.Tests --coverage --coverage-output-format cobertura --coverage-output canvas.cobertura.xml --results-directory coverage +dotnet run eng/CheckCoverage.cs -- 90 coverage/core.cobertura.xml=SharpMath coverage/canvas.cobertura.xml=SharpMath.Canvas + +# Benchmarks (results in doc/benchmarks.md) +dotnet run -c Release --project benchmarks/SharpMath.Benchmarks -- --filter "*" +``` + +The WinForms projects build on every platform, but their tests only run on Windows. + +## Credits + +Thanks to [NikxDa](https://github.com/nikxda), who wrote the original Canvas2D control, and to +[Stefan Baumann](https://github.com/stefan-baumann) and [Voon Foo](https://github.com/voonfoo) for their +contributions. -![Calculator](https://trade-programming.de/pixelkram/vstiebqhlj.png) +## License -## Canvas2D -SharpMath provides a two-dimensional canvas control for representing vectors and functions graphically in your application. It was written by [NikxDa](https://github.com/nikxda). -A three-dimensional representation (e.g. for vectors, points and lines) will follow as well. Thanks to the `Geometry`-namespace, the implementation of this one should not be too hard as the projection onto a two-dimensional surface can be done using the integrated matrices. -![Canvas2D](https://github.com/ProgTrade/SharpMath/blob/master/Canvas2D.png) +SharpMath is licensed under the [MIT License](LICENSE). diff --git a/SharpMath.3DTest/App.config b/SharpMath.3DTest/App.config deleted file mode 100644 index 0ac2de8..0000000 --- a/SharpMath.3DTest/App.config +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/SharpMath.3DTest/PerspectiveForm.Designer.cs b/SharpMath.3DTest/PerspectiveForm.Designer.cs deleted file mode 100644 index abd35c5..0000000 --- a/SharpMath.3DTest/PerspectiveForm.Designer.cs +++ /dev/null @@ -1,211 +0,0 @@ -namespace SharpMath._3DTest -{ - partial class PerspectiveForm - { - /// - /// Required designer variable. - /// - private System.ComponentModel.IContainer components = null; - - /// - /// Clean up any resources being used. - /// - /// true if managed resources should be disposed; otherwise, false. - protected override void Dispose(bool disposing) - { - if (disposing && (components != null)) - { - components.Dispose(); - } - base.Dispose(disposing); - } - - #region Windows Form Designer generated code - - /// - /// Required method for Designer support - do not modify - /// the contents of this method with the code editor. - /// - private void InitializeComponent() - { - this.components = new System.ComponentModel.Container(); - this.trackBar1 = new System.Windows.Forms.TrackBar(); - this.trackBar2 = new System.Windows.Forms.TrackBar(); - this.trackBar3 = new System.Windows.Forms.TrackBar(); - this.rotationTimer = new System.Windows.Forms.Timer(this.components); - this.autoRotateCheckBox = new System.Windows.Forms.CheckBox(); - this.label1 = new System.Windows.Forms.Label(); - this.label2 = new System.Windows.Forms.Label(); - this.label3 = new System.Windows.Forms.Label(); - this.label4 = new System.Windows.Forms.Label(); - this.scalationTrackBar = new System.Windows.Forms.TrackBar(); - this.perspectivePanel = new SharpMath._3DTest.PerspectivePanel(); - ((System.ComponentModel.ISupportInitialize)(this.trackBar1)).BeginInit(); - ((System.ComponentModel.ISupportInitialize)(this.trackBar2)).BeginInit(); - ((System.ComponentModel.ISupportInitialize)(this.trackBar3)).BeginInit(); - ((System.ComponentModel.ISupportInitialize)(this.scalationTrackBar)).BeginInit(); - this.SuspendLayout(); - // - // trackBar1 - // - this.trackBar1.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); - this.trackBar1.AutoSize = false; - this.trackBar1.Location = new System.Drawing.Point(118, 672); - this.trackBar1.Maximum = 360; - this.trackBar1.Name = "trackBar1"; - this.trackBar1.Size = new System.Drawing.Size(241, 37); - this.trackBar1.TabIndex = 1; - this.trackBar1.TickStyle = System.Windows.Forms.TickStyle.None; - this.trackBar1.Scroll += new System.EventHandler(this.trackBar1_Scroll); - // - // trackBar2 - // - this.trackBar2.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); - this.trackBar2.AutoSize = false; - this.trackBar2.Location = new System.Drawing.Point(118, 707); - this.trackBar2.Maximum = 360; - this.trackBar2.Name = "trackBar2"; - this.trackBar2.Size = new System.Drawing.Size(241, 42); - this.trackBar2.TabIndex = 2; - this.trackBar2.TickStyle = System.Windows.Forms.TickStyle.None; - this.trackBar2.Scroll += new System.EventHandler(this.trackBar2_Scroll); - // - // trackBar3 - // - this.trackBar3.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); - this.trackBar3.AutoSize = false; - this.trackBar3.Location = new System.Drawing.Point(118, 746); - this.trackBar3.Maximum = 360; - this.trackBar3.Name = "trackBar3"; - this.trackBar3.Size = new System.Drawing.Size(241, 37); - this.trackBar3.TabIndex = 3; - this.trackBar3.TickStyle = System.Windows.Forms.TickStyle.None; - this.trackBar3.Scroll += new System.EventHandler(this.trackBar3_Scroll); - // - // rotationTimer - // - this.rotationTimer.Interval = 40; - this.rotationTimer.Tick += new System.EventHandler(this.rotationTimer_Tick); - // - // autoRotateCheckBox - // - this.autoRotateCheckBox.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); - this.autoRotateCheckBox.Appearance = System.Windows.Forms.Appearance.Button; - this.autoRotateCheckBox.AutoSize = true; - this.autoRotateCheckBox.Location = new System.Drawing.Point(365, 673); - this.autoRotateCheckBox.Name = "autoRotateCheckBox"; - this.autoRotateCheckBox.Size = new System.Drawing.Size(118, 30); - this.autoRotateCheckBox.TabIndex = 5; - this.autoRotateCheckBox.Text = "Auto Rotation"; - this.autoRotateCheckBox.UseVisualStyleBackColor = true; - this.autoRotateCheckBox.CheckedChanged += new System.EventHandler(this.autoRotateCheckBox_CheckedChanged); - // - // label1 - // - this.label1.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); - this.label1.AutoSize = true; - this.label1.Location = new System.Drawing.Point(27, 678); - this.label1.Name = "label1"; - this.label1.Size = new System.Drawing.Size(85, 20); - this.label1.TabIndex = 6; - this.label1.Text = "RotationX:"; - // - // label2 - // - this.label2.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); - this.label2.AutoSize = true; - this.label2.Location = new System.Drawing.Point(27, 712); - this.label2.Name = "label2"; - this.label2.Size = new System.Drawing.Size(85, 20); - this.label2.TabIndex = 7; - this.label2.Text = "RotationY:"; - // - // label3 - // - this.label3.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); - this.label3.AutoSize = true; - this.label3.Location = new System.Drawing.Point(27, 750); - this.label3.Name = "label3"; - this.label3.Size = new System.Drawing.Size(84, 20); - this.label3.TabIndex = 8; - this.label3.Text = "RotationZ:"; - // - // label4 - // - this.label4.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); - this.label4.AutoSize = true; - this.label4.Location = new System.Drawing.Point(541, 676); - this.label4.Name = "label4"; - this.label4.Size = new System.Drawing.Size(79, 20); - this.label4.TabIndex = 10; - this.label4.Text = "Scalation:"; - // - // scalationTrackBar - // - this.scalationTrackBar.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); - this.scalationTrackBar.AutoSize = false; - this.scalationTrackBar.Location = new System.Drawing.Point(632, 672); - this.scalationTrackBar.Maximum = 360; - this.scalationTrackBar.Name = "scalationTrackBar"; - this.scalationTrackBar.Size = new System.Drawing.Size(241, 37); - this.scalationTrackBar.TabIndex = 9; - this.scalationTrackBar.TickStyle = System.Windows.Forms.TickStyle.None; - this.scalationTrackBar.Value = 360; - this.scalationTrackBar.Scroll += new System.EventHandler(this.scalationTrackBar_Scroll); - // - // perspectivePanel - // - this.perspectivePanel.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) - | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); - this.perspectivePanel.BackColor = System.Drawing.Color.CornflowerBlue; - this.perspectivePanel.Location = new System.Drawing.Point(0, 0); - this.perspectivePanel.Name = "perspectivePanel"; - this.perspectivePanel.Size = new System.Drawing.Size(1067, 659); - this.perspectivePanel.TabIndex = 0; - this.perspectivePanel.Paint += new System.Windows.Forms.PaintEventHandler(this.perspectivePanel_Paint); - // - // PerspectiveForm - // - this.AutoScaleDimensions = new System.Drawing.SizeF(9F, 20F); - this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.BackColor = System.Drawing.SystemColors.Window; - this.ClientSize = new System.Drawing.Size(1067, 790); - this.Controls.Add(this.label4); - this.Controls.Add(this.scalationTrackBar); - this.Controls.Add(this.label3); - this.Controls.Add(this.label2); - this.Controls.Add(this.label1); - this.Controls.Add(this.autoRotateCheckBox); - this.Controls.Add(this.trackBar3); - this.Controls.Add(this.trackBar2); - this.Controls.Add(this.trackBar1); - this.Controls.Add(this.perspectivePanel); - this.Name = "PerspectiveForm"; - this.Text = "SharpMath.3DTest"; - this.Load += new System.EventHandler(this.PerspectiveForm_Load); - ((System.ComponentModel.ISupportInitialize)(this.trackBar1)).EndInit(); - ((System.ComponentModel.ISupportInitialize)(this.trackBar2)).EndInit(); - ((System.ComponentModel.ISupportInitialize)(this.trackBar3)).EndInit(); - ((System.ComponentModel.ISupportInitialize)(this.scalationTrackBar)).EndInit(); - this.ResumeLayout(false); - this.PerformLayout(); - - } - - #endregion - - private PerspectivePanel perspectivePanel; - private System.Windows.Forms.TrackBar trackBar1; - private System.Windows.Forms.TrackBar trackBar2; - private System.Windows.Forms.TrackBar trackBar3; - private System.Windows.Forms.Timer rotationTimer; - private System.Windows.Forms.CheckBox autoRotateCheckBox; - private System.Windows.Forms.Label label1; - private System.Windows.Forms.Label label2; - private System.Windows.Forms.Label label3; - private System.Windows.Forms.Label label4; - private System.Windows.Forms.TrackBar scalationTrackBar; - } -} - diff --git a/SharpMath.3DTest/PerspectiveForm.cs b/SharpMath.3DTest/PerspectiveForm.cs deleted file mode 100644 index f2ca787..0000000 --- a/SharpMath.3DTest/PerspectiveForm.cs +++ /dev/null @@ -1,158 +0,0 @@ -// PerspectiveForm.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -using System; -using System.Drawing; -using System.Drawing.Drawing2D; -using System.Windows.Forms; -using SharpMath.Geometry; - -namespace SharpMath._3DTest -{ - public partial class PerspectiveForm : Form - { - private readonly Vector4[] _vertices = - { - new Vector4(-0.5, 0.5, 1.5, 1), new Vector4(0.5, 0.5, 1.5, 1), new Vector4(0.5, -0.5, 1.5, 1), - new Vector4(-0.5, -0.5, 1.5, 1), - new Vector4(-0.5, 0.5, 2.5, 1), new Vector4(0.5, 0.5, 2.5, 1), new Vector4(0.5, -0.5, 2.5, 1), - new Vector4(-0.5, -0.5, 2.5, 1) - }; - - private Color _color = Color.White; - private Matrix4x4 _rotationX = Matrix4x4.Identity; - private Matrix4x4 _rotationY = Matrix4x4.Identity; - private Matrix4x4 _rotationZ = Matrix4x4.Identity; - private Matrix4x4 _scalation = Matrix4x4.Scalation(0.5f); - private Matrix4x4 _view; - private Matrix4x4 _world; - private float angle; - - private float delta = 1f; - //private Matrix4x4 _projection; - - public PerspectiveForm() - { - InitializeComponent(); - } - - private void autoRotateCheckBox_CheckedChanged(object sender, EventArgs e) - { - rotationTimer.Enabled = !rotationTimer.Enabled; - trackBar1.Enabled = !trackBar1.Enabled; - trackBar2.Enabled = !trackBar2.Enabled; - trackBar3.Enabled = !trackBar3.Enabled; - if (!rotationTimer.Enabled) - _color = Color.White; - } - - private void PerspectiveForm_Load(object sender, EventArgs e) - { - // Bottom front left, Bottom front right, Bottom back right, Bottom back left, Top front left, Top front right, Top back right, Top back left - //_vertices = new[] { new Vector4(-1.5, 0, 0, 1), new Vector4(1.5, 0, 0, 1), new Vector4(1.5, 0, -1.5, 1), new Vector4(-1.5, 0, -1.5, 1), new Vector4(-1.5, 1.5, 0, 1), new Vector4(1.5, 1.5, 0, 1), new Vector4(1.5, 1.5, -1.5, 1), new Vector4(-1.5, 1.5, -1.5, 1) }; - UpdateMatrices(); - } - - private void perspectivePanel_Paint(object sender, PaintEventArgs e) - { - var transformationMatrix = Matrix4x4.Translation(0, 0, 2) * _view * _world * _rotationX * _rotationY * - _rotationZ * - _scalation * Matrix4x4.Translation(0, 0, -2); - var projectionData = new ProjectionData(perspectivePanel.Size, 16f / 9f, (float) Math.PI / 3f, 0.1f, 100f); - - Func projectPerspective = vector => - { - var perspectiveVector = vector * Matrix4x4.PerspectiveFieldOfView(projectionData); - perspectiveVector.X /= perspectiveVector.W; - perspectiveVector.Y /= perspectiveVector.W; - perspectiveVector.Z /= perspectiveVector.W; - - var deviceVector = perspectiveVector * - Matrix4x4.Scalation(projectionData.CanvasSize.Width / 2.0, - projectionData.CanvasSize.Height / 2.0, 1); - return new Vector2(deviceVector.X + projectionData.CanvasSize.Width / 2, - projectionData.CanvasSize.Height / 2 - deviceVector.Y); - }; - - var displayCoordinates = new PointF[_vertices.Length]; - for (var i = 0; i < _vertices.Length; i++) - { - var displayVector = projectPerspective(_vertices[i] * transformationMatrix); - displayCoordinates[i] = new PointF((float) displayVector.X, (float) displayVector.Y); - } - - var g = e.Graphics; - g.SmoothingMode = SmoothingMode.AntiAlias; - g.PixelOffsetMode = PixelOffsetMode.HighQuality; - using (var p = new Pen(_color, 3)) - { - for (int i = 0, j = 3; i < 4; j = i, i++) - g.DrawLine(p, displayCoordinates[i], displayCoordinates[j]); - for (int i = 4, j = 7; i < 8; j = i, i++) - g.DrawLine(p, displayCoordinates[i], displayCoordinates[j]); - for (int i = 0, j = 4; i < 4; i++, j++) - g.DrawLine(p, displayCoordinates[i], displayCoordinates[j]); - } - } - - private void rotationTimer_Tick(object sender, EventArgs e) - { - if (angle <= 0) - delta = 1f; - else if (angle >= 360) - delta = -1f; - angle += delta; - - _rotationX = Matrix4x4.RotationX(MathHelper.DegreesToRadians(angle)); - _rotationY = Matrix4x4.RotationY(MathHelper.DegreesToRadians(angle)); - _rotationZ = Matrix4x4.RotationZ(MathHelper.DegreesToRadians(angle)); - - trackBar1.Value = (int) angle; - trackBar2.Value = (int) angle; - trackBar3.Value = (int) angle; - - // Causes headaches - //Random randomGen = new Random(); - //KnownColor[] names = (KnownColor[])Enum.GetValues(typeof(KnownColor)); - //KnownColor randomColorName = names[randomGen.Next(names.Length)]; - //_color = Color.FromKnownColor(randomColorName); - - UpdateMatrices(); - } - - private void scalationTrackBar_Scroll(object sender, EventArgs e) - { - _scalation = Matrix4x4.Scalation(scalationTrackBar.Value / 720f); - UpdateMatrices(); - } - - private void trackBar1_Scroll(object sender, EventArgs e) - { - _rotationX = Matrix4x4.RotationX(MathHelper.DegreesToRadians(trackBar1.Value)); - UpdateMatrices(); - } - - private void trackBar2_Scroll(object sender, EventArgs e) - { - _rotationY = Matrix4x4.RotationY(MathHelper.DegreesToRadians(trackBar2.Value)); - UpdateMatrices(); - } - - private void trackBar3_Scroll(object sender, EventArgs e) - { - _rotationZ = Matrix4x4.RotationZ(MathHelper.DegreesToRadians(trackBar3.Value)); - UpdateMatrices(); - } - - private void UpdateMatrices() - { - var cameraPosition = Vector3.Zero; - var cameraTarget = Vector3.Back; - _view = Matrix4x4.View(cameraPosition, cameraTarget, Vector3.Up); - _world = Matrix4x4.World(cameraPosition, Vector3.Back, Vector3.Up); - //_projection = Matrix4x4.PerspectiveFieldOfView(projectionData); - - perspectivePanel.Invalidate(); - } - } -} \ No newline at end of file diff --git a/SharpMath.3DTest/PerspectiveForm.resx b/SharpMath.3DTest/PerspectiveForm.resx deleted file mode 100644 index b2e20b4..0000000 --- a/SharpMath.3DTest/PerspectiveForm.resx +++ /dev/null @@ -1,123 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - 17, 17 - - \ No newline at end of file diff --git a/SharpMath.3DTest/PerspectivePanel.cs b/SharpMath.3DTest/PerspectivePanel.cs deleted file mode 100644 index 8a9ad49..0000000 --- a/SharpMath.3DTest/PerspectivePanel.cs +++ /dev/null @@ -1,18 +0,0 @@ -// PerspectivePanel.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -using System.Windows.Forms; - -namespace SharpMath._3DTest -{ - public class PerspectivePanel : Panel - { - public PerspectivePanel() - { - SetStyle( - ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw | - ControlStyles.SupportsTransparentBackColor, true); - UpdateStyles(); - } - } -} \ No newline at end of file diff --git a/SharpMath.3DTest/Program.cs b/SharpMath.3DTest/Program.cs deleted file mode 100644 index a6b7af2..0000000 --- a/SharpMath.3DTest/Program.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Program.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -using System; -using System.Windows.Forms; - -namespace SharpMath._3DTest -{ - internal static class Program - { - /// - /// The main entry point for the application. - /// - [STAThread] - private static void Main() - { - Application.EnableVisualStyles(); - Application.SetCompatibleTextRenderingDefault(false); - Application.Run(new PerspectiveForm()); - } - } -} \ No newline at end of file diff --git a/SharpMath.3DTest/Properties/AssemblyInfo.cs b/SharpMath.3DTest/Properties/AssemblyInfo.cs deleted file mode 100644 index 2862195..0000000 --- a/SharpMath.3DTest/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,42 +0,0 @@ -// AssemblyInfo.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. - -[assembly: AssemblyTitle("SharpMath.3DTest")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("SharpMath.3DTest")] -[assembly: AssemblyCopyright("Copyright © 2016")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. - -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM - -[assembly: Guid("15428131-2ac9-4af4-8966-664ff1974ce8")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] - -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] \ No newline at end of file diff --git a/SharpMath.3DTest/Properties/Resources.Designer.cs b/SharpMath.3DTest/Properties/Resources.Designer.cs deleted file mode 100644 index abdb4ff..0000000 --- a/SharpMath.3DTest/Properties/Resources.Designer.cs +++ /dev/null @@ -1,63 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace SharpMath._3DTest.Properties { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resources { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resources() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("SharpMath._3DTest.Properties.Resources", typeof(Resources).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - } -} diff --git a/SharpMath.3DTest/Properties/Resources.resx b/SharpMath.3DTest/Properties/Resources.resx deleted file mode 100644 index af7dbeb..0000000 --- a/SharpMath.3DTest/Properties/Resources.resx +++ /dev/null @@ -1,117 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - \ No newline at end of file diff --git a/SharpMath.3DTest/Properties/Settings.Designer.cs b/SharpMath.3DTest/Properties/Settings.Designer.cs deleted file mode 100644 index 5b7c4cb..0000000 --- a/SharpMath.3DTest/Properties/Settings.Designer.cs +++ /dev/null @@ -1,26 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace SharpMath._3DTest.Properties { - - - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "14.0.0.0")] - internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { - - private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); - - public static Settings Default { - get { - return defaultInstance; - } - } - } -} diff --git a/SharpMath.3DTest/Properties/Settings.settings b/SharpMath.3DTest/Properties/Settings.settings deleted file mode 100644 index e04fc63..0000000 --- a/SharpMath.3DTest/Properties/Settings.settings +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/SharpMath.3DTest/SharpMath.3DTest.csproj b/SharpMath.3DTest/SharpMath.3DTest.csproj deleted file mode 100644 index ec2f198..0000000 --- a/SharpMath.3DTest/SharpMath.3DTest.csproj +++ /dev/null @@ -1,100 +0,0 @@ - - - - - Debug - AnyCPU - {15428131-2AC9-4AF4-8966-664FF1974CE8} - WinExe - Properties - SharpMath._3DTest - SharpMath.3DTest - v4.0 - 512 - true - - - - AnyCPU - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - AnyCPU - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - - - - - - - - - Form - - - PerspectiveForm.cs - - - Component - - - - - PerspectiveForm.cs - - - ResXFileCodeGenerator - Resources.Designer.cs - Designer - - - True - Resources.resx - True - - - SettingsSingleFileGenerator - Settings.Designer.cs - - - True - Settings.settings - True - - - - - - - - {4706ae0f-30c8-460b-bbbf-5f189be4eb76} - SharpMath - - - - - \ No newline at end of file diff --git a/SharpMath.CanvasTest/App.config b/SharpMath.CanvasTest/App.config deleted file mode 100644 index 0ac2de8..0000000 --- a/SharpMath.CanvasTest/App.config +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/SharpMath.CanvasTest/Canvas2DTest.Designer.cs b/SharpMath.CanvasTest/Canvas2DTest.Designer.cs deleted file mode 100644 index 890c3bb..0000000 --- a/SharpMath.CanvasTest/Canvas2DTest.Designer.cs +++ /dev/null @@ -1,64 +0,0 @@ -namespace SharpMath.Canvas2DTest { - partial class Canvas2DTest { - /// - /// Erforderliche Designervariable. - /// - private System.ComponentModel.IContainer components = null; - - /// - /// Verwendete Ressourcen bereinigen. - /// - /// True, wenn verwaltete Ressourcen gelöscht werden sollen; andernfalls False. - protected override void Dispose (bool disposing) { - if (disposing && (components != null)) { - components.Dispose (); - } - base.Dispose (disposing); - } - - #region Vom Windows Form-Designer generierter Code - - /// - /// Erforderliche Methode für die Designerunterstützung. - /// Der Inhalt der Methode darf nicht mit dem Code-Editor geändert werden. - /// - private void InitializeComponent () { - System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager (typeof (Canvas2DTest)); - this.testCanvas = new SharpMath.Presentation.Canvas2D (); - this.SuspendLayout (); - // - // testCanvas - // - this.testCanvas.Dock = System.Windows.Forms.DockStyle.Fill; - this.testCanvas.Font = new System.Drawing.Font ("Segoe UI", 8F); - this.testCanvas.ForeColor = System.Drawing.Color.Gray; - this.testCanvas.GridOrigin = ((System.Drawing.PointF)(resources.GetObject ("testCanvas.GridOrigin"))); - this.testCanvas.GridScale = new System.Drawing.SizeF (20F, 10F); - this.testCanvas.GridSize = new System.Drawing.SizeF (20F, 20F); - this.testCanvas.Location = new System.Drawing.Point (0, 0); - this.testCanvas.Name = "testCanvas"; - this.testCanvas.Size = new System.Drawing.Size (825, 469); - this.testCanvas.TabIndex = 0; - this.testCanvas.Text = "testCanvas"; - this.testCanvas.TrackingLines = false; - this.testCanvas.ValueIndicator = true; - // - // Canvas2DTest - // - this.AutoScaleDimensions = new System.Drawing.SizeF (6F, 13F); - this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size (825, 469); - this.Controls.Add (this.testCanvas); - this.Name = "Canvas2DTest"; - this.Text = "Canvas2DTest"; - this.Load += new System.EventHandler (this.Canvas2DTest_Load); - this.ResumeLayout (false); - - } - - #endregion - - private SharpMath.Presentation.Canvas2D testCanvas; - } -} - diff --git a/SharpMath.CanvasTest/Canvas2DTest.cs b/SharpMath.CanvasTest/Canvas2DTest.cs deleted file mode 100644 index 2854c0a..0000000 --- a/SharpMath.CanvasTest/Canvas2DTest.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Canvas2DTest.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -using System; -using System.Windows.Forms; -using SharpMath.Geometry; -using SharpMath.Presentation; - -namespace SharpMath.Canvas2DTest -{ - public partial class Canvas2DTest : Form - { - public Canvas2DTest() - { - InitializeComponent(); - } - - private void Canvas2DTest_Load(object sender, EventArgs e) - { - testCanvas.Functions.Add(new FunctionWrapper(x => Math.Pow(x, 3))); - testCanvas.Functions.Add(new FunctionWrapper(x => 1.5 * Math.Sin(x))); - testCanvas.Vectors.Add(new Vector2(4, 8)); - } - } -} \ No newline at end of file diff --git a/SharpMath.CanvasTest/Canvas2DTest.resx b/SharpMath.CanvasTest/Canvas2DTest.resx deleted file mode 100644 index 08e6fc5..0000000 --- a/SharpMath.CanvasTest/Canvas2DTest.resx +++ /dev/null @@ -1,127 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - - AAEAAAD/////AQAAAAAAAAAMAgAAAFFTeXN0ZW0uRHJhd2luZywgVmVyc2lvbj00LjAuMC4wLCBDdWx0 - dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWIwM2Y1ZjdmMTFkNTBhM2EFAQAAABVTeXN0ZW0uRHJh - d2luZy5Qb2ludEYCAAAAAXgBeQAACwsCAAAAAADOQwAAakML - - - \ No newline at end of file diff --git a/SharpMath.CanvasTest/Program.cs b/SharpMath.CanvasTest/Program.cs deleted file mode 100644 index bc88b7f..0000000 --- a/SharpMath.CanvasTest/Program.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Program.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -using System; -using System.Windows.Forms; - -namespace SharpMath.Canvas2DTest -{ - internal static class Program - { - /// - /// Der Haupteinstiegspunkt für die Anwendung. - /// - [STAThread] - private static void Main() - { - Application.EnableVisualStyles(); - Application.SetCompatibleTextRenderingDefault(false); - Application.Run(new Canvas2DTest()); - } - } -} \ No newline at end of file diff --git a/SharpMath.CanvasTest/Properties/AssemblyInfo.cs b/SharpMath.CanvasTest/Properties/AssemblyInfo.cs deleted file mode 100644 index 410cd1f..0000000 --- a/SharpMath.CanvasTest/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,38 +0,0 @@ -// AssemblyInfo.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -using System.Reflection; -using System.Runtime.InteropServices; - -// Allgemeine Informationen über eine Assembly werden über die folgenden -// Attribute gesteuert. Ändern Sie diese Attributwerte, um die Informationen zu ändern, -// die einer Assembly zugeordnet sind. -[assembly: AssemblyTitle("CanvasTest")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("CanvasTest")] -[assembly: AssemblyCopyright("Copyright © 2016")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Durch Festlegen von ComVisible auf "false" werden die Typen in dieser Assembly unsichtbar -// für COM-Komponenten. Wenn Sie auf einen Typ in dieser Assembly von -// COM aus zugreifen müssen, sollten Sie das ComVisible-Attribut für diesen Typ auf "True" festlegen. -[assembly: ComVisible(false)] - -// Die folgende GUID bestimmt die ID der Typbibliothek, wenn dieses Projekt für COM verfügbar gemacht wird -[assembly: Guid("e8982e21-deed-40ea-8df9-0bd161ad1490")] - -// Versionsinformationen für eine Assembly bestehen aus den folgenden vier Werten: -// -// Hauptversion -// Nebenversion -// Buildnummer -// Revision -// -// Sie können alle Werte angeben oder die standardmäßigen Build- und Revisionsnummern -// übernehmen, indem Sie "*" eingeben: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] \ No newline at end of file diff --git a/SharpMath.CanvasTest/Properties/Resources.Designer.cs b/SharpMath.CanvasTest/Properties/Resources.Designer.cs deleted file mode 100644 index 3c13750..0000000 --- a/SharpMath.CanvasTest/Properties/Resources.Designer.cs +++ /dev/null @@ -1,63 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace SharpMath.Canvas2DTest.Properties { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resources { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resources() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("SharpMath.Canvas2DTest.Properties.Resources", typeof(Resources).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - } -} diff --git a/SharpMath.CanvasTest/Properties/Resources.resx b/SharpMath.CanvasTest/Properties/Resources.resx deleted file mode 100644 index af7dbeb..0000000 --- a/SharpMath.CanvasTest/Properties/Resources.resx +++ /dev/null @@ -1,117 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - \ No newline at end of file diff --git a/SharpMath.CanvasTest/Properties/Settings.Designer.cs b/SharpMath.CanvasTest/Properties/Settings.Designer.cs deleted file mode 100644 index 82581d2..0000000 --- a/SharpMath.CanvasTest/Properties/Settings.Designer.cs +++ /dev/null @@ -1,26 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace SharpMath.Canvas2DTest.Properties { - - - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "14.0.0.0")] - internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { - - private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); - - public static Settings Default { - get { - return defaultInstance; - } - } - } -} diff --git a/SharpMath.CanvasTest/Properties/Settings.settings b/SharpMath.CanvasTest/Properties/Settings.settings deleted file mode 100644 index e04fc63..0000000 --- a/SharpMath.CanvasTest/Properties/Settings.settings +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/SharpMath.CanvasTest/SharpMath.Canvas2DTest.csproj b/SharpMath.CanvasTest/SharpMath.Canvas2DTest.csproj deleted file mode 100644 index bd8f91c..0000000 --- a/SharpMath.CanvasTest/SharpMath.Canvas2DTest.csproj +++ /dev/null @@ -1,125 +0,0 @@ - - - - - Debug - AnyCPU - {E8982E21-DEED-40EA-8DF9-0BD161AD1490} - WinExe - Properties - SharpMath.Canvas2DTest - SharpMath.CanvasTest - v4.0 - 512 - true - publish\ - true - Disk - false - Foreground - 7 - Days - false - false - true - 0 - 1.0.0.%2a - false - false - true - - - - AnyCPU - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - AnyCPU - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - - - - - - - - - - Form - - - Canvas2DTest.cs - - - - - Canvas2DTest.cs - - - ResXFileCodeGenerator - Resources.Designer.cs - Designer - - - True - Resources.resx - True - - - SettingsSingleFileGenerator - Settings.Designer.cs - - - True - Settings.settings - True - - - - - - - - {4706AE0F-30C8-460B-BBBF-5F189BE4EB76} - SharpMath - - - - - False - Microsoft .NET Framework 4.5.2 %28x86 and x64%29 - true - - - False - .NET Framework 3.5 SP1 - false - - - - - \ No newline at end of file diff --git a/SharpMath.PolygonTest/App.config b/SharpMath.PolygonTest/App.config deleted file mode 100644 index 0ac2de8..0000000 --- a/SharpMath.PolygonTest/App.config +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/SharpMath.PolygonTest/ContainsTestForm.Designer.cs b/SharpMath.PolygonTest/ContainsTestForm.Designer.cs deleted file mode 100644 index c5042b1..0000000 --- a/SharpMath.PolygonTest/ContainsTestForm.Designer.cs +++ /dev/null @@ -1,49 +0,0 @@ -namespace SharpMath.PolygonTest -{ - partial class ContainsTestForm - { - /// - /// Required designer variable. - /// - private System.ComponentModel.IContainer components = null; - - /// - /// Clean up any resources being used. - /// - /// true if managed resources should be disposed; otherwise, false. - protected override void Dispose(bool disposing) - { - if (disposing && (components != null)) - { - components.Dispose(); - } - base.Dispose(disposing); - } - - #region Windows Form Designer generated code - - /// - /// Required method for Designer support - do not modify - /// the contents of this method with the code editor. - /// - private void InitializeComponent() - { - this.SuspendLayout(); - // - // ContainsTestForm - // - this.AutoScaleDimensions = new System.Drawing.SizeF(9F, 20F); - this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(543, 405); - this.Name = "ContainsTestForm"; - this.Text = "ContainsTestForm"; - this.Paint += new System.Windows.Forms.PaintEventHandler(this.ContainsTestForm_Paint); - this.MouseMove += new System.Windows.Forms.MouseEventHandler(this.ContainsTestForm_MouseMove); - this.ResumeLayout(false); - - } - - #endregion - } -} - diff --git a/SharpMath.PolygonTest/ContainsTestForm.cs b/SharpMath.PolygonTest/ContainsTestForm.cs deleted file mode 100644 index 94c2d2f..0000000 --- a/SharpMath.PolygonTest/ContainsTestForm.cs +++ /dev/null @@ -1,51 +0,0 @@ -// ContainsTestForm.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -using System.Collections.Generic; -using System.Drawing; -using System.Windows.Forms; -using SharpMath.Geometry; - -namespace SharpMath.PolygonTest -{ - public partial class ContainsTestForm : Form - { - private readonly PointF[] _points = - { - new PointF(0, 0), new PointF(200, 80), new PointF(400, 200), - new PointF(100, 60) - }; - - private readonly Polygon _polygon; - private bool _containsPoint; - - public ContainsTestForm() - { - InitializeComponent(); - var polygonPoints = new List(); - foreach (var point in _points) - polygonPoints.Add(FromPointF(point)); - _polygon = new Polygon(polygonPoints.ToArray()); - } - - private void ContainsTestForm_MouseMove(object sender, MouseEventArgs e) - { - var containsPointOld = _containsPoint; - _containsPoint = _polygon.ContainsPoint(new Point2D(e.X, e.Y)); - if (_containsPoint != containsPointOld) - Invalidate(); - Text = e.Location.ToString(); - } - - private void ContainsTestForm_Paint(object sender, PaintEventArgs e) - { - e.Graphics.FillRectangle(Brushes.Black, ClientRectangle); - e.Graphics.FillPolygon(_containsPoint ? Brushes.AliceBlue : Brushes.Red, _points); - } - - private Point2D FromPointF(PointF point) - { - return new Point2D(point.X, point.Y); - } - } -} \ No newline at end of file diff --git a/SharpMath.PolygonTest/ContainsTestForm.resx b/SharpMath.PolygonTest/ContainsTestForm.resx deleted file mode 100644 index 1af7de1..0000000 --- a/SharpMath.PolygonTest/ContainsTestForm.resx +++ /dev/null @@ -1,120 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - \ No newline at end of file diff --git a/SharpMath.PolygonTest/Program.cs b/SharpMath.PolygonTest/Program.cs deleted file mode 100644 index 681f752..0000000 --- a/SharpMath.PolygonTest/Program.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Program.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -using System; -using System.Windows.Forms; - -namespace SharpMath.PolygonTest -{ - internal static class Program - { - /// - /// The main entry point for the application. - /// - [STAThread] - private static void Main() - { - Application.EnableVisualStyles(); - Application.SetCompatibleTextRenderingDefault(false); - Application.Run(new ContainsTestForm()); - } - } -} \ No newline at end of file diff --git a/SharpMath.PolygonTest/Properties/AssemblyInfo.cs b/SharpMath.PolygonTest/Properties/AssemblyInfo.cs deleted file mode 100644 index 3e1928a..0000000 --- a/SharpMath.PolygonTest/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,42 +0,0 @@ -// AssemblyInfo.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. - -[assembly: AssemblyTitle("SharpMath.PolygonTest")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("SharpMath.PolygonTest")] -[assembly: AssemblyCopyright("Copyright © 2016")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. - -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM - -[assembly: Guid("1d3dd70b-e845-4993-a73e-7ebd14b9e005")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] - -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] \ No newline at end of file diff --git a/SharpMath.PolygonTest/Properties/Resources.Designer.cs b/SharpMath.PolygonTest/Properties/Resources.Designer.cs deleted file mode 100644 index 125e299..0000000 --- a/SharpMath.PolygonTest/Properties/Resources.Designer.cs +++ /dev/null @@ -1,63 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace SharpMath.PolygonTest.Properties { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resources { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resources() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("SharpMath.PolygonTest.Properties.Resources", typeof(Resources).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - } -} diff --git a/SharpMath.PolygonTest/Properties/Resources.resx b/SharpMath.PolygonTest/Properties/Resources.resx deleted file mode 100644 index af7dbeb..0000000 --- a/SharpMath.PolygonTest/Properties/Resources.resx +++ /dev/null @@ -1,117 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - \ No newline at end of file diff --git a/SharpMath.PolygonTest/Properties/Settings.Designer.cs b/SharpMath.PolygonTest/Properties/Settings.Designer.cs deleted file mode 100644 index c5980a2..0000000 --- a/SharpMath.PolygonTest/Properties/Settings.Designer.cs +++ /dev/null @@ -1,26 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace SharpMath.PolygonTest.Properties { - - - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "14.0.0.0")] - internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { - - private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); - - public static Settings Default { - get { - return defaultInstance; - } - } - } -} diff --git a/SharpMath.PolygonTest/Properties/Settings.settings b/SharpMath.PolygonTest/Properties/Settings.settings deleted file mode 100644 index e04fc63..0000000 --- a/SharpMath.PolygonTest/Properties/Settings.settings +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/SharpMath.PolygonTest/SharpMath.PolygonTest.csproj b/SharpMath.PolygonTest/SharpMath.PolygonTest.csproj deleted file mode 100644 index 4291ccb..0000000 --- a/SharpMath.PolygonTest/SharpMath.PolygonTest.csproj +++ /dev/null @@ -1,98 +0,0 @@ - - - - - Debug - AnyCPU - {1D3DD70B-E845-4993-A73E-7EBD14B9E005} - WinExe - Properties - SharpMath.PolygonTest - SharpMath.PolygonTest - v4.0 - 512 - true - - - - AnyCPU - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - AnyCPU - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - - - - - - - - - - Form - - - ContainsTestForm.cs - - - - - ContainsTestForm.cs - - - ResXFileCodeGenerator - Resources.Designer.cs - Designer - - - True - Resources.resx - True - - - SettingsSingleFileGenerator - Settings.Designer.cs - - - True - Settings.settings - True - - - - - - - - {4706ae0f-30c8-460b-bbbf-5f189be4eb76} - SharpMath - - - - - \ No newline at end of file diff --git a/SharpMath.Tests/Line2DTest.cs b/SharpMath.Tests/Line2DTest.cs deleted file mode 100644 index 35ed602..0000000 --- a/SharpMath.Tests/Line2DTest.cs +++ /dev/null @@ -1,85 +0,0 @@ -// Line2DTest.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -using Microsoft.VisualStudio.TestTools.UnitTesting; -using SharpMath.Geometry; - -namespace SharpMath.Tests -{ - [TestClass] - public class Line2DTest - { - [TestMethod] - public void CanCreateLineFromPoints() - { - var line = Line2D.FromPoints(new Point2D(0, 2), new Point2D(3, 1)); - Assert.AreEqual(-1d / 3d, line.Slope); - Assert.AreEqual(2, line.Offset); - - var secondLine = Line2D.FromPoints(new Point2D(4, 5), new Point2D(1, 3)); - Assert.AreEqual(2d / 3d, secondLine.Slope); - Assert.AreEqual(7d / 3d, secondLine.Offset); - } - - [TestMethod] - public void CanDetermineIfLinesAreParallel() - { - var line = new Line2D(2, 0); - var secondLine = new Line2D(2, 3); - var thirdLine = new Line2D(3, 1); - Assert.IsTrue(line.IsParallelTo(secondLine)); - Assert.IsFalse(secondLine.IsParallelTo(thirdLine)); - } - - [TestMethod] - public void CanDetermineIfLinesIntersect() - { - var line = new Line2D(2, 0); - var secondLine = new Line2D(3, 1); - var thirdLine = new Line2D(2, 3); - Assert.IsTrue(line.IntersectsWith(secondLine)); - Assert.IsFalse(line.IntersectsWith(thirdLine)); - } - - [TestMethod] - public void CanDetermineIfPointIsOnLine() - { - var line = new Line2D(1, 0); - Assert.IsTrue(line.IsPointOnLine(new Point2D(1, 1))); - Assert.IsFalse(line.IsPointOnLine(new Point2D(2, 1))); - - var secondLine = new Line2D(2, 3); - Assert.IsTrue(secondLine.IsPointOnLine(new Point2D(4, 11))); - Assert.IsFalse(secondLine.IsPointOnLine(new Point2D(3, 2))); - } - - [TestMethod] - public void CanFindIntersectionPoint() - { - // 0.5x - 1 - var firstLine = new Line2D(0.5, -1); - // -2x - 6 - var secondLine = new Line2D(-2, -6); - - var intersectionPoint = firstLine.GetIntersectionPoint(secondLine); - Assert.AreEqual(-2, intersectionPoint.X); - Assert.AreEqual(-2, intersectionPoint.Y); - } - - [TestMethod] - public void CanGetIntersectionPoint() - { - var line = new Line2D(2, 0); - var secondLine = new Line2D(3, 1); - Assert.AreEqual(new Point2D(-1, -2), line.GetIntersectionPoint(secondLine)); - } - - [TestMethod] - public void CanGetPoint() - { - var line = new Line2D(3, 1); - Assert.AreEqual(new Point2D(1, 4), line.GetPoint(1)); - Assert.AreEqual(new Point2D(10, 31), line.GetPoint(10)); - } - } -} \ No newline at end of file diff --git a/SharpMath.Tests/Line3DTest.cs b/SharpMath.Tests/Line3DTest.cs deleted file mode 100644 index b705b76..0000000 --- a/SharpMath.Tests/Line3DTest.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Line3DTest.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -using Microsoft.VisualStudio.TestTools.UnitTesting; -using SharpMath.Geometry; - -namespace SharpMath.Tests -{ - [TestClass] - public class Line3DTest - { - [TestMethod] - public void CanCreateLineFromPoints() - { - var line = Line3D.FromPoints(new Point3D(Vector3.Zero), new Point3D(Vector3.One)); - Assert.AreEqual(new Point3D(Vector3.Zero), line.Point); - Assert.AreEqual(Vector3.One, line.Direction); - - var secondLine = Line3D.FromPoints(new Point3D(2, 3, 1), new Point3D(3, 2, 4)); - Assert.AreEqual(new Point3D(2, 3, 1), secondLine.Point); - Assert.AreEqual(new Vector3(1, -1, 3), secondLine.Direction); - } - - [TestMethod] - public void CanDetermineIfPointIsOnLine() - { - var line = new Line3D(new Point3D(2, 3, 1), new Vector3(2, 1, 2)); - Assert.IsTrue(line.IsPointOnLine(new Point3D(6, 5, 5))); - Assert.IsFalse(line.IsPointOnLine(new Point3D(2, 3.5, 2))); - } - - [TestMethod] - public void CanGetPoint() - { - var line = new Line3D(new Point3D(2, 3, 1), new Vector3(2, 1, 2)); - Assert.AreEqual(new Point3D(6, 5, 5), line.GetPoint(2)); - } - } -} \ No newline at end of file diff --git a/SharpMath.Tests/LinearEquationTest.cs b/SharpMath.Tests/LinearEquationTest.cs deleted file mode 100644 index 43042a6..0000000 --- a/SharpMath.Tests/LinearEquationTest.cs +++ /dev/null @@ -1,53 +0,0 @@ -// LinearEquationTest.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -using Microsoft.VisualStudio.TestTools.UnitTesting; -using SharpMath.Equations; -using SharpMath.Equations.Exceptions; - -namespace SharpMath.Tests -{ - [TestClass] - public class LinearEquationTest - { - [TestMethod] - [ExpectedException(typeof(EquationNotSolvableException))] - public void CanNotSolveLinearEquationSystemClearly() - { - // 0x = 0 - infinite solutions - var equation = new LinearEquation(new[] {0.0}, 0); - var results = new LinearEquationSystem(equation).Solve(); // This will throw an exception - Assert.AreEqual(0, results[0]); - } - - [TestMethod] - public void CanSolveLinearEquationSystem() - { - // x - y + 2z = 6 - var firstEquation = new LinearEquation(new[] {1.0, -1.0, 2.0}, 6); - // 2x + 3y + 2z = 11 - var secondEquation = new LinearEquation(new[] {2.0, 3.0, 2.0}, 11); - // 3x + 2y + z = 8 - var thirdEquation = new LinearEquation(new[] {3.0, 2.0, 1.0}, 8); - - var linearEquationSystem = new LinearEquationSystem(firstEquation, secondEquation, thirdEquation); - var results = linearEquationSystem.Solve(); - Assert.AreEqual(1, results[0]); - Assert.AreEqual(1, results[1]); - Assert.AreEqual(3, results[2]); - } - - [TestMethod] - public void CanSolveSimpleLinearEquationSystem() - { - // 2x = 1 - var equation = new LinearEquation - { - Coefficients = new[] {2.0}, - Result = 1 - }; - var linearEquationSystem = new LinearEquationSystem(equation); - Assert.AreEqual(0.5, linearEquationSystem.Solve()[0]); - } - } -} \ No newline at end of file diff --git a/SharpMath.Tests/MatrixTest.cs b/SharpMath.Tests/MatrixTest.cs deleted file mode 100644 index 8a2a2c6..0000000 --- a/SharpMath.Tests/MatrixTest.cs +++ /dev/null @@ -1,269 +0,0 @@ -// MatrixTest.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -using System.Diagnostics; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using SharpMath.Geometry; - -namespace SharpMath.Tests -{ - [TestClass] - public class MatrixTest - { - [TestMethod] - public void CanCalculateDeterminant() - { - var firstMatrix = new Matrix1x1 {[0, 0] = 2}; - Assert.AreEqual(2, firstMatrix.Determinant); - - var secondMatrix = new Matrix2x2 - { - [0, 0] = 8, - [0, 1] = 3, - [1, 0] = 4, - [1, 1] = 2 - }; - Assert.AreEqual(4, secondMatrix.Determinant); - - var thirdMatrix = new Matrix3x3 - { - [0, 0] = 2, - [0, 1] = 5, - [0, 2] = 2, - [1, 0] = 3, - [1, 1] = -3, - [1, 2] = 1, - [2, 0] = 1, - [2, 1] = 4, - [2, 2] = -4 - }; - - Assert.AreEqual(111, thirdMatrix.Determinant); - - var fourthMatrix = new Matrix3x3 - { - [0, 0] = -4, - [0, 1] = 2.5, - [0, 2] = 3, - [1, 0] = 5, - [1, 1] = 6, - [1, 2] = 4, - [2, 0] = 9, - [2, 1] = 10, - [2, 2] = -9 - }; - - Assert.AreEqual(566.5, fourthMatrix.Determinant); - - var fifthMatrix = new Matrix4x4 - { - [0, 0] = 1, - [0, 1] = 0, - [0, 2] = 0, - [0, 3] = 1, - [1, 0] = 1, - [1, 1] = 2, - [1, 2] = 3, - [1, 3] = 2, - [2, 0] = 2, - [2, 1] = 3, - [2, 2] = 4, - [2, 3] = 0, - [3, 0] = 1, - [3, 1] = 2, - [3, 2] = -1, - [3, 3] = -2 - }; - - Assert.AreEqual(-24, fifthMatrix.Determinant); - } - - [TestMethod] - public void CanDetermineIfMatrixIsDiagonal() - { - var matrix = new Matrix3x3 - { - M11 = 1, - M22 = 1, - M33 = 1 - }; - Assert.IsTrue(matrix.IsDiagonal); - - var secondMatrix = new Matrix4x4 - { - M11 = 1, - M22 = 1, - M33 = 1, - M44 = 1 - }; - Assert.IsTrue(secondMatrix.IsDiagonal); - - var thirdMatrix = new Matrix4x4 - { - M11 = 1, - M22 = 1, - M33 = 1, - M44 = 1, - M14 = 1 - }; - Assert.IsFalse(thirdMatrix.IsDiagonal); - - var fourthMatrix = new Matrix4x4 - { - M11 = 1, - M22 = 1, - M33 = 1 - }; - Assert.IsFalse(fourthMatrix.IsDiagonal); - } - - [TestMethod] - public void CanDetermineIfMatrixIsTriangle() - { - var matrix = new Matrix3x3 - { - M11 = 1, - M22 = 2, - M33 = 3, - M12 = 3, - M13 = 4, - M23 = 7 - }; - Assert.IsTrue(matrix.IsTriangle); - - var secondMatrix = new Matrix4x4 - { - M11 = 1, - M22 = 2, - M33 = 3, - M44 = 8, - M12 = 3, - M13 = 4, - M14 = 7, - M23 = 2, - M24 = 3, - M34 = 9 - }; - Assert.IsTrue(secondMatrix.IsTriangle); - - var thirdMatrix = new Matrix4x4 - { - M11 = 5, - M22 = 7, - M33 = 4, - M44 = 3, - M14 = 2, - M31 = 9 - }; - Assert.IsFalse(thirdMatrix.IsTriangle); - - var fourthMatrix = new Matrix4x4 - { - M11 = 1, - M22 = 1, - M33 = 1, - M44 = 1 - }; - Assert.IsTrue(fourthMatrix.IsTriangle); - } - - [TestMethod] - public void CanGetStringRepresentation() - { - var matrix = Matrix4x4.View(Vector3.Zero, Vector3.Forward, Vector3.Up); - Debug.Print(matrix.ToString()); - } - - [TestMethod] - public void CanMultiplyMatrices() - { - var firstMatrix = new Matrix1x3(); - var secondMatrix = new Matrix3x1(); - - firstMatrix[0, 0] = 1; - firstMatrix[0, 1] = 2; - firstMatrix[0, 2] = 3; - - secondMatrix[0, 0] = 2; - secondMatrix[1, 0] = 1; - secondMatrix[2, 0] = 2; - - var matrixProduct = MatrixUtils.Multiply(firstMatrix, secondMatrix); - Assert.AreEqual(matrixProduct[0, 0], 10); - - // -------------------------------- - - var thirdMatrix = new Matrix3x3 - { - [0, 0] = 2, - [0, 1] = 5, - [0, 2] = 2, - [1, 0] = 3, - [1, 1] = -3, - [1, 2] = 1, - [2, 0] = 1, - [2, 1] = 4, - [2, 2] = -4 - }; - - var fourthMatrix = new Matrix3x3 - { - [0, 0] = -4, - [0, 1] = 2.5, - [0, 2] = 3, - [1, 0] = 5, - [1, 1] = 6, - [1, 2] = 4, - [2, 0] = 9, - [2, 1] = 10, - [2, 2] = -9 - }; - - /* - 35.000 55.000 8.000 - -18.000 -0.500 -12.000 - -20.000 -13.500 55.000 - */ - var resultMatrix = new Matrix3x3 - { - [0, 0] = 35, - [0, 1] = 55, - [0, 2] = 8, - [1, 0] = -18, - [1, 1] = -0.5, - [1, 2] = -12, - [2, 0] = -20, - [2, 1] = -13.5, - [2, 2] = 55 - }; - - Assert.AreEqual(resultMatrix, MatrixUtils.Multiply(thirdMatrix, fourthMatrix)); - } - - [TestMethod] - public void CanMultiplyMatrix3X3WithVector2() - { - var translation = Matrix3x3.Translation(10, 10); - var transformed = translation * Vector2.One; - var result = new Vector2(11, 11); - Assert.AreEqual(result, transformed); - } - - // TODO: Implement - //[TestMethod] - //public void CanGetCore() - //{ - // var matrix = new Matrix3x3 - // { - // M11 = 3, - // M12 = -1, - // M13 = 4, - // M21 = 0, - // M22 = 1, - // M23 = 5, - // }; - - // Assert.AreEqual(new Vector3(-3, -5, 1), matrix.GetCore()); - //} - } -} \ No newline at end of file diff --git a/SharpMath.Tests/ParserTest.cs b/SharpMath.Tests/ParserTest.cs deleted file mode 100644 index 7c05efa..0000000 --- a/SharpMath.Tests/ParserTest.cs +++ /dev/null @@ -1,22 +0,0 @@ -// ParserTest.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -using Microsoft.VisualStudio.TestTools.UnitTesting; -using SharpMath.Expressions; - -namespace SharpMath.Tests -{ - [TestClass] - public class ParserTest - { - [TestMethod] - public void CanParseTerm() - { - var term = "(-2+3)*(cos(0)*(2/3))"; - Assert.AreEqual(2d / 3d, new Parser(term).Evaluate()); - - var secondTerm = "3*4^2"; - Assert.AreEqual(48, new Parser(secondTerm).Evaluate()); - } - } -} \ No newline at end of file diff --git a/SharpMath.Tests/PolygonTest.cs b/SharpMath.Tests/PolygonTest.cs deleted file mode 100644 index 85a417b..0000000 --- a/SharpMath.Tests/PolygonTest.cs +++ /dev/null @@ -1,48 +0,0 @@ -// PolygonTest.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -using System; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using SharpMath.Geometry; - -namespace SharpMath.Tests -{ - [TestClass] - public class PolygonTest - { - [TestMethod] - public void CanCalculateArea() - { - var square = - new Polygon(new Point2D(0, 0), new Point2D(2, 0), new Point2D(2, 2), new Point2D(0, 2)); - Assert.IsTrue(FloatingNumber.AreApproximatelyEqual(4, square.Area)); - - var triangle = - new Polygon(new Point2D(0, 0), new Point2D(2, 0), new Point2D(1, 2)); - Assert.AreEqual(2, triangle.Area); - - var polygon = new Polygon(new Point2D(-2, -2), new Point2D(2, -2), new Point2D(4, 1), new Point2D(0, 2), - new Point2D(-4, 1)); - Assert.AreEqual(22, polygon.Area); - } - - [TestMethod] - public void CanCalculatePerimeter() - { - var square = - new Polygon(new Point2D(0, 0), new Point2D(2, 0), new Point2D(2, 2), new Point2D(0, 2)); - Assert.AreEqual(8, square.Perimeter); - - var polygon = new Polygon(new Point2D(-2, -2), new Point2D(2, -2), new Point2D(4, 1), new Point2D(0, 2), - new Point2D(-4, 1)); - Assert.AreEqual(4 + 2 * Math.Sqrt(13) + 2 * Math.Sqrt(17), polygon.Perimeter); - } - - [TestMethod] - [ExpectedException(typeof(ArgumentException))] - public void CanValidatePointAmount() - { - new Polygon(new Point2D(0, 1), new Point2D(3, 4)); - } - } -} \ No newline at end of file diff --git a/SharpMath.Tests/Properties/AssemblyInfo.cs b/SharpMath.Tests/Properties/AssemblyInfo.cs deleted file mode 100644 index 97746f6..0000000 --- a/SharpMath.Tests/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,42 +0,0 @@ -// AssemblyInfo.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. - -[assembly: AssemblyTitle("SharpMath.Tests")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("SharpMath.Tests")] -[assembly: AssemblyCopyright("Copyright © 2015")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. - -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM - -[assembly: Guid("42b815a5-ddaa-4ded-bd69-fa212c1d0d46")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] - -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] \ No newline at end of file diff --git a/SharpMath.Tests/SharpMath.Tests.csproj b/SharpMath.Tests/SharpMath.Tests.csproj deleted file mode 100644 index 17acca0..0000000 --- a/SharpMath.Tests/SharpMath.Tests.csproj +++ /dev/null @@ -1,98 +0,0 @@ - - - - Debug - AnyCPU - {42B815A5-DDAA-4DED-BD69-FA212C1D0D46} - Library - Properties - SharpMath.Tests - SharpMath.Tests - v4.0 - 512 - {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - 10.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages - False - UnitTest - - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - - - - - - - False - - - - - - - - - - - - - - - - - {4706ae0f-30c8-460b-bbbf-5f189be4eb76} - SharpMath - - - - - - - False - - - False - - - False - - - False - - - - - - - - \ No newline at end of file diff --git a/SharpMath.Tests/VectorTest.cs b/SharpMath.Tests/VectorTest.cs deleted file mode 100644 index abfa977..0000000 --- a/SharpMath.Tests/VectorTest.cs +++ /dev/null @@ -1,308 +0,0 @@ -// VectorTest.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -using System; -using System.Diagnostics; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using SharpMath.Geometry; - -// ReSharper disable UnusedVariable - -namespace SharpMath.Tests -{ - [TestClass] - public class VectorTest - { - [TestMethod] - public void CanCalculateAngle() - { - var vector1 = Vector2.UnitX; - var vector2 = Vector2.UnitY; - - Assert.AreEqual(Math.PI / 2, vector1.Angle(vector2)); - Assert.IsTrue(vector1.CheckForOrthogonality(vector2)); - } - - [TestMethod] - public void CanCalculateArea() - { - var firstVector = new Vector3(3, 4, 4); - var secondVector = new Vector3(1, -2, 3); - var area = Vector3.Area(firstVector, secondVector); - Assert.AreEqual(Math.Sqrt(525), area); - - var thirdVector = new Vector2(2, 4); - var fourthVector = new Vector2(3, 1); - var secondArea = Vector2.Area(thirdVector, fourthVector); - Assert.IsTrue(FloatingNumber.AreApproximatelyEqual(10, secondArea)); - } - - [TestMethod] - public void CanCalculateCrossProduct() - { - var vector = new Vector3(1, -5, 2); - var secondVector = new Vector3(2, 0, 3); - var resultVector = vector.VectorProduct(secondVector); - - Assert.AreEqual(-15, resultVector.X); - Assert.AreEqual(1, resultVector.Y); - Assert.AreEqual(10, resultVector.Z); - } - - [TestMethod] - public void CanCalculateDistance() - { - var vector = new Vector3(10, 5, 3); - var secondVector = new Vector3(5, 6, 1); // 5, -1, 2 // Magnitude: sqrt(30) - Assert.AreEqual(Math.Sqrt(30), vector.Distance(secondVector)); - } - - [TestMethod] - public void CanCalculateLength() - { - var vector = new Vector3(13, 0, 0); - Assert.AreEqual(13, vector.Magnitude); - - var secondVector = new Vector3(1, 2, 2); - Assert.AreEqual(3, secondVector.Magnitude); - } - - [TestMethod] - public void CanCalculateScalarProduct() - { - Assert.AreEqual(0, Vector2.UnitX.DotProduct(Vector2.UnitY)); - Assert.AreEqual(20, new Vector2(2, 4).DotProduct(new Vector2(4, 3))); - Assert.AreEqual(0, Vector3.Forward.DotProduct(Vector3.Up)); - Assert.AreEqual(8, new Vector3(2, 3, 1).DotProduct(new Vector3(-1, 2, 4))); - } - - [TestMethod] - public void CanCompareVectors() - { - // Let's see, if the dimension check is working - var vector = new Vector3(); - var secondVector = new Vector2(); - Assert.IsFalse(vector.Equals(secondVector)); - - // Let's see, if the coordinate comparison is working (in this case simply two zero vectors) - var thirdVector = new Vector4(); - var fourthVector = new Vector4(); - Assert.IsTrue(thirdVector.Equals(fourthVector)); - Assert.IsTrue(thirdVector == fourthVector); - Assert.IsFalse(thirdVector != fourthVector); - - // Let's see, if the coordinate comparison is working when we have the same dimension but different coordinate values - var fifthVector = new Vector3 - { - [0] = 1, - [1] = 2, - [2] = 1 - }; - - Assert.AreEqual(new Vector3(1, 2, 1), fifthVector); - - var sixthVector = new Vector3 - { - [0] = 2, - [1] = 2, - [2] = 1 - }; - - Assert.IsFalse(fifthVector.Equals(sixthVector)); - Assert.IsFalse(fifthVector == sixthVector); - Assert.IsTrue(fifthVector != sixthVector); - } - - [TestMethod] - public void CanConvertVectorIntoMatrices() - { - var firstMatrix = new Matrix3x1 - { - [0, 0] = 1, - [1, 0] = 0, - [2, 0] = 0 - }; - var firstVectorMatrix = Vector3.Right.AsVerticalMatrix(); - Assert.AreEqual(firstMatrix, firstVectorMatrix); - - var secondMatrix = new Matrix1x3 - { - [0, 0] = 1, - [0, 1] = 0, - [0, 2] = 0 - }; - var secondVectorMatrix = Vector3.Right.AsHorizontalMatrix(); - Assert.AreEqual(secondMatrix, secondVectorMatrix); - } - - [TestMethod] - public void CanConvertVectors() - { - var firstVector = new Vector3(2, 6, 8); - var newVector = firstVector.Convert(); - Assert.AreEqual(2, (int) newVector.Dimension); - Assert.AreEqual(2, newVector.X); - Assert.AreEqual(6, newVector.Y); - } - - [TestMethod] - public void CanCreateScalarTripleProduct() - { - var firstVector = new Vector3(2, 0, 5); - var secondVector = new Vector3(-1, 5, -2); - var thirdVector = new Vector3(2, 1, 2); - - var scalarTripleProduct = Vector3.ScalarTripleProduct(firstVector, secondVector, thirdVector); - Debug.Print(scalarTripleProduct.ToString()); - Assert.AreEqual(31, scalarTripleProduct); - } - - [TestMethod] - public void CanDetermineIfVectorsAreOrthogonal() - { - Assert.IsTrue(Vector3.Forward.CheckForOrthogonality(Vector3.Up)); - Assert.IsFalse(Vector3.Forward.CheckForOrthogonality(Vector3.Back)); - Assert.IsFalse(Vector3.Zero.CheckForOrthogonality(Vector3.UnitX)); - } - - [TestMethod] - public void CanDetermineIfVectorsAreOrthonormal() - { - Assert.IsTrue(Vector3.Forward.CheckForOrthonormality(Vector3.Up)); - Assert.IsTrue(Vector3.Back.CheckForOrthonormality(Vector3.Down)); - Assert.IsFalse(Vector3.Forward.CheckForOrthonormality(Vector3.Back)); - Assert.IsFalse(Vector3.Forward.CheckForOrthonormality(new Vector3(2, 3, 2))); - } - - [TestMethod] - public void CanDetermineIfVectorsAreParallel() - { - Assert.IsTrue(new Vector3(2, 3, 3).CheckForParallelism(new Vector3(4, 6, 6))); - Assert.IsTrue(new Vector3(1, 2, 3).CheckForParallelism(new Vector3(3, 6, 9))); - Assert.IsFalse(new Vector3(0, 1, 3).CheckForParallelism(new Vector3(0, 3, 2))); - } - - [TestMethod] - public void CanGetCorrectLaTeXString() - { - var vector = new Vector3(1, 6, 8); - Assert.AreEqual(@"\left( \begin{array}{c} 1 \\ 6 \\ 8 \end{array} \right)", vector.ToLaTeXString()); - } - - [TestMethod] - public void CanGetNegatedVector() - { - var vector = new Vector3(2, 3, 2); - Assert.AreEqual(new Vector3(-2, -3, -2), vector.Negate()); - Assert.AreEqual(new Vector3(-2, -3, -2), vector.Negate()); - } - - [TestMethod] - public void CanGetNormalizedVector() - { - var vector = new Vector2(3, 4); - Assert.AreEqual(new Vector2(3d / 5d, 4d / 5d), vector.Normalize()); - Assert.AreEqual(new Vector2(3d / 5d, 4d / 5d), vector.Normalize()); - } - - [TestMethod] - public void CanLerp() - { - var firstVector = new Vector3(2, 6, 8); - var secondVector = new Vector3(6, 10, 10); - var lerpResult = Vector3.Lerp(firstVector, secondVector, 0.5); - - // ((6,10,10)-(2,6,8))*0.5+(2,6,8) = (4,4,2)*0.5+(2,6,8) = (2,2,1)+(2,6,8) = (4,8,9) - Assert.AreEqual(new Vector3(4, 8, 9), lerpResult); - - var thirdVector = new Vector3(13, 2, 9); - var fourthVector = new Vector3(3, 10, 5); - var secondLerpResult = Vector3.Lerp(thirdVector, fourthVector, 0.25); - - // ((3,10,5)-(13,2,9))*0.25+(13,2,9) = (-10,8,-4)*0.25+(13,2,9) = (-2.5,2,-1)+(13,2,9) = (10.5,4,8) - Assert.AreEqual(new Vector3(10.5, 4, 8), secondLerpResult); - } - - [TestMethod] - [ExpectedException(typeof(InvalidOperationException))] - public void CanNotCalculateAngleBetweenZeroVectors() - { - var angle = Vector2.Zero.Angle(Vector2.UnitX); - } - - [TestMethod] - [ExpectedException(typeof(IndexOutOfRangeException))] - public void CanNotUseIndexerWithInvalidIndex() - { - var vector = new Vector3(10, 5, 3); - var w = vector[3]; // Throws an exception - } - - [TestMethod] - public void CanRotateTwoDimensionalVector() - { - var firstVector = new Vector2(0, 1); - var rotatedVector = Matrix3x3.Rotation(MathHelper.DegreesToRadians(180)) * firstVector; - Assert.IsTrue(FloatingNumber.AreApproximatelyEqual(firstVector.X, 0)); - Assert.AreEqual(-1, rotatedVector.Y); - } - - [TestMethod] - public void CanUseIndexer() - { - var vector = new Vector3(10, 5, 3); - var x = vector[0]; - var y = vector[1]; - var z = vector[2]; - - Assert.AreEqual(x, 10); - Assert.AreEqual(y, 5); - Assert.AreEqual(z, 3); - } - - [TestMethod] - public void CompareAreaCalculations() - { - var stopWatch = new Stopwatch(); - - // ------------------------- 3D ------------------------------- - - var firstVector = new Vector3(3, 4, 4); - var secondVector = new Vector3(1, -2, 3); - - stopWatch.Start(); - var crossProductArea = Vector3.VectorProduct(firstVector, secondVector).Magnitude; - stopWatch.Stop(); - // This is faster, if the vectors are already 3-dimensional, because we have no arccos, sin etc. - Debug.Print("Vector3 area calculation over the cross product takes " + stopWatch.ElapsedMilliseconds + - " milliseconds."); - - stopWatch.Restart(); - var defaultFormulaArea = firstVector.Magnitude * Math.Sin(firstVector.Angle(secondVector)) * - secondVector.Magnitude; - stopWatch.Stop(); - Debug.Print("Vector3 area calculation over the default formula takes " + stopWatch.ElapsedMilliseconds + - " milliseconds."); - - // ------------------------- 2D ------------------------------- - - var thirdVector = new Vector2(3, 4); - var fourthVector = new Vector2(1, -2); - - stopWatch.Restart(); - var secondCrossProductArea = - Vector3.VectorProduct(thirdVector.Convert(), fourthVector.Convert()).Magnitude; - stopWatch.Stop(); - Debug.Print("Vector2 area calculation over the cross product takes " + stopWatch.ElapsedMilliseconds + - " milliseconds."); - - stopWatch.Restart(); - var secondDefaultFormulaArea = thirdVector.Magnitude * Math.Sin(thirdVector.Angle(fourthVector)) * - fourthVector.Magnitude; - stopWatch.Stop(); - // This is faster because we don't convert the vector - Debug.Print("Vector2 area calculation over the default formula takes " + stopWatch.ElapsedMilliseconds + - " milliseconds."); - } - } -} \ No newline at end of file diff --git a/SharpMath.png b/SharpMath.png deleted file mode 100644 index e29171e..0000000 Binary files a/SharpMath.png and /dev/null differ diff --git a/SharpMath.sln b/SharpMath.sln deleted file mode 100644 index 092fe06..0000000 --- a/SharpMath.sln +++ /dev/null @@ -1,46 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 14 -VisualStudioVersion = 14.0.23107.0 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpMath", "SharpMath\SharpMath.csproj", "{4706AE0F-30C8-460B-BBBF-5F189BE4EB76}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpMath.Tests", "SharpMath.Tests\SharpMath.Tests.csproj", "{42B815A5-DDAA-4DED-BD69-FA212C1D0D46}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpMath.3DTest", "SharpMath.3DTest\SharpMath.3DTest.csproj", "{15428131-2AC9-4AF4-8966-664FF1974CE8}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpMath.PolygonTest", "SharpMath.PolygonTest\SharpMath.PolygonTest.csproj", "{1D3DD70B-E845-4993-A73E-7EBD14B9E005}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpMath.Canvas2DTest", "SharpMath.CanvasTest\SharpMath.Canvas2DTest.csproj", "{E8982E21-DEED-40EA-8DF9-0BD161AD1490}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {4706AE0F-30C8-460B-BBBF-5F189BE4EB76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4706AE0F-30C8-460B-BBBF-5F189BE4EB76}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4706AE0F-30C8-460B-BBBF-5F189BE4EB76}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4706AE0F-30C8-460B-BBBF-5F189BE4EB76}.Release|Any CPU.Build.0 = Release|Any CPU - {42B815A5-DDAA-4DED-BD69-FA212C1D0D46}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {42B815A5-DDAA-4DED-BD69-FA212C1D0D46}.Debug|Any CPU.Build.0 = Debug|Any CPU - {42B815A5-DDAA-4DED-BD69-FA212C1D0D46}.Release|Any CPU.ActiveCfg = Release|Any CPU - {42B815A5-DDAA-4DED-BD69-FA212C1D0D46}.Release|Any CPU.Build.0 = Release|Any CPU - {15428131-2AC9-4AF4-8966-664FF1974CE8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {15428131-2AC9-4AF4-8966-664FF1974CE8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {15428131-2AC9-4AF4-8966-664FF1974CE8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {15428131-2AC9-4AF4-8966-664FF1974CE8}.Release|Any CPU.Build.0 = Release|Any CPU - {1D3DD70B-E845-4993-A73E-7EBD14B9E005}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1D3DD70B-E845-4993-A73E-7EBD14B9E005}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1D3DD70B-E845-4993-A73E-7EBD14B9E005}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1D3DD70B-E845-4993-A73E-7EBD14B9E005}.Release|Any CPU.Build.0 = Release|Any CPU - {E8982E21-DEED-40EA-8DF9-0BD161AD1490}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E8982E21-DEED-40EA-8DF9-0BD161AD1490}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E8982E21-DEED-40EA-8DF9-0BD161AD1490}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E8982E21-DEED-40EA-8DF9-0BD161AD1490}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection -EndGlobal diff --git a/SharpMath.sln.GhostDoc.xml b/SharpMath.sln.GhostDoc.xml deleted file mode 100644 index 0c987e9..0000000 --- a/SharpMath.sln.GhostDoc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - *.min.js - jquery*.js - - diff --git a/SharpMath.slnx b/SharpMath.slnx new file mode 100644 index 0000000..7955acf --- /dev/null +++ b/SharpMath.slnx @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SharpMath/Algorithms.cs b/SharpMath/Algorithms.cs deleted file mode 100644 index b414997..0000000 --- a/SharpMath/Algorithms.cs +++ /dev/null @@ -1,90 +0,0 @@ -// Algorithms.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -using System.Collections.Generic; -using SharpMath.Expressions; -using SharpMath.Expressions.Exceptions; - -namespace SharpMath -{ - /// - /// Provides algorithms for common mathematical operations. - /// - public static class Algorithms - { - /// - /// Converts infix tokens to postfix tokens. - /// - /// The infix tokens to convert. - /// The postfix tokens.> - public static IEnumerable ShuntingYard(List infixTokens) - { - var opStack = new Stack>(); - var output = new List(infixTokens.Count); - - Token lastToken = null; - foreach (var token in infixTokens) - { - if (lastToken != null) - { - // A constant value or number must be followed by an operator or closing bracket... - if ((lastToken.Type == TokenType.Constant || lastToken.Type == TokenType.Number) && - token.Type != TokenType.Operator && !token.IsClosingBracket()) - throw new ParserException( - "Cannot calculate the postfix tokens of the given input as the term is invalid. A constant or number must be followed by an operator or a closing bracket."); - - // A closing bracket must be followed by an operator or another closing bracket... - if (lastToken.IsClosingBracket() && token.Type != TokenType.Operator && !token.IsClosingBracket()) - throw new ParserException( - "Cannot calculate the postfix tokens of the given input as the term is invalid. A closing bracket must be followed by an operator or another closing bracket."); - } - - lastToken = token; - switch (token.Type) - { - case TokenType.Number: - output.Add((Token) token); - break; - case TokenType.Function: - opStack.Push((Token) token); - break; - case TokenType.Operator: - var currentOperatorToken = (Token) token; - while (opStack.Count > 0 && opStack.Peek().Type == TokenType.Operator && - (!currentOperatorToken.IsRightAssociative && - currentOperatorToken.Priority == opStack.Peek().Priority || - currentOperatorToken.Priority < opStack.Peek().Priority)) - output.Add(opStack.Pop()); - opStack.Push(currentOperatorToken); - break; - case TokenType.Bracket: - var bracketToken = (Token) token; - switch (bracketToken.Value) - { - case "(": - opStack.Push(bracketToken); - break; - case ")": - while (opStack.Peek().Value != "(") - output.Add(opStack.Pop()); - opStack.Pop(); - - if (opStack.Count > 0 && opStack.Peek().Type == TokenType.Function) - output.Add(opStack.Pop()); - break; - } - - break; - case TokenType.Constant: - output.Add((Token) token); - break; - } - } - - while (opStack.Count > 0) // Flush the remaining stuff - output.Add(opStack.Pop()); - - return output; - } - } -} \ No newline at end of file diff --git a/SharpMath/Analysis/Domain.cs b/SharpMath/Analysis/Domain.cs deleted file mode 100644 index 840df40..0000000 --- a/SharpMath/Analysis/Domain.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Collections.Generic; - -namespace SharpMath.Analysis -{ - public struct Domain - { - public IEnumerable Exceptions { get; set; } - public Signs SignAreas { get; set; } - } -} diff --git a/SharpMath/Analysis/Function.cs b/SharpMath/Analysis/Function.cs deleted file mode 100644 index 4681958..0000000 --- a/SharpMath/Analysis/Function.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.Globalization; -using SharpMath.Expressions; - -namespace SharpMath.Analysis -{ - /// - /// Represents a mathematical function. - /// - public class Function - { - public Function(string term) - { - - } - - public string Term { get; set; } - - /// - /// Gets the poles of this . - /// - public double[] Poles { get; } - - /// - /// Gets the roots of this . - /// - public double[] Roots { get; } - - /// - /// Gets the of this . - /// - public Domain Domain { get; } - - /// - /// Calculates the derivation of this . - /// - /// The derivation of this . - public Function Derive() - { - return null; - } - - /// - /// Gets the slope of the tangent that touches the with the specified x-coordinate. - /// - /// The x-coordinate value of the the tangent touches. - /// The slope of the . - public double Derive(double xValue) - { - var parser = new Parser(Derive().Term.Replace("x", xValue.ToString(CultureInfo.InvariantCulture))); - return parser.Evaluate(); - } - } -} \ No newline at end of file diff --git a/SharpMath/Analysis/Interval.cs b/SharpMath/Analysis/Interval.cs deleted file mode 100644 index 45cdb2f..0000000 --- a/SharpMath/Analysis/Interval.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace SharpMath.Analysis -{ - public struct Interval - { - public Interval(IntervalValue leftSide, IntervalValue rightSide) - { - LeftSide = leftSide; - RightSide = rightSide; - } - - public IntervalValue LeftSide { get; set; } - public IntervalValue RightSide { get; set; } - - public override string ToString() - { - return $"{LeftSide.ValueOption.GetEnumDescription()}{LeftSide.Value};{RightSide.Value}{RightSide.ValueOption.GetEnumDescription()}"; - } - } -} \ No newline at end of file diff --git a/SharpMath/Analysis/IntervalValue.cs b/SharpMath/Analysis/IntervalValue.cs deleted file mode 100644 index 9e43f9b..0000000 --- a/SharpMath/Analysis/IntervalValue.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace SharpMath.Analysis -{ - public struct IntervalValue - { - public IntervalValue(double value, IntervalValueOption valueOption) - { - Value = value; - ValueOption = valueOption; - } - - public double Value { get; set; } - public IntervalValueOption ValueOption { get; set; } - } -} diff --git a/SharpMath/Analysis/IntervalValueOption.cs b/SharpMath/Analysis/IntervalValueOption.cs deleted file mode 100644 index 0e417ff..0000000 --- a/SharpMath/Analysis/IntervalValueOption.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.ComponentModel; - -namespace SharpMath.Analysis -{ - public enum IntervalValueOption - { - [Description("[")] - Include, - [Description("]")] - Exclude, - } -} \ No newline at end of file diff --git a/SharpMath/Analysis/IntervalValueOptionAttribute.cs b/SharpMath/Analysis/IntervalValueOptionAttribute.cs deleted file mode 100644 index 7950f90..0000000 --- a/SharpMath/Analysis/IntervalValueOptionAttribute.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; - -namespace SharpMath.Analysis -{ - public class IntervalValueOptionAttribute : Attribute - { - public IntervalValueOption ValueOption { get; set; } - - public IntervalValueOptionAttribute(IntervalValueOption valueOption) - { - ValueOption = valueOption; - } - } -} diff --git a/SharpMath/Analysis/Monotony.cs b/SharpMath/Analysis/Monotony.cs deleted file mode 100644 index 6678da3..0000000 --- a/SharpMath/Analysis/Monotony.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace SharpMath.Analysis -{ - public class Monotony - { - public Dictionary MonotonyIntervals { get; set; } = new Dictionary(); - - //public Monotony Determine(string term) - //{ - // Determine(new Function(term)); - //} - - //public Monotony Determine(Function function) - //{ - // var monotony = new Monotony(); - // var derivation = function.Derive(); - // double[] roots = derivation.Roots; - // double[] poles = derivation.Poles; - - // var specialValues = roots.ToList().Concat(poles).ToList(); - // double beginElement = double.NegativeInfinity; - // if ((derivation.Domain.SignAreas & Signs.Negative) != Signs.Negative) - // { - // beginElement = specialValues.Min(); - // specialValues.Remove(beginElement); - // } - - // // If the value is a root it can neither be a pole, nor infinity and must be included. - // var determineIntervalValueOption = new Func(d => roots.Contains(d) ? IntervalValueOption.Include : IntervalValueOption.Exclude); - // foreach (var value in specialValues) - // { - // var interval = - // new Interval(new IntervalValue(beginElement, determineIntervalValueOption(beginElement)), - // new IntervalValue(value, determineIntervalValueOption(value))); - // double difference = 0; - - // beginElement = value; - // } - //} - } -} \ No newline at end of file diff --git a/SharpMath/Analysis/MonotonyType.cs b/SharpMath/Analysis/MonotonyType.cs deleted file mode 100644 index 9e1fdb9..0000000 --- a/SharpMath/Analysis/MonotonyType.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace SharpMath.Analysis -{ - public enum MonotonyType - { - MonotonicallyIncreasing, - StrictlyMonotonicallyIncreasing, - MonotonicallyDecreasing, - StrictlyMonotonicallyDecreasing, - } -} \ No newline at end of file diff --git a/SharpMath/Analysis/Rule.cs b/SharpMath/Analysis/Rule.cs deleted file mode 100644 index 33e73a6..0000000 --- a/SharpMath/Analysis/Rule.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Collections.Generic; - -namespace SharpMath.Analysis -{ - public class Rule - { - - } -} \ No newline at end of file diff --git a/SharpMath/Analysis/RuleSet/IRuleSet.cs b/SharpMath/Analysis/RuleSet/IRuleSet.cs deleted file mode 100644 index df985b1..0000000 --- a/SharpMath/Analysis/RuleSet/IRuleSet.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Collections.Generic; - -namespace SharpMath.Analysis.RuleSet -{ - public interface IRuleSet - { - IEnumerable Rules { get; set; } - } -} \ No newline at end of file diff --git a/SharpMath/Analysis/Signs.cs b/SharpMath/Analysis/Signs.cs deleted file mode 100644 index c0a6fa5..0000000 --- a/SharpMath/Analysis/Signs.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; - -namespace SharpMath.Analysis -{ - [Flags] - public enum Signs - { - Positive, - Negative, - } -} diff --git a/SharpMath/Equations/Exceptions/EquationNotSolvableException.cs b/SharpMath/Equations/Exceptions/EquationNotSolvableException.cs deleted file mode 100644 index 4d3414e..0000000 --- a/SharpMath/Equations/Exceptions/EquationNotSolvableException.cs +++ /dev/null @@ -1,22 +0,0 @@ -// EquationNotSolvableException.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -using System; - -namespace SharpMath.Equations.Exceptions -{ - /// - /// The exception that is thrown when an equation is not solvable. - /// - /// - public class EquationNotSolvableException : Exception - { - /// - /// Initializes a new instance of the class. - /// - /// The message that describes the error. - public EquationNotSolvableException(string message) : base(message) - { - } - } -} \ No newline at end of file diff --git a/SharpMath/Equations/LinearEquation.cs b/SharpMath/Equations/LinearEquation.cs deleted file mode 100644 index c4624db..0000000 --- a/SharpMath/Equations/LinearEquation.cs +++ /dev/null @@ -1,30 +0,0 @@ -// LinearEquation.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -namespace SharpMath.Equations -{ - /// - /// Represents a linear equation. - /// - public class LinearEquation - { - public LinearEquation() : this(new double[] { }, 0) - { - } - - public LinearEquation(double[] coefficients, double result) - { - Coefficients = coefficients; - Result = result; - } - - public LinearEquation(double result, params double[] coefficients) - { - Coefficients = coefficients; - Result = result; - } - - public double[] Coefficients { get; set; } - public double Result { get; set; } - } -} \ No newline at end of file diff --git a/SharpMath/Equations/LinearEquationSystem.cs b/SharpMath/Equations/LinearEquationSystem.cs deleted file mode 100644 index 247b5dd..0000000 --- a/SharpMath/Equations/LinearEquationSystem.cs +++ /dev/null @@ -1,87 +0,0 @@ -// LinearEquationSystem.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -using System; -using System.Collections.Generic; -using System.Linq; -using SharpMath.Equations.Exceptions; -using SharpMath.Geometry; - -namespace SharpMath.Equations -{ - /// - /// Represents a linear equation system. - /// - public class LinearEquationSystem - { - /// - /// Initializes a new instance of the class. - /// - public LinearEquationSystem() - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// The s that this should - /// contain. - /// - public LinearEquationSystem(IEnumerable equations) - { - Equations.AddRange(equations); - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// The s that this should - /// contain. - /// - public LinearEquationSystem(params LinearEquation[] equations) - { - Equations.AddRange(equations); - } - - /// - /// Gets or sets the s of this . - /// - public List Equations { get; set; } = new List(); - - /// - /// Solves this using the Gauss-Jordan algorithm. - /// - /// The result of each variable in chronological order. - public double[] Solve() - { - var count = (uint) Equations.Count; - if (count == 0) - throw new ArgumentException("There must be at least one equation to solve."); - - var coefficientsCount = Equations.Select(item => item.Coefficients).Count(); - if (coefficientsCount == 0 || coefficientsCount != count) - throw new EquationNotSolvableException("This linear equation system cannot be solved."); - - var leftSide = new VariableMatrix(count, count); - var rightSide = new VariableMatrix(count, 1); - - for (uint i = 0; i < count; ++i) - { - var currentEquation = Equations[(int) i]; - var coefficients = currentEquation.Coefficients; - for (uint c = 0; c < coefficients.Length; ++c) - leftSide[i, c] = coefficients[c]; - rightSide[i, 0] = currentEquation.Result; - } - - var resultMatrix = MatrixUtils.GaussJordan(leftSide, rightSide); - var result = new double[rightSide.RowCount]; - for (uint i = 0; i < resultMatrix.RowCount; ++i) - result[i] = resultMatrix[i, 0]; - - return result; - } - } -} \ No newline at end of file diff --git a/SharpMath/Expressions/CharEx.cs b/SharpMath/Expressions/CharEx.cs deleted file mode 100644 index 1c75e14..0000000 --- a/SharpMath/Expressions/CharEx.cs +++ /dev/null @@ -1,18 +0,0 @@ -// CharEx.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -namespace SharpMath.Expressions -{ - public class CharEx - { - public static bool IsBracket(char c) - { - return c == '(' || c == ')'; - } - - public static bool IsMathematicOperator(char c) - { - return "+-*/%^!".IndexOf(c) >= 0; - } - } -} \ No newline at end of file diff --git a/SharpMath/Expressions/Exceptions/ParserException.cs b/SharpMath/Expressions/Exceptions/ParserException.cs deleted file mode 100644 index 9da5673..0000000 --- a/SharpMath/Expressions/Exceptions/ParserException.cs +++ /dev/null @@ -1,23 +0,0 @@ -// ParserException.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -using System; - -namespace SharpMath.Expressions.Exceptions -{ - /// - /// The exception that is thrown when the parser encountered an invalid token. - /// - /// - public class ParserException : Exception - { - /// - /// Initializes a new instance of the class. - /// - /// The message that describes the error. - public ParserException(string message) - : base(message) - { - } - } -} \ No newline at end of file diff --git a/SharpMath/Expressions/Parser.cs b/SharpMath/Expressions/Parser.cs deleted file mode 100644 index e4bc6fd..0000000 --- a/SharpMath/Expressions/Parser.cs +++ /dev/null @@ -1,49 +0,0 @@ -// Parser.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -using System; -using System.Collections.Generic; -using System.Linq; - -namespace SharpMath.Expressions -{ - /// - /// Provides functions to evaluate mathematic terms. - /// - public class Parser - { - /// - /// Initializes a new instance of the class. - /// - /// The expression/term that should evaluated. - public Parser(string expression) - { - if (expression == null) - throw new ArgumentNullException(nameof(expression)); - Expression = expression; - } - - /// - /// Gets or sets the expression/term that should be evaluated. - /// - public string Expression { get; set; } - - /// - /// Evaluates the term. - /// - /// The result of the evaluation. - public double Evaluate() - { - if (string.IsNullOrWhiteSpace(Expression)) - throw new Exception("The term that should be evaluated is empty."); - - var resultStack = new Stack(); - var infixTokens = Token.CalculateInfixTokens(Expression); - var postfixTokens = Algorithms.ShuntingYard(infixTokens.ToList()); - foreach (var pToken in postfixTokens) - pToken.Evaluate(resultStack); - - return resultStack.Pop(); - } - } -} \ No newline at end of file diff --git a/SharpMath/Expressions/Token.cs b/SharpMath/Expressions/Token.cs deleted file mode 100644 index c43a8cf..0000000 --- a/SharpMath/Expressions/Token.cs +++ /dev/null @@ -1,317 +0,0 @@ -// Token.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using SharpMath.Expressions.Exceptions; - -// ReSharper disable InconsistentNaming - -namespace SharpMath.Expressions -{ - /// - /// Represents a part of a mathematic term. - /// - public class Token - { - private static readonly Regex SWhitespace = new Regex(@"\s+"); - - protected static Dictionary>> _functionActions = new Dictionary - >> - { - {"sin", stack => stack.Push(Math.Sin(stack.Pop()))}, - {"cos", stack => stack.Push(Math.Cos(stack.Pop()))}, - {"tan", stack => stack.Push(Math.Tan(stack.Pop()))}, - {"asin", stack => stack.Push(Math.Asin(stack.Pop()))}, - {"acos", stack => stack.Push(Math.Acos(stack.Pop()))}, - {"atan", stack => stack.Push(Math.Atan(stack.Pop()))}, - {"sqrt", stack => stack.Push(Math.Sqrt(stack.Pop()))}, - {"abs", stack => stack.Push(Math.Abs(stack.Pop()))}, - {"ln", stack => stack.Push(Math.Log(stack.Pop()))}, - {"lg", stack => stack.Push(Math.Log(stack.Pop(), 10))}, - { - "log", stack => - { - var exponent = stack.Pop(); - stack.Push(Math.Log(exponent, stack.Pop())); - } - } - }; - - protected static Dictionary>> _constantActions = new Dictionary - >> - { - {"pi", stack => stack.Push(Math.PI)}, - {"e", stack => stack.Push(Math.E)} - }; - - protected static Dictionary>> _operatorActions = new Dictionary - >> - { - {"+", stack => stack.Push(stack.Pop() + stack.Pop())}, - { - "-", stack => - { - var subtrahend = stack.Pop(); - stack.Push(stack.Pop() - subtrahend); - } - }, - {"*", stack => stack.Push(stack.Pop() * stack.Pop())}, - { - "/", stack => - { - var divisor = stack.Pop(); - stack.Push(stack.Pop() / divisor); - } - }, - {"!", stack => stack.Push(-stack.Pop())}, - { - "^", stack => - { - var exponent = stack.Pop(); - stack.Push(Math.Pow(stack.Pop(), exponent)); - } - } - }; - - /// - /// Initializes a new instance of the class. - /// - /// The type of the . - public Token(TokenType type) - { - Type = type; - } - - /// - /// Gets or sets the type of this . - /// - public TokenType Type { get; set; } - - /// - /// Creates an containing infix tokens from partitioning the specified term. - /// - /// The term to partition into infix tokens. - /// containing the created infix tokens. - public static IEnumerable CalculateInfixTokens(string term) - { - var infixTokens = new List(); - term = SWhitespace.Replace(term, string.Empty).ToLowerInvariant(); - var i = 0; - while (i < term.Length) - { - var current = term[i]; - if (char.IsLetter(current) || CharEx.IsMathematicOperator(current) || CharEx.IsBracket(current)) - // Functions/Operators/Constants - { - if ((current == '+' || current == '-') && - (i == 0 || - infixTokens.Last().Type == TokenType.Bracket && - ((Token) infixTokens.Last()).Value == "(")) - // Must be a sign and handled differently. - { - if (current == '-') - { - var sb = new StringBuilder(term) {[term.IndexOf(current)] = '!'}; - term = sb.ToString(); - } - else - { - term.Remove(term.IndexOf(current), 1); - } - - // Remove the '+' as it is redundant and disturbs our calculations - } - - infixTokens.Add(ReadStringToken(term, ref i)); - } - else if (char.IsDigit(current)) // Numbers - { - infixTokens.Add(ReadNumberToken(term, ref i)); - } - else - { - throw new ParserException($"Char {current} cannot be interpreted as a valid token."); - } - } - - return infixTokens; - } - - /// - /// Evaluates this . - /// - /// The to take relating from and push the result to. - public void Evaluate(Stack stack) - { - switch (Type) - { - case TokenType.Number: - var token = this as Token; - if (token != null) - stack.Push(token.Value); - break; - case TokenType.Function: - var token1 = this as Token; - if (token1 != null) - _functionActions[token1.Value](stack); - break; - case TokenType.Operator: - var token2 = this as Token; - if (token2 != null) - _operatorActions[token2.Value](stack); - break; - case TokenType.Constant: - var token3 = this as Token; - if (token3 != null) - _constantActions[token3.Value](stack); - break; - } - } - - /// - /// Reads a number token in the input string starting at the specified index. - /// - /// The input string. - /// The index to start at. - /// The that has been read. - public static Token ReadNumberToken(string input, ref int index) - { - var endIndex = index; - while (endIndex < input.Length && (char.IsDigit(input, endIndex) || input[endIndex] == '.')) - endIndex++; - var numberToken = input.Substring(index, endIndex - index); - double numberValue; - if ( - !double.TryParse(numberToken, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, - out numberValue)) - throw new ParserException($"Invalid token: {numberToken} is not a valid number."); - index = endIndex; - return new Token(numberValue, TokenType.Number); - } - - /// - /// Reads a bracket, constant, operator or function token in the input string starting at the specified index. - /// - /// The input string. - /// The index to start at. - /// The that has been read. - public static Token ReadStringToken(string input, ref int index) - { - TokenType newType; - var endIndex = index; - while (endIndex < input.Length && - (char.IsLetter(input, endIndex) || CharEx.IsMathematicOperator(input[endIndex]) || - CharEx.IsBracket(input[endIndex]))) - { - endIndex++; - var currentString = input.Substring(index, endIndex - index); - if (_functionActions.ContainsKey(currentString) || _operatorActions.ContainsKey(currentString) || - _constantActions.ContainsKey(currentString) || - currentString.IsBracket() - ) // Last two conditions to parse e.g. "* sin(5)" correctly: As soon as the current token has finished, we should stop reading. - break; // This indicates that it has been found in the dictionary, so that is already it. - } - - var stringToken = input.Substring(index, endIndex - index); - - if (_functionActions.ContainsKey(stringToken)) - newType = TokenType.Function; - else if (_operatorActions.ContainsKey(stringToken)) - newType = TokenType.Operator; - else if (stringToken.IsBracket()) - newType = TokenType.Bracket; - else if (_constantActions.ContainsKey(stringToken)) - newType = TokenType.Constant; - else - throw new ParserException( - $"Invalid token: {stringToken} is not a valid function/operator, bracket or number equivalent."); - - index = endIndex; - return new Token(stringToken, newType); - } - } - - /// - /// Represents a part of a mathematic term with a specific data type that should represent it. - /// - /// The data type of the that represents it. - public class Token : Token - { - /// - /// Initializes a new instance of the class. - /// - /// The value of the . - /// The type of the . - public Token(T value, TokenType type) - : base(type) - { - Value = value; - switch (type) - { - case TokenType.Number: - case TokenType.Constant: - Priority = 100; - break; - case TokenType.Operator: - switch (value as string) - { - case "+": - Priority = 1; - IsRightAssociative = false; - break; - case "-": - Priority = 1; - IsRightAssociative = false; - break; - case "*": - Priority = 2; - IsRightAssociative = false; - break; - case "/": - Priority = 2; - IsRightAssociative = false; - break; - case "%": - Priority = 3; - IsRightAssociative = false; - break; - case "!": - Priority = 4; - IsRightAssociative = false; - break; - case "^": - Priority = 5; - IsRightAssociative = true; - break; - } - - break; - } - } - - /// - /// Gets or sets a value indicating whether this is right associative, or not. - /// - public bool IsRightAssociative { get; set; } - - /// - /// Gets or sets the priority of this . - /// - public int Priority { get; set; } - - /// - /// Gets or sets the value of this . - /// - public T Value { get; set; } - - public override string ToString() - { - return Value.ToString(); - } - } -} \ No newline at end of file diff --git a/SharpMath/Expressions/TokenEx.cs b/SharpMath/Expressions/TokenEx.cs deleted file mode 100644 index 50c95f1..0000000 --- a/SharpMath/Expressions/TokenEx.cs +++ /dev/null @@ -1,18 +0,0 @@ -// TokenEx.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -namespace SharpMath.Expressions -{ - public static class TokenEx - { - public static bool IsClosingBracket(this Token token) - { - return token.Type == TokenType.Bracket && ((Token) token).Value == ")"; - } - - public static bool IsOpeningBracket(this Token token) - { - return token.Type == TokenType.Bracket && ((Token) token).Value == "("; - } - } -} \ No newline at end of file diff --git a/SharpMath/Expressions/TokenType.cs b/SharpMath/Expressions/TokenType.cs deleted file mode 100644 index b3806e4..0000000 --- a/SharpMath/Expressions/TokenType.cs +++ /dev/null @@ -1,17 +0,0 @@ -// TokenType.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -namespace SharpMath.Expressions -{ - /// - /// Represents the different token types of a mathematic term. - /// - public enum TokenType - { - Function, - Operator, - Number, - Constant, - Bracket - } -} \ No newline at end of file diff --git a/SharpMath/Extensions.cs b/SharpMath/Extensions.cs deleted file mode 100644 index 6fb4ccd..0000000 --- a/SharpMath/Extensions.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Extensions.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -using System; -using System.Collections.Generic; -using System.ComponentModel; - -namespace SharpMath -{ - internal static class Extensions - { - public static IEnumerable AllIndicesOf(this string str, string searchString) - { - var minIndex = str.IndexOf(searchString, StringComparison.Ordinal); - while (minIndex != -1) - { - yield return minIndex; - minIndex = str.IndexOf(searchString, minIndex + searchString.Length, StringComparison.Ordinal); - } - } - - public static string GetEnumDescription(this Enum value) - { - var fi = value.GetType().GetField(value.ToString()); - - var attributes = - (DescriptionAttribute[]) fi.GetCustomAttributes( - typeof(DescriptionAttribute), - false); - - if (attributes.Length > 0) - return attributes[0].Description; - return value.ToString(); - } - - public static bool IsBracket(this string s) - { - return s == "(" || s == ")"; - } - } -} \ No newline at end of file diff --git a/SharpMath/FloatingNumber.cs b/SharpMath/FloatingNumber.cs deleted file mode 100644 index e314cc0..0000000 --- a/SharpMath/FloatingNumber.cs +++ /dev/null @@ -1,66 +0,0 @@ -// FloatingNumber.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -using System; - -namespace SharpMath -{ - /// - /// Provides functions for comparing floating numbers using approximations. - /// - public static class FloatingNumber - { - /// - /// Gets the default tolerance value for comparing floating numbers. - /// - public static double Epsilon => 1.6234133E-09; - - /// - /// Determines whether two floating numbers are approximately equal to each other using the - /// value. - /// - /// The first . - /// The second . - /// Returns true, if they are approximately equal, otherwise false. - public static bool AreApproximatelyEqual(float firstNumber, float secondNumber) - { - return AreApproximatelyEqual(firstNumber, secondNumber, Epsilon); - } - - /// - /// Determines whether two floating numbers are approximately equal to each other using the specified epsilon value. - /// - /// The first . - /// The second . - /// The epsilon value that represents the tolerance. - /// Returns true, if they are approximately equal, otherwise false. - public static bool AreApproximatelyEqual(float firstNumber, float secondNumber, double epsilon) - { - return Math.Abs(firstNumber - secondNumber) <= epsilon; - } - - /// - /// Determines whether two floating numbers are approximately equal to each other using the - /// value. - /// - /// The first . - /// The second . - /// Returns true, if they are approximately equal, otherwise false. - public static bool AreApproximatelyEqual(double firstNumber, double secondNumber) - { - return AreApproximatelyEqual(firstNumber, secondNumber, Epsilon); - } - - /// - /// Determines whether two floating numbers are approximately equal to each other using the specified epsilon value. - /// - /// The first . - /// The second . - /// The epsilon value that represents the tolerance. - /// Returns true, if they are approximately equal, otherwise false. - public static bool AreApproximatelyEqual(double firstNumber, double secondNumber, double epsilon) - { - return Math.Abs(firstNumber - secondNumber) <= epsilon; - } - } -} \ No newline at end of file diff --git a/SharpMath/FloatingNumberExtensions.cs b/SharpMath/FloatingNumberExtensions.cs deleted file mode 100644 index 2b0825a..0000000 --- a/SharpMath/FloatingNumberExtensions.cs +++ /dev/null @@ -1,60 +0,0 @@ -// FloatingNumberExtensions.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -namespace SharpMath -{ - /// - /// Provides extensions for comparing floating numbers using approximations. - /// - public static class FloatingNumberExtensions - { - /// - /// Determines whether two floating numbers are approximately equal to each other using the - /// value. - /// - /// The current . - /// The other . - /// Returns true, if they are approximately equal, otherwise false. - public static bool IsApproximatelyEqualTo(this float number, float other) - { - return FloatingNumber.AreApproximatelyEqual(number, other); - } - - /// - /// Determines whether two floating numbers are approximately equal to each other using the specified epsilon value. - /// - /// The current . - /// The other . - /// The epsilon value that represents the tolerance. - /// Returns true, if they are approximately equal, otherwise false. - public static bool IsApproximatelyEqualTo(this float number, float other, double epsilon) - { - return FloatingNumber.AreApproximatelyEqual(number, other, epsilon); - } - - /// - /// Determines whether two floating numbers are approximately equal to each other using the - /// - /// value. - /// - /// The current . - /// The other . - /// Returns true, if they are approximately equal, otherwise false. - public static bool IsApproximatelyEqualTo(this double number, double other) - { - return FloatingNumber.AreApproximatelyEqual(number, other); - } - - /// - /// Determines whether two floating numbers are approximately equal to each other using the specified epsilon value. - /// - /// The current . - /// The other . - /// The epsilon value that represents the tolerance. - /// Returns true, if they are approximately equal, otherwise false. - public static bool IsApproximatelyEqualTo(this double number, double other, double epsilon) - { - return FloatingNumber.AreApproximatelyEqual(number, other, epsilon); - } - } -} \ No newline at end of file diff --git a/SharpMath/Geometry/Exceptions/DimensionException.cs b/SharpMath/Geometry/Exceptions/DimensionException.cs deleted file mode 100644 index 5e0e5cd..0000000 --- a/SharpMath/Geometry/Exceptions/DimensionException.cs +++ /dev/null @@ -1,23 +0,0 @@ -// DimensionException.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -using System; - -namespace SharpMath.Geometry.Exceptions -{ - /// - /// The exception that is thrown when the dimension count of two vectors is different. - /// - /// - public class DimensionException : Exception - { - /// - /// Initializes a new instance of the class. - /// - /// The message that describes the error. - public DimensionException(string message) - : base(message) - { - } - } -} \ No newline at end of file diff --git a/SharpMath/Geometry/IMatrix.cs b/SharpMath/Geometry/IMatrix.cs deleted file mode 100644 index 3a7c286..0000000 --- a/SharpMath/Geometry/IMatrix.cs +++ /dev/null @@ -1,13 +0,0 @@ -// IMatrix.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -namespace SharpMath.Geometry -{ - public interface IMatrix - { - uint ColumnCount { get; } - double this[uint row, uint column] { get; set; } - double this[uint index] { get; set; } - uint RowCount { get; } - } -} \ No newline at end of file diff --git a/SharpMath/Geometry/ISquareMatrix.cs b/SharpMath/Geometry/ISquareMatrix.cs deleted file mode 100644 index a071128..0000000 --- a/SharpMath/Geometry/ISquareMatrix.cs +++ /dev/null @@ -1,29 +0,0 @@ -// ISquareMatrix.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -namespace SharpMath.Geometry -{ - public interface ISquareMatrix : IMatrix - { - double Determinant { get; } - uint Dimension { get; } - bool IsAntiSymmetric { get; } - bool IsDiagonal { get; } - bool IsIdentity { get; } - bool IsOrthogonal { get; } - bool IsSingular { get; } - bool IsSkewSymmetric { get; } - bool IsSymmetric { get; } - bool IsTriangle { get; } - double Trace { get; } - } - - public interface ISquareMatrix : ISquareMatrix where T : ISquareMatrix - { - T Adjugate { get; } - T CofactorMatrix { get; } - T Inverse { get; } - T Negate { get; } - T Transpose { get; } - } -} \ No newline at end of file diff --git a/SharpMath/Geometry/IVector.cs b/SharpMath/Geometry/IVector.cs deleted file mode 100644 index b91e55f..0000000 --- a/SharpMath/Geometry/IVector.cs +++ /dev/null @@ -1,40 +0,0 @@ -// IVector.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -namespace SharpMath.Geometry -{ - public interface IVector - { - /// - /// Gets the dimension of the . - /// - uint Dimension { get; } - - /// - /// Gets a value indicating whether the is normalized, or not. - /// - bool IsNormalized { get; } - - /// - /// Gets a value indicating whether the has all of its components set to zero, or not. - /// - bool IsZero { get; } - - /// - /// Gets or sets the value of the coordinate at the specified index. - /// - /// The index. - /// The value of the coordinate at the specified index. - double this[uint index] { get; set; } - - /// - /// Gets the length of the . - /// - double Magnitude { get; } - - /// - /// Gets the squared length of the . - /// - double SquareMagnitude { get; } - } -} \ No newline at end of file diff --git a/SharpMath/Geometry/Line2D.cs b/SharpMath/Geometry/Line2D.cs deleted file mode 100644 index 0c9fbcb..0000000 --- a/SharpMath/Geometry/Line2D.cs +++ /dev/null @@ -1,229 +0,0 @@ -// Line2D.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -using System; - -namespace SharpMath.Geometry -{ - /// - /// Represents a line in a 2-dimensional room. - /// - [Serializable] - public struct Line2D : IEquatable - { - /// - /// Initializes a new instance of the class. - /// - /// The slope of the . - /// The offset of the . - public Line2D(double slope, double offset) - { - Slope = slope; - Offset = offset; - } - - /// - /// Initializes a new instance of the class. - /// - /// The first that should be used to create the . - /// The second that should be used to create the . - public Line2D(Point2D a, Point2D b) - { - var vectorA = a.PositionVector; - var vectorB = b.PositionVector; - - Slope = (vectorB.Y - vectorA.Y) / (vectorB.X - vectorA.X); - Offset = vectorA.Y - Slope * vectorA.X; // Insert a point - } - - /// - /// Gets or sets the slope of the . - /// - public double Slope { get; } - - /// - /// Gets or sets the offset of the . - /// - public double Offset { get; } - - /// - /// Determines whether the specified is equal to this instance. - /// - /// The to compare with this instance. - /// - /// true if the specified is equal to this instance; otherwise, false. - /// - public bool Equals(Line2D other) - { - return ReferenceEquals(other, null) - ? ReferenceEquals(this, null) - : other.Offset == Offset && other.Slope == Slope; - } - - /// - /// Creates a using the two specified instances. - /// - /// The first . - /// The second . - /// - public static Line2D FromPoints(Point2D a, Point2D b) - { - return new Line2D(a, b); - } - - /// - /// Determines whether the specified is on the , or not. - /// - /// The point. - /// true if the is on the ; otherwise false. - public bool IsPointOnLine(Point2D point) - { - return Math.Abs(GetPoint(point.X).Y - point.Y) < FloatingNumber.Epsilon; - } - - /// - /// Determines whether this is parallel to the specified . - /// - /// The other . - /// - /// true if this is parallel to the specified , otherwise - /// false. - /// - public bool IsParallelTo(Line2D line) - { - return FloatingNumber.AreApproximatelyEqual(Slope, line.Slope); - } - - /// - /// Determines whether the specified instances are parallel to each other, or not. - /// - /// The first . - /// The second . - /// true if the instances are parallel to each other, otherwise false. - public static bool AreParallel(Line2D firstLine, Line2D secondLine) - { - return firstLine.IsParallelTo(secondLine); - } - - /// - /// Determines whether this intersects with the specified , or not. - /// - /// The other . - /// - /// true if this intersects with the specified , otherwise - /// false. - /// - public bool IntersectsWith(Line2D line) - { - return !IsParallelTo(line); - } - - /// - /// Determines whether the specified instances intersect, or not. - /// - /// The first . - /// The second . - /// true if the instances intersect, otherwise false. - public static bool AreIntersecting(Line2D firstLine, Line2D secondLine) - { - return firstLine.IntersectsWith(secondLine); - } - - /// - /// Gets the intersection of this and the specified . - /// - /// The other . - /// The intersection . - public Point2D GetIntersectionPoint(Line2D line) - { - if (IsParallelTo(line)) - return new Point2D(double.NaN, double.NaN); - - var pointX = (line.Offset - Offset) / (Slope - line.Slope); - return new Point2D(pointX, Slope * pointX + Offset); - } - - /// - /// Gets the intersection of the specified instances. - /// - /// The first . - /// The second . - /// The intersection . - public static Point2D GetIntersectionPoint(Line2D firstLine, Line2D secondLine) - { - return firstLine.GetIntersectionPoint(secondLine); - } - - /// - /// Gets the on the with the specified X-coordinate. - /// - /// The X-coordinate. - /// The with the specified X-coordinate. - public Point2D GetPoint(double x) - { - return new Point2D(x, Slope * x + Offset); - } - - /// - /// Determines whether the specified is equal to this instance. - /// - /// The to compare with this instance. - /// - /// true if the specified is equal to this instance; otherwise, false. - /// - public override bool Equals(object obj) - { - return ReferenceEquals(obj, null) - ? ReferenceEquals(this, null) - : obj is Line2D && ((Line2D) obj).Offset == Offset && ((Line2D) obj).Slope == Slope; - } - - /// - /// Returns a hash code for this instance. - /// - /// - /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. - /// - public override int GetHashCode() - { - return Offset.GetHashCode() ^ Slope.GetHashCode(); - } - - /// - /// Returns a that represents this instance. - /// - /// - /// A that represents this instance. - /// - public override string ToString() - { - return $"Line2D [Offset={Offset}; Slope={Slope}]"; - } - - /// - /// Determines whether the two specified instances are equal to each other. - /// - /// The first . - /// The to compare with the other . - /// - /// true if the two specified are equal to each other; otherwise, false. - /// - public static bool operator ==(Line2D left, Line2D right) - { - return left.Equals(right); - } - - /// - /// Determines whether the two specified instances are not equal to each other. - /// - /// The first . - /// The to compare with the other . - /// - /// true if the two specified are not equal to each other; otherwise, false. - /// - public static bool operator !=(Line2D left, Line2D right) - { - return !left.Equals(right); - } - } -} \ No newline at end of file diff --git a/SharpMath/Geometry/Line3D.cs b/SharpMath/Geometry/Line3D.cs deleted file mode 100644 index a10b255..0000000 --- a/SharpMath/Geometry/Line3D.cs +++ /dev/null @@ -1,154 +0,0 @@ -// Line3D.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -using System; - -namespace SharpMath.Geometry -{ - /// - /// Represents a 3D-line. - /// - [Serializable] - public struct Line3D : IEquatable - { - /// - /// Initializes a new instance of the class. - /// - /// A point on the . - /// The direction . - public Line3D(Point3D point, Vector3 direction) - { - Point = point; - if (direction == Vector3.Zero) - throw new InvalidOperationException("The direction vector of the line must not be the zero vector."); - Direction = direction; - } - - /// - /// Gets or sets a that represents the direction of the . - /// - public Vector3 Direction { get; } - - /// - /// Gets or sets a point on the that the line intersects with. - /// - public Point3D Point { get; } - - /// - /// Determines whether the specified is equal to this instance. - /// - /// The to compare with this instance. - /// - /// true if the specified is equal to this instance; otherwise, false. - /// - public bool Equals(Line3D other) - { - return ReferenceEquals(other, null) - ? ReferenceEquals(this, null) - : other.Direction == Direction && other.Point == Point; - } - - /// - /// Creates a from two instances. - /// - /// The first . - /// The second . - /// The created . - public static Line3D FromPoints(Point3D firstPoint, Point3D secondPoint) - { - return new Line3D(firstPoint, (secondPoint - firstPoint).PositionVector); - } - - /// - /// Gets a on the . - /// - /// The scalar that the direction vector should be multiplied with. - /// The relating on the . - public Point3D GetPoint(double lambda) - { - var resultVector = Point.PositionVector + Direction * lambda; - return new Point3D(resultVector.X, resultVector.Y, resultVector.Z); - } - - /// - /// Determines whether the specified is on the , or not. - /// - /// The . - /// true, if the is on the ; otherwise false. - public bool IsPointOnLine(Point3D point) - { - double lambda = 0; - for (uint i = 0; i < 3; ++i) - { - var value = (point[i] - Point[i]) / Direction[i]; - if (i != 0 && !FloatingNumber.AreApproximatelyEqual(value, lambda)) - return false; - lambda = value; - } - - return true; - } - - /// - /// Determines whether the specified is equal to this instance. - /// - /// The to compare with this instance. - /// - /// true if the specified is equal to this instance; otherwise, false. - /// - public override bool Equals(object obj) - { - return ReferenceEquals(obj, null) - ? ReferenceEquals(this, null) - : obj is Line3D && ((Line3D) obj).Direction == Direction && ((Line3D) obj).Point == Point; - } - - /// - /// Returns a hash code for this instance. - /// - /// - /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. - /// - public override int GetHashCode() - { - return Direction.GetHashCode() ^ Point.GetHashCode(); - } - - /// - /// Returns a that represents this instance. - /// - /// - /// A that represents this instance. - /// - public override string ToString() - { - return string.Format("Line3D [Direction={0}; Point={1}]", Direction, Point); - } - - /// - /// Determines whether the two specified instances are equal to each other. - /// - /// The first . - /// The to compare with the other . - /// - /// true if the two specified are equal to each other; otherwise, false. - /// - public static bool operator ==(Line3D left, Line3D right) - { - return left.Equals(right); - } - - /// - /// Determines whether the two specified instances are not equal to each other. - /// - /// The first . - /// The to compare with the other . - /// - /// true if the two specified are not equal to each other; otherwise, false. - /// - public static bool operator !=(Line3D left, Line3D right) - { - return !left.Equals(right); - } - } -} \ No newline at end of file diff --git a/SharpMath/Geometry/Matrix1x1.cs b/SharpMath/Geometry/Matrix1x1.cs deleted file mode 100644 index e6d6bc9..0000000 --- a/SharpMath/Geometry/Matrix1x1.cs +++ /dev/null @@ -1,310 +0,0 @@ -// Matrix1x1.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -using System; -using System.Collections; -using System.Collections.Generic; - -// ReSharper disable InconsistentNaming - -namespace SharpMath.Geometry -{ - /// - /// Represents a 1x1 matrix. - /// - [Serializable] - public struct Matrix1x1 : IEnumerable, ISquareMatrix, IEquatable - { - /// - /// Initializes a struct. - /// - /// The value at row 1 and column 1. - public Matrix1x1(double m11) - { - M11 = m11; - } - - /// - /// Gets the zero . - /// - public static Matrix1x1 Zero => new Matrix1x1(); - - /// - /// Gets the identity . - /// - public static Matrix1x1 Identity => MatrixUtils.GetIdentity(); - - /// - /// Gets or sets the value at row 1 and column 1. - /// - public double M11 { get; set; } - - /// - /// Gets a value indicating whether the is singular, or not. If true, this - /// doesn't have an inverse. - /// - public bool IsSingular => Determinant.IsApproximatelyEqualTo(0); - - /// - /// Gets the inverse of the . - /// - public Matrix1x1 Inverse => MatrixUtils.GaussJordan(this, Identity); - - /// - /// Gets the cofactor of the . - /// - public Matrix1x1 CofactorMatrix => this.BuildCofactorMatrix(); - - /// - /// Gets the adjugate of the . - /// - public Matrix1x1 Adjugate => CofactorMatrix.Transpose; - - /// - /// Gets the negated of the . - /// - public Matrix1x1 Negate => this.GetNegate(); - - /// - /// Gets the transpose of the . - /// - public Matrix1x1 Transpose => this.GetTranspose(); - - public IEnumerator GetEnumerator() - { - return new MatrixEnumerator(this); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return new MatrixEnumerator(this); - } - - public bool Equals(Matrix1x1 other) - { - return this == other; - } - - /// - /// Gets or sets the value at the specified index. The values are accessed as: row + column. - /// - /// The index. - /// The value at the specified index. - public double this[uint index] - { - get - { - switch (index) - { - case 0: - return M11; - default: - throw new IndexOutOfRangeException("The index must be 0."); - } - } - - set - { - switch (index) - { - case 0: - M11 = value; - break; - default: - throw new IndexOutOfRangeException("The index must be 0."); - } - } - } - - /// - /// Gets or sets the value at the specified row and column. - /// - /// The row. - /// The column. - /// The value at the specified row and column. - public double this[uint row, uint column] - { - get => this[row + column]; - set => this[row + column] = value; - } - - /// - /// Gets the dimension of the . - /// - public uint Dimension => 1; - - /// - /// Gets the row count of the . - /// - public uint RowCount => 1; - - /// - /// Gets the column count of the . - /// - public uint ColumnCount => 1; - - /// - /// Gets a value indicating whether the is orthogonal, or not. - /// - public bool IsOrthogonal => this * Transpose == Identity; - - /// - /// Gets a value indicating whether the is the identity , or not. - /// - public bool IsIdentity => M11.IsApproximatelyEqualTo(1); - - /// - /// Gets the determinant of the . - /// - public double Determinant => this.GetDeterminant(); // Determinant == M11 - - /// - /// Gets the trace of the . - /// - public double Trace => M11; - - /// - /// Gets a value indicating whether the is symmetric, or not. - /// - public bool IsSymmetric => this == Transpose; - - /// - /// Gets a value indicating whether the is skew symmetric, or not. - /// - public bool IsSkewSymmetric => Negate == Transpose; - - /// - /// Gets a value indicating whether the is antisymmetric, or not. - /// - public bool IsAntiSymmetric => this == Transpose.Negate; - - /// - /// Gets a value indicating whether the is a diagonal matrix, or not. - /// - public bool IsDiagonal => false; - - /// - /// Gets a value indicating whether the is a triangle matrix, or not. - /// - public bool IsTriangle => false; - - /// - /// Implements the operator +. - /// - /// The first . - /// The second . - /// - /// The result of the operator. - /// - public static Matrix1x1 operator +(Matrix1x1 firstMatrix, Matrix1x1 secondMatrix) - { - return MatrixUtils.Add(firstMatrix, secondMatrix); - } - - /// - /// Implements the operator -. - /// - /// The first . - /// The second . - /// - /// The result of the operator. - /// - public static Matrix1x1 operator -(Matrix1x1 firstMatrix, Matrix1x1 secondMatrix) - { - return MatrixUtils.Subtract(firstMatrix, secondMatrix); - } - - /// - /// Implements the operator * to multiply a with the specified scalar. - /// - /// The scalar. - /// The . - /// - /// The result of the operator. - /// - public static Matrix1x1 operator *(double scalar, Matrix1x1 matrix) - { - return matrix.Multiply(scalar); - } - - /// - /// Implements the operator * to multiply a with the specified scalar. - /// - /// The . - /// The scalar. - /// - /// The result of the operator. - /// - public static Matrix1x1 operator *(Matrix1x1 matrix, double scalar) - { - return matrix.Multiply(scalar); - } - - /// - /// Implements the operator *. - /// - /// The first . - /// The second . - /// - /// The result of the operator. - /// - public static Matrix1x1 operator *(Matrix1x1 firstMatrix, Matrix1x1 secondMatrix) - { - return MatrixUtils.Multiply(firstMatrix, secondMatrix); - } - - /// - /// Determines whether the specified , is equal to the instance. - /// - /// The to compare with the instance. - /// - /// true if the specified is equal to the instance; otherwise, false. - /// - public override bool Equals(object obj) - { - if (!(obj is Matrix1x1)) - return false; - return this == (Matrix1x1) obj; - } - - /// - /// Returns a hash code for the instance. - /// - /// - /// A hash code for the instance, suitable for use in hashing algorithms and data structures like a hash table. - /// - public override int GetHashCode() - { - unchecked - { - return 17 * 23 + this[0, 0].GetHashCode(); - } - } - - /// - /// Implements the operator ==. - /// - /// The left . - /// The right . - /// - /// The result of the operator. - /// - public static bool operator ==(Matrix1x1 left, Matrix1x1 right) - { - return FloatingNumber.AreApproximatelyEqual(left[0, 0], right[0, 0]); - } - - /// - /// Implements the operator !=. - /// - /// The left . - /// The right . - /// - /// The result of the operator. - /// - public static bool operator !=(Matrix1x1 left, Matrix1x1 right) - { - return !FloatingNumber.AreApproximatelyEqual(left[0, 0], right[0, 0]); - } - } -} \ No newline at end of file diff --git a/SharpMath/Geometry/Matrix1x3.cs b/SharpMath/Geometry/Matrix1x3.cs deleted file mode 100644 index cf025c2..0000000 --- a/SharpMath/Geometry/Matrix1x3.cs +++ /dev/null @@ -1,106 +0,0 @@ -// Matrix1x3.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -using System; -using System.Collections; -using System.Collections.Generic; - -// ReSharper disable InconsistentNaming - -namespace SharpMath.Geometry -{ - /// - /// Represents a 1x3 matrix for representing three-dimensional vectors horizontally. - /// - [Serializable] - public struct Matrix1x3 : IEnumerable, IMatrix - { - /// - /// Gets the value at row 1 and column 1. - /// - public double M11 { get; set; } - - /// - /// Gets the value at row 1 and column 2. - /// - public double M12 { get; set; } - - /// - /// Gets the value at row 1 and column 3. - /// - public double M13 { get; set; } - - public IEnumerator GetEnumerator() - { - return new MatrixEnumerator(this); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return new MatrixEnumerator(this); - } - - /// - /// Gets or sets the value at the specified index. The values are accessed as: (row +) column. - /// - /// The index. - /// The value at the specified index. - public double this[uint index] - { - get - { - switch (index) - { - case 0: - return M11; - case 1: - return M12; - case 2: - return M13; - default: - throw new IndexOutOfRangeException("The index must be between 0 and 2."); - } - } - set - { - switch (index) - { - case 0: - M11 = value; - break; - case 1: - M12 = value; - break; - case 2: - M13 = value; - break; - default: - throw new IndexOutOfRangeException("The index must be between 0 and 2."); - } - } - } - - /// - /// Gets or sets the value at the specified row and column. - /// - /// The row. - /// The column. - /// The value at the specified row and column. - public double this[uint row, uint column] - { - get => this[row + column]; - - set => this[row + column] = value; - } - - /// - /// Gets the column count of the . - /// - public uint ColumnCount => 3; - - /// - /// Gets the row count of the . - /// - public uint RowCount => 1; - } -} \ No newline at end of file diff --git a/SharpMath/Geometry/Matrix1x4.cs b/SharpMath/Geometry/Matrix1x4.cs deleted file mode 100644 index affa01c..0000000 --- a/SharpMath/Geometry/Matrix1x4.cs +++ /dev/null @@ -1,116 +0,0 @@ -// Matrix1x4.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -using System; -using System.Collections; -using System.Collections.Generic; - -// ReSharper disable InconsistentNaming - -namespace SharpMath.Geometry -{ - /// - /// Represents a 1x4 matrix for representing four-dimensional vectors horizontally. - /// - [Serializable] - public struct Matrix1x4 : IEnumerable, IMatrix - { - /// - /// Gets the value at row 1 and column 1. - /// - public double M11 { get; set; } - - /// - /// Gets the value at row 1 and column 2. - /// - public double M12 { get; set; } - - /// - /// Gets the value at row 1 and column 3. - /// - public double M13 { get; set; } - - /// - /// Gets the value at row 1 and column 4. - /// - public double M14 { get; set; } - - public IEnumerator GetEnumerator() - { - return new MatrixEnumerator(this); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return new MatrixEnumerator(this); - } - - /// - /// Gets or sets the value at the specified index. The values are accessed as: (row +) column. - /// - /// The index. - /// The value at the specified index. - public double this[uint index] - { - get - { - switch (index) - { - case 0: - return M11; - case 1: - return M12; - case 2: - return M13; - case 3: - return M14; - default: - throw new IndexOutOfRangeException("The index must be between 0 and 3."); - } - } - set - { - switch (index) - { - case 0: - M11 = value; - break; - case 1: - M12 = value; - break; - case 2: - M13 = value; - break; - case 3: - M14 = value; - break; - default: - throw new IndexOutOfRangeException("The index must be between 0 and 3."); - } - } - } - - /// - /// Gets or sets the value at the specified row and column. - /// - /// The row. - /// The column. - /// The value at the specified row and column. - public double this[uint row, uint column] - { - get => this[row + column]; - - set => this[row + column] = value; - } - - /// - /// Gets the column count of the . - /// - public uint ColumnCount => 4; - - /// - /// Gets the row count of the . - /// - public uint RowCount => 1; - } -} \ No newline at end of file diff --git a/SharpMath/Geometry/Matrix2x2.cs b/SharpMath/Geometry/Matrix2x2.cs deleted file mode 100644 index 658ca14..0000000 --- a/SharpMath/Geometry/Matrix2x2.cs +++ /dev/null @@ -1,351 +0,0 @@ -// Matrix2x2.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; - -// ReSharper disable InconsistentNaming - -namespace SharpMath.Geometry -{ - /// - /// Represents a 2x2 matrix. - /// - [Serializable] - public struct Matrix2x2 : IEnumerable, ISquareMatrix - { - /// - /// Initializes a struct. - /// - /// The value at row 1 and column 1. - /// The value at row 1 and column 2. - /// The value at row 2 and column 1. - /// The value at row 2 and column 2. - public Matrix2x2(double m11, double m12, double m21, double m22) - { - M11 = m11; - M12 = m12; - M21 = m21; - M22 = m22; - } - - /// - /// Gets the zero . - /// - public static Matrix2x2 Zero => new Matrix2x2(); - - /// - /// Gets the identity . - /// - public static Matrix2x2 Identity => MatrixUtils.GetIdentity(); - - /// - /// Gets or sets the value at row 1 and column 1. - /// - public double M11 { get; set; } - - /// - /// Gets or sets the value at row 1 and column 2. - /// - public double M12 { get; set; } - - /// - /// Gets or sets the value at row 2 and column 1. - /// - public double M21 { get; set; } - - /// - /// Gets or sets the value at row 2 and column 2. - /// - public double M22 { get; set; } - - /// - /// Gets a value indicating whether the is singular, or not. If true, this - /// doesn't have an inverse. - /// - public bool IsSingular => Determinant.IsApproximatelyEqualTo(0); - - /// - /// Gets the inverse of the . - /// - public Matrix2x2 Inverse => MatrixUtils.GaussJordan(this, Identity); - - /// - /// Gets the cofactor of the . - /// - public Matrix2x2 CofactorMatrix => this.BuildCofactorMatrix(); - - /// - /// Gets the adjugate of the . - /// - public Matrix2x2 Adjugate => CofactorMatrix.Transpose; - - /// - /// Gets the negated of the . - /// - public Matrix2x2 Negate => this.GetNegate(); - - /// - /// Gets the transpose of the . - /// - public Matrix2x2 Transpose => this.GetTranspose(); - - public IEnumerator GetEnumerator() - { - return new MatrixEnumerator(this); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return new MatrixEnumerator(this); - } - - /// - /// Gets or sets the value at the specified index. The values are accessed as: (row*2) + column. - /// - /// The index. - /// The value at the specified index. - public double this[uint index] - { - get - { - switch (index) - { - case 0: - return M11; - case 1: - return M12; - case 2: - return M21; - case 3: - return M22; - default: - throw new IndexOutOfRangeException("The index must be between 0 and 3."); - } - } - - set - { - switch (index) - { - case 0: - M11 = value; - break; - case 1: - M12 = value; - break; - case 2: - M21 = value; - break; - case 3: - M22 = value; - break; - default: - throw new IndexOutOfRangeException("The index must be between 0 and 3."); - } - } - } - - /// - /// Gets or sets the value at the specified row and column. - /// - /// The row. - /// The column. - /// The value at the specified row and column. - public double this[uint row, uint column] - { - get => this[row * 2 + column]; - set => this[row * 2 + column] = value; - } - - /// - /// Gets the dimension of the . - /// - public uint Dimension => 2; - - /// - /// Gets the row count of the . - /// - public uint RowCount => 2; - - /// - /// Gets the column count of the . - /// - public uint ColumnCount => 2; - - /// - /// Gets a value indicating whether the is orthogonal, or not. - /// - public bool IsOrthogonal => this * Transpose == Identity; - - /// - /// Gets a value indicating whether the is the identity , or not. - /// - public bool IsIdentity => M11.IsApproximatelyEqualTo(1) && M22.IsApproximatelyEqualTo(0); - - /// - /// Gets the determinant of the . - /// - public double Determinant => this.GetDeterminant(); - - /// - /// Gets the trace of the . - /// - public double Trace => M11 + M22; - - /// - /// Gets a value indicating whether the is symmetric, or not. - /// - public bool IsSymmetric => this == Transpose; - - /// - /// Gets a value indicating whether the is skew symmetric, or not. - /// - public bool IsSkewSymmetric => Negate == Transpose; - - /// - /// Gets a value indicating whether the is antisymmetric, or not. - /// - public bool IsAntiSymmetric => this == Transpose.Negate; - - /// - /// Gets a value indicating whether the is a diagonal matrix, or not. - /// - public bool IsDiagonal => this.GetIsDiagonal(); - - /// - /// Gets a value indicating whether the is a triangle matrix, or not. - /// - public bool IsTriangle => this.GetIsTriangle(); - - /// - /// Implements the operator +. - /// - /// The first . - /// The second . - /// - /// The result of the operator. - /// - public static Matrix2x2 operator +(Matrix2x2 firstMatrix, Matrix2x2 secondMatrix) - { - return MatrixUtils.Add(firstMatrix, secondMatrix); - } - - /// - /// Implements the operator -. - /// - /// The first . - /// The second . - /// - /// The result of the operator. - /// - public static Matrix2x2 operator -(Matrix2x2 firstMatrix, Matrix2x2 secondMatrix) - { - return MatrixUtils.Subtract(firstMatrix, secondMatrix); - } - - /// - /// Implements the operator * to multiply a with the specified scalar. - /// - /// The scalar. - /// The . - /// - /// The result of the operator. - /// - public static Matrix2x2 operator *(double scalar, Matrix2x2 matrix) - { - return matrix.Multiply(scalar); - } - - /// - /// Implements the operator * to multiply a with the specified scalar. - /// - /// The . - /// The scalar. - /// - /// The result of the operator. - /// - public static Matrix2x2 operator *(Matrix2x2 matrix, double scalar) - { - return matrix.Multiply(scalar); - } - - /// - /// Implements the operator *. - /// - /// The first . - /// The second . - /// - /// The result of the operator. - /// - public static Matrix2x2 operator *(Matrix2x2 firstMatrix, Matrix2x2 secondMatrix) - { - return MatrixUtils.Multiply(firstMatrix, secondMatrix); - } - - /// - /// Determines whether the specified , is equal to the instance. - /// - /// The to compare with the instance. - /// - /// true if the specified is equal to the instance; otherwise, false. - /// - public override bool Equals(object obj) - { - if (!(obj is Matrix2x2)) - return false; - return this == (Matrix2x2) obj; - } - - /// - /// Returns a hash code for the instance. - /// - /// - /// A hash code for the instance, suitable for use in hashing algorithms and data structures like a hash table. - /// - public override int GetHashCode() - { - unchecked - { - var hash = 17; - for (uint y = 0; y < RowCount; ++y) - for (uint x = 0; x < ColumnCount; ++x) - hash = hash * 23 + this[y, x].GetHashCode(); - return hash; - } - } - - public bool Equals(Matrix2x2 other) - { - return this == other; - } - - /// - /// Implements the operator ==. - /// - /// The left . - /// The right . - /// - /// The result of the operator. - /// - public static bool operator ==(Matrix2x2 left, Matrix2x2 right) - { - return left.SequenceEqual(right); - } - - /// - /// Implements the operator !=. - /// - /// The left . - /// The right . - /// - /// The result of the operator. - /// - public static bool operator !=(Matrix2x2 left, Matrix2x2 right) - { - return !left.SequenceEqual(right); - } - } -} \ No newline at end of file diff --git a/SharpMath/Geometry/Matrix3x1.cs b/SharpMath/Geometry/Matrix3x1.cs deleted file mode 100644 index 16248c4..0000000 --- a/SharpMath/Geometry/Matrix3x1.cs +++ /dev/null @@ -1,106 +0,0 @@ -// Matrix3x1.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -using System; -using System.Collections; -using System.Collections.Generic; - -// ReSharper disable InconsistentNaming - -namespace SharpMath.Geometry -{ - /// - /// Represents a 3x1 matrix for representing three-dimensional vectors vertically. - /// - [Serializable] - public struct Matrix3x1 : IEnumerable, IMatrix - { - /// - /// Gets the value at row 1 and column 1. - /// - public double M11 { get; set; } - - /// - /// Gets the value at row 2 and column 1. - /// - public double M21 { get; set; } - - /// - /// Gets the value at row 3 and column 1. - /// - public double M31 { get; set; } - - public IEnumerator GetEnumerator() - { - return new MatrixEnumerator(this); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return new MatrixEnumerator(this); - } - - /// - /// Gets or sets the value at the specified index. The values are accessed as: row (+ column). - /// - /// The index. - /// The value at the specified index. - public double this[uint index] - { - get - { - switch (index) - { - case 0: - return M11; - case 1: - return M21; - case 2: - return M31; - default: - throw new IndexOutOfRangeException("The index must be between 0 and 2."); - } - } - set - { - switch (index) - { - case 0: - M11 = value; - break; - case 1: - M21 = value; - break; - case 2: - M31 = value; - break; - default: - throw new IndexOutOfRangeException("The index must be between 0 and 2."); - } - } - } - - /// - /// Gets or sets the value at the specified row and column. - /// - /// The row. - /// The column. - /// The value at the specified row and column. - public double this[uint row, uint column] - { - get => this[row + column]; - - set => this[row + column] = value; - } - - /// - /// Gets the column count of the . - /// - public uint ColumnCount => 1; - - /// - /// Gets the row count of the . - /// - public uint RowCount => 3; - } -} \ No newline at end of file diff --git a/SharpMath/Geometry/Matrix3x3.cs b/SharpMath/Geometry/Matrix3x3.cs deleted file mode 100644 index 0feeec6..0000000 --- a/SharpMath/Geometry/Matrix3x3.cs +++ /dev/null @@ -1,547 +0,0 @@ -// Matrix3x3.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; - -// ReSharper disable InconsistentNaming - -namespace SharpMath.Geometry -{ - /// - /// Represents a 3x3 matrix and provides functions to transform 2-dimensional vectors. - /// - // ReSharper disable once InconsistentNaming - [Serializable] - public struct Matrix3x3 : IEnumerable, IEquatable, ISquareMatrix - { - /// - /// Initializes a struct. - /// - /// The value at row 1 and column 1. - /// The value at row 1 and column 2. - /// The value at row 1 and column 3. - /// The value at row 2 and column 1. - /// The value at row 2 and column 2 - /// The value at row 2 and column 3. - /// The value at row 3 and column 1. - /// The value at row 3 and column 2. - /// The value at row 3 and column 3. - public Matrix3x3(double m11, double m12, double m13, double m21, double m22, double m23, double m31, double m32, - double m33) - { - M11 = m11; - M12 = m12; - M13 = m13; - M21 = m21; - M22 = m22; - M23 = m23; - M31 = m31; - M32 = m32; - M33 = m33; - } - - /// - /// Initializes a struct. - /// - /// The first row . - /// The second row . - /// The third row . - public Matrix3x3(Vector3 row1, Vector3 row2, Vector3 row3) - : this(row1.X, row2.X, row3.X, row1.Y, row2.Y, row3.Y, row1.Z, row2.Z, row3.Z) - { - } - - /// - /// Gets the zero . - /// - public static Matrix3x3 Zero => new Matrix3x3(); - - /// - /// Gets the identity . - /// - public static Matrix3x3 Identity => MatrixUtils.GetIdentity(); - - /// - /// Gets or sets the value at row 1 and column 1. - /// - public double M11 { get; set; } - - /// - /// Gets or sets the value at row 1 and column 2. - /// - public double M12 { get; set; } - - /// - /// Gets or sets the value at row 1 and column 3. - /// - public double M13 { get; set; } - - /// - /// Gets or sets the value at row 2 and column 1. - /// - public double M21 { get; set; } - - /// - /// Gets or sets the value at row 2 and column 2. - /// - public double M22 { get; set; } - - /// - /// Gets or sets the value at row 2 and column 3. - /// - public double M23 { get; set; } - - /// - /// Gets or sets the value at row 3 and column 1. - /// - public double M31 { get; set; } - - /// - /// Gets or sets the value at row 3 and column 2. - /// - public double M32 { get; set; } - - /// - /// Gets or sets the value at row 3 and column 3. - /// - public double M33 { get; set; } - - /// - /// Gets a value indicating whether the is singular, or not. If true, this - /// doesn't have an inverse. - /// - public bool IsSingular => Determinant.IsApproximatelyEqualTo(0); - - /// - /// Gets the inverse of the . - /// - public Matrix3x3 Inverse => MatrixUtils.GaussJordan(this, Identity); - - /// - /// Gets the cofactor of the . - /// - public Matrix3x3 CofactorMatrix => this.BuildCofactorMatrix(); - - /// - /// Gets the adjugate of the . - /// - public Matrix3x3 Adjugate => CofactorMatrix.GetTranspose(); - - /// - /// Gets the negated of the . - /// - public Matrix3x3 Negate => this.GetNegate(); - - /// - /// Gets the transpose of the . - /// - public Matrix3x3 Transpose => this.GetTranspose(); - - public IEnumerator GetEnumerator() - { - return new MatrixEnumerator(this); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return new MatrixEnumerator(this); - } - - public bool Equals(Matrix3x3 other) - { - return this == other; - } - - /// - /// Gets or sets the value at the specified index. The values are accessed as: (row*3) + column. - /// - /// The index. - /// The value at the specified index. - public double this[uint index] - { - get - { - switch (index) - { - case 0: - return M11; - case 1: - return M12; - case 2: - return M13; - case 3: - return M21; - case 4: - return M22; - case 5: - return M23; - case 6: - return M31; - case 7: - return M32; - case 8: - return M33; - default: - throw new IndexOutOfRangeException("The index must be between 0 and 8."); - } - } - set - { - switch (index) - { - case 0: - M11 = value; - break; - case 1: - M12 = value; - break; - case 2: - M13 = value; - break; - case 3: - M21 = value; - break; - case 4: - M22 = value; - break; - case 5: - M23 = value; - break; - case 6: - M31 = value; - break; - case 7: - M32 = value; - break; - case 8: - M33 = value; - break; - default: - throw new IndexOutOfRangeException("The index must be between 0 and 8."); - } - } - } - - /// - /// Gets or sets the value at the specified row and column. - /// - /// The row. - /// The column. - /// The value at the specified row and column. - public double this[uint row, uint column] - { - get => this[row * 3 + column]; - set => this[row * 3 + column] = value; - } - - /// - /// Gets the dimension of the . - /// - public uint Dimension => 3; - - /// - /// Gets the row count of the . - /// - public uint RowCount => 3; - - /// - /// Gets the column count of the . - /// - public uint ColumnCount => 3; - - /// - /// Gets a value indicating whether the is orthogonal, or not. - /// - public bool IsOrthogonal => this * Transpose == Identity; - - /// - /// Gets a value indicating whether the is the identity , or not. - /// - public bool IsIdentity - => M11.IsApproximatelyEqualTo(1) && M22.IsApproximatelyEqualTo(1) && M33.IsApproximatelyEqualTo(1); - - /// - /// Gets the determinant of the . - /// - public double Determinant => this.GetDeterminant(); - - /// - /// Gets the trace of the . - /// - public double Trace => M11 + M22 + M33; - - /// - /// Gets a value indicating whether the is symmetric, or not. - /// - public bool IsSymmetric => this == Transpose; - - /// - /// Gets a value indicating whether the is skew symmetric, or not. - /// - public bool IsSkewSymmetric => Negate == Transpose; - - /// - /// Gets a value indicating whether the is antisymmetric, or not. - /// - public bool IsAntiSymmetric => this == Transpose.Negate; - - /// - /// Gets a value indicating whether the is a diagonal matrix, or not. - /// - public bool IsDiagonal => this.GetIsDiagonal(); - - /// - /// Gets a value indicating whether the is a triangle matrix, or not. - /// - public bool IsTriangle => this.GetIsTriangle(); - - /// - /// Creates a from a object. - /// - /// The to convert. - /// The that has been created. - public static Matrix3x3 FromMatrix(IMatrix matrix) - { - if (matrix.RowCount != 3 || matrix.ColumnCount != 3) - throw new ArgumentException("The square matrix cannot be converted into a Matrix3x3"); - - var resultMatrix = new Matrix3x3(); - for (uint y = 0; y < resultMatrix.Dimension; ++y) - for (uint x = 0; x < resultMatrix.Dimension; ++x) - resultMatrix[y, x] = matrix[y, x]; - return resultMatrix; - } - - /// - /// Creates a that represents a rotation. - /// - /// The angle to rotate around in radians. - /// A that represents a rotation using the specified angle. - public static Matrix3x3 Rotation(double angle) - { - // the is actually the same as Matrix4x4.RotationZ as we are rotating around the Z-axis that is 0 in 2D-space. - - var sin = Math.Sin(angle); - var cos = Math.Cos(angle); - - var matrix = Identity; - matrix[0, 0] = cos; - matrix[1, 0] = sin; - matrix[0, 1] = -sin; - matrix[1, 1] = cos; - - return matrix; - } - - /// - /// Creates a that represents a scalation. - /// - /// The scalation factor of the X-component. - /// The scalation factor of the Y-component. - /// A that represents a scalation using the specified factors. - public static Matrix3x3 Scalation(double scaleX, double scaleY) - { - var matrix = Identity; - matrix[0, 0] = scaleX; - matrix[1, 1] = scaleY; - - return matrix; - } - - /// - /// Creates a that represents a translation. - /// - /// The translation of the X-component. - /// The translation of the Y-component. - /// A that represents a translation using the specified addends. - public static Matrix3x3 Translation(double x, double y) - { - var matrix = Identity; - matrix[0, 2] = x; - matrix[1, 2] = y; - - return matrix; - } - - /// - /// Implements the operator +. - /// - /// The first . - /// The second . - /// - /// The result of the operator. - /// - public static Matrix3x3 operator +(Matrix3x3 firstMatrix, Matrix3x3 secondMatrix) - { - return MatrixUtils.Add(firstMatrix, secondMatrix); - } - - /// - /// Implements the operator -. - /// - /// The first . - /// The second . - /// - /// The result of the operator. - /// - public static Matrix3x3 operator -(Matrix3x3 firstMatrix, Matrix3x3 secondMatrix) - { - return MatrixUtils.Subtract(firstMatrix, secondMatrix); - } - - /// - /// Implements the operator * to multiply a with the specified scalar. - /// - /// The scalar. - /// The . - /// - /// The result of the operator. - /// - public static Matrix3x3 operator *(double scalar, Matrix3x3 matrix) - { - return matrix.Multiply(scalar); - } - - /// - /// Implements the operator * to multiply a with the specified scalar. - /// - /// The . - /// The scalar. - /// - /// The result of the operator. - /// - public static Matrix3x3 operator *(Matrix3x3 matrix, double scalar) - { - return matrix.Multiply(scalar); - } - - /// - /// Implements the operator * to transform a with a . - /// - /// The . - /// The . - /// - /// The result of the operator. - /// - public static Vector2 operator *(Matrix3x3 matrix, Vector2 vector) - { - var resultMatrix = MatrixUtils.Multiply(matrix, - new Vector3(vector.X, vector.Y, 1).AsVerticalMatrix()); - return new Vector2(resultMatrix[0], resultMatrix[1]); - } - - /// - /// Implements the operator * to transform a with a . - /// - /// The . - /// The . - /// - /// The result of the operator. - /// - public static Vector2 operator *(Vector2 vector, Matrix3x3 matrix) - { - return matrix * vector; - } - - /// - /// Implements the operator * to transform a with a . - /// - /// The . - /// The . - /// - /// The result of the operator. - /// - public static Vector3 operator *(Matrix3x3 matrix, Vector3 vector) - { - var resultMatrix = MatrixUtils.Multiply(matrix, vector.AsVerticalMatrix()); - return new Vector3(resultMatrix[0], resultMatrix[1], resultMatrix[2]); - } - - /// - /// Implements the operator * to transform a with a . - /// - /// The . - /// The . - /// - /// The result of the operator. - /// - public static Vector3 operator *(Vector3 vector, Matrix3x3 matrix) - { - return matrix * vector; - } - - /// - /// Implements the operator *. - /// - /// The first . - /// The second . - /// - /// The result of the operator. - /// - public static Matrix3x3 operator *(Matrix3x3 firstMatrix, Matrix3x3 secondMatrix) - { - return MatrixUtils.Multiply(firstMatrix, secondMatrix); - } - - /// - /// Determines whether the specified , is equal to the instance. - /// - /// The to compare with the instance. - /// - /// true if the specified is equal to the instance; otherwise, false. - /// - public override bool Equals(object obj) - { - if (!(obj is Matrix3x3)) - return false; - return this == (Matrix3x3) obj; - } - - /// - /// Returns a hash code for the instance. - /// - /// - /// A hash code for the instance, suitable for use in hashing algorithms and data structures like a hash table. - /// - public override int GetHashCode() - { - unchecked - { - var hash = 17; - for (uint y = 0; y < RowCount; ++y) - for (uint x = 0; x < ColumnCount; ++x) - hash = hash * 23 + this[y, x].GetHashCode(); - return hash; - } - } - - /// - /// Implements the operator ==. - /// - /// The left . - /// The right . - /// - /// The result of the operator. - /// - public static bool operator ==(Matrix3x3 left, Matrix3x3 right) - { - return left.SequenceEqual(right); - } - - /// - /// Implements the operator !=. - /// - /// The left . - /// The right . - /// - /// The result of the operator. - /// - public static bool operator !=(Matrix3x3 left, Matrix3x3 right) - { - return !left.SequenceEqual(right); - } - } -} \ No newline at end of file diff --git a/SharpMath/Geometry/Matrix4x1.cs b/SharpMath/Geometry/Matrix4x1.cs deleted file mode 100644 index ad40448..0000000 --- a/SharpMath/Geometry/Matrix4x1.cs +++ /dev/null @@ -1,116 +0,0 @@ -// Matrix4x1.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -using System; -using System.Collections; -using System.Collections.Generic; - -// ReSharper disable InconsistentNaming - -namespace SharpMath.Geometry -{ - /// - /// Represents a 4x1 matrix for representing four-dimensional vectors vertically. - /// - [Serializable] - public struct Matrix4x1 : IEnumerable, IMatrix - { - /// - /// Gets the value at row 1 and column 1. - /// - public double M11 { get; set; } - - /// - /// Gets the value at row 2 and column 1. - /// - public double M21 { get; set; } - - /// - /// Gets the value at row 3 and column 1. - /// - public double M31 { get; set; } - - /// - /// Gets the value at row 4 and column 1. - /// - public double M41 { get; set; } - - public IEnumerator GetEnumerator() - { - return new MatrixEnumerator(this); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return new MatrixEnumerator(this); - } - - /// - /// Gets or sets the value at the specified index. The values are accessed as: row (+ column). - /// - /// The index. - /// The value at the specified index. - public double this[uint index] - { - get - { - switch (index) - { - case 0: - return M11; - case 1: - return M21; - case 2: - return M31; - case 3: - return M41; - default: - throw new IndexOutOfRangeException("The index must be between 0 and 3."); - } - } - set - { - switch (index) - { - case 0: - M11 = value; - break; - case 1: - M21 = value; - break; - case 2: - M31 = value; - break; - case 3: - M41 = value; - break; - default: - throw new IndexOutOfRangeException("The index must be between 0 and 3."); - } - } - } - - /// - /// Gets or sets the value at the specified row and column. - /// - /// The row. - /// The column. - /// The value at the specified row and column. - public double this[uint row, uint column] - { - get => this[row + column]; - - set => this[row + column] = value; - } - - /// - /// Gets the column count of the . - /// - public uint ColumnCount => 1; - - /// - /// Gets the row count of the . - /// - public uint RowCount => 4; - } -} \ No newline at end of file diff --git a/SharpMath/Geometry/Matrix4x4.cs b/SharpMath/Geometry/Matrix4x4.cs deleted file mode 100644 index fd0ed17..0000000 --- a/SharpMath/Geometry/Matrix4x4.cs +++ /dev/null @@ -1,803 +0,0 @@ -// Matrix4x4.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; - -// ReSharper disable InconsistentNaming - -namespace SharpMath.Geometry -{ - /// - /// Represents a 4x4 matrix and provides functions to transform 3-dimensional vectors. - /// - // ReSharper disable once InconsistentNaming - [Serializable] - public struct Matrix4x4 : IEnumerable, IEquatable, ISquareMatrix - { - /// - /// Initializes a struct. - /// - /// The value at row 1 and column 1. - /// The value at row 1 and column 2. - /// The value at row 1 and column 3. - /// The value at row 1 and column 4. - /// The value at row 2 and column 1. - /// The value at row 2 and column 2 - /// The value at row 2 and column 3. - /// The value at row 2 and column 4. - /// The value at row 3 and column 1. - /// The value at row 3 and column 2. - /// The value at row 3 and column 3. - /// The value at row 3 and column 4. - /// The value at row 4 and column 1. - /// The value at row 4 and column 2. - /// The value at row 4 and column 3. - /// The value at row 4 and column 4. - public Matrix4x4(double m11, double m12, double m13, double m14, double m21, double m22, double m23, double m24, - double m31, double m32, double m33, double m34, double m41, double m42, double m43, double m44) - { - M11 = m11; - M12 = m12; - M13 = m13; - M14 = m14; - M21 = m21; - M22 = m22; - M23 = m23; - M24 = m24; - M31 = m31; - M32 = m32; - M33 = m33; - M34 = m34; - M41 = m41; - M42 = m42; - M43 = m43; - M44 = m44; - } - - /// - /// Initializes a struct. - /// - /// The first row . - /// The second row . - /// The third row . - /// The fourth row . - public Matrix4x4(Vector4 row1, Vector4 row2, Vector4 row3, Vector4 row4) - : this( - row1.X, row2.X, row3.X, row4.X, row1.Y, row2.Y, row3.Y, row4.Y, row1.Z, row2.Z, row3.Z, row4.Z, row1.W, - row2.W, row3.W, row4.W) - { - } - - /// - /// Gets the zero . - /// - public static Matrix4x4 Zero => new Matrix4x4(); - - /// - /// Gets or sets the identity . - /// - public static Matrix4x4 Identity => MatrixUtils.GetIdentity(); - - /// - /// Gets or sets the value at row 1 and column 1. - /// - public double M11 { get; set; } - - /// - /// Gets or sets the value at row 1 and column 2. - /// - public double M12 { get; set; } - - /// - /// Gets or sets the value at row 1 and column 3. - /// - public double M13 { get; set; } - - /// - /// Gets or sets the value at row 1 and column 4. - /// - public double M14 { get; set; } - - /// - /// Gets or sets the value at row 2 and column 1. - /// - public double M21 { get; set; } - - /// - /// Gets or sets the value at row 2 and column 2. - /// - public double M22 { get; set; } - - /// - /// Gets or sets the value at row 2 and column 3. - /// - public double M23 { get; set; } - - /// - /// Gets or sets the value at row 2 and column 4. - /// - public double M24 { get; set; } - - /// - /// Gets or sets the value at row 3 and column 1. - /// - public double M31 { get; set; } - - /// - /// Gets or sets the value at row 3 and column 2. - /// - public double M32 { get; set; } - - /// - /// Gets or sets the value at row 3 and column 3. - /// - public double M33 { get; set; } - - /// - /// Gets or sets the value at row 3 and column 4. - /// - public double M34 { get; set; } - - /// - /// Gets or sets the value at row 4 and column 1. - /// - public double M41 { get; set; } - - /// - /// Gets or sets the value at row 4 and column 2. - /// - public double M42 { get; set; } - - /// - /// Gets or sets the value at row 4 and column 3. - /// - public double M43 { get; set; } - - /// - /// Gets or sets the value at row 4 and column 4. - /// - public double M44 { get; set; } - - /// - /// Gets a value indicating whether the is singular, or not. If true, this - /// doesn't have an inverse. - /// - public bool IsSingular => Determinant.IsApproximatelyEqualTo(0); - - /// - /// Gets the inverse of the . - /// - public Matrix4x4 Inverse => MatrixUtils.GaussJordan(this, Identity); - - /// - /// Gets the cofactor of the . - /// - public Matrix4x4 CofactorMatrix => this.BuildCofactorMatrix(); - - /// - /// Gets the adjugate of the . - /// - public Matrix4x4 Adjugate => CofactorMatrix.GetTranspose(); - - /// - /// Gets the negated of the . - /// - public Matrix4x4 Negate => this.GetNegate(); - - /// - /// Gets the transpose of the . - /// - public Matrix4x4 Transpose => this.GetTranspose(); - - public IEnumerator GetEnumerator() - { - return new MatrixEnumerator(this); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return new MatrixEnumerator(this); - } - - public bool Equals(Matrix4x4 other) - { - return this == other; - } - - /// - /// Gets or sets the value at the specified index. The values are accessed as: (row*4) + column. - /// - /// The index. - /// The value at the specified index. - public double this[uint index] - { - get - { - switch (index) - { - case 0: - return M11; - case 1: - return M12; - case 2: - return M13; - case 3: - return M14; - case 4: - return M21; - case 5: - return M22; - case 6: - return M23; - case 7: - return M24; - case 8: - return M31; - case 9: - return M32; - case 10: - return M33; - case 11: - return M34; - case 12: - return M41; - case 13: - return M42; - case 14: - return M43; - case 15: - return M44; - default: - throw new IndexOutOfRangeException("The index must be between 0 and 15."); - } - } - set - { - switch (index) - { - case 0: - M11 = value; - break; - case 1: - M12 = value; - break; - case 2: - M13 = value; - break; - case 3: - M14 = value; - break; - case 4: - M21 = value; - break; - case 5: - M22 = value; - break; - case 6: - M23 = value; - break; - case 7: - M24 = value; - break; - case 8: - M31 = value; - break; - case 9: - M32 = value; - break; - case 10: - M33 = value; - break; - case 11: - M34 = value; - break; - case 12: - M41 = value; - break; - case 13: - M42 = value; - break; - case 14: - M43 = value; - break; - case 15: - M44 = value; - break; - default: - throw new IndexOutOfRangeException("The index must be between 0 and 15."); - } - } - } - - /// - /// Gets or sets the value at the specified row and column. - /// - /// The row. - /// The column. - /// The value at the specified row and column. - public double this[uint row, uint column] - { - get => this[row * 4 + column]; - set => this[row * 4 + column] = value; - } - - /// - /// Gets the dimension of the . - /// - public uint Dimension => 4; - - /// - /// Gets the row count of the . - /// - public uint RowCount => 4; - - /// - /// Gets the column count of the . - /// - public uint ColumnCount => 4; - - /// - /// Gets a value indicating whether the is orthogonal, or not. - /// - public bool IsOrthogonal => this * Transpose == Identity; - - /// - /// Gets a value indicating whether the is the identity , or not. - /// - public bool IsIdentity - => M11.IsApproximatelyEqualTo(1) && M22.IsApproximatelyEqualTo(1) && M33.IsApproximatelyEqualTo(1); - - /// - /// Gets the determinant of the . - /// - public double Determinant => this.GetDeterminant(); - - /// - /// Gets the trace of the . - /// - public double Trace => M11 + M22 + M33 + M44; - - /// - /// Gets a value indicating whether the is symmetric, or not. - /// - public bool IsSymmetric => this == Transpose; - - /// - /// Gets a value indicating whether the is skew symmetric, or not. - /// - public bool IsSkewSymmetric => Negate == Transpose; - - /// - /// Gets a value indicating whether the is antisymmetric, or not. - /// - public bool IsAntiSymmetric => this == Transpose.Negate; - - /// - /// Gets a value indicating whether the is a diagonal matrix, or not. - /// - public bool IsDiagonal => this.GetIsDiagonal(); - - /// - /// Gets a value indicating whether the is a triangle matrix, or not. - /// - public bool IsTriangle => this.GetIsTriangle(); - - /// - /// Creates a from an abstract object. - /// - /// The to convert. - /// The that has been created. - public static Matrix4x4 FromMatrix(IMatrix matrix) - { - if (matrix.RowCount != 4 || matrix.ColumnCount != 4) - throw new ArgumentException("The square matrix cannot be converted into a Matrix4x4"); - - var resultMatrix = new Matrix4x4(); - for (uint y = 0; y < resultMatrix.Dimension; ++y) - for (uint x = 0; x < resultMatrix.Dimension; ++x) - resultMatrix[y, x] = matrix[y, x]; - return resultMatrix; - } - - /// - /// Creates a that represents a scalation. - /// - /// The scalation factor of the X-component. - /// The scalation factor of the Y-component. - /// The scalation factor of the Z-component. - /// A that represents a scalation using the specified factors. - public static Matrix4x4 Scalation(double scaleX, double scaleY, double scaleZ) - { - var matrix = Identity; - matrix[0, 0] = scaleX; - matrix[1, 1] = scaleY; - matrix[2, 2] = scaleZ; - - return matrix; - } - - /// - /// Creates a that represents a scalation. - /// - /// The scalation factor of all components. - /// A that represents a scalation using the specified factor. - public static Matrix4x4 Scalation(double scale) - { - return Scalation(scale, scale, scale); - } - - /// - /// Creates a by multiplying the built instances that represent - /// rotations around the relating angles. - /// - /// The angle to rotate around the X-axis in radians. - /// The angle to rotate around the Y-axis in radians. - /// The angle to rotate around the Z-axis in radians. - /// - public static Matrix4x4 Rotation(double angleX, double angleY, double angleZ) - { - return RotationX(angleX) * RotationY(angleY) * RotationZ(angleZ); - } - - /// - /// Creates a that represents a rotation around the X-axis. - /// - /// The angle to rotate around in radians. - /// A that represents a rotation around the X-axis using the specified angle. - // https://ksgamedev.files.wordpress.com/2010/01/matrix-rotation.png - public static Matrix4x4 RotationX(double angle) - { - var sin = Math.Sin(angle); - var cos = Math.Cos(angle); - - var matrix = Identity; - matrix[1, 1] = cos; - matrix[1, 2] = -sin; - matrix[2, 1] = sin; - matrix[2, 2] = cos; - - return matrix; - } - - /// - /// Creates a that represents a rotation around the Y-axis. - /// - /// The angle to rotate around in radians. - /// A that represents a rotation around the Y-axis using the specified angle. - public static Matrix4x4 RotationY(double angle) - { - var sin = Math.Sin(angle); - var cos = Math.Cos(angle); - - var matrix = Identity; - matrix[0, 0] = cos; - matrix[0, 2] = sin; - matrix[2, 0] = -sin; - matrix[2, 2] = cos; - - return matrix; - } - - /// - /// Creates a that represents a rotation around the Z-axis. - /// - /// The angle to rotate around in radians. - /// A that represents a rotation around the Z-axis using the specified angle. - public static Matrix4x4 RotationZ(double angle) - { - var sin = Math.Sin(angle); - var cos = Math.Cos(angle); - - var matrix = Identity; - matrix[0, 0] = cos; - matrix[0, 1] = -sin; - matrix[1, 0] = sin; - matrix[1, 1] = cos; - - return matrix; - } - - /// - /// Creates a that represents a translation. - /// - /// The translation of the X-component. - /// The translation of the Y-component. - /// The translation of the Z-component. - /// A that represents a translation using the specified addends. - public static Matrix4x4 Translation(double x, double y, double z) - { - var matrix = Identity; - matrix[0, 3] = x; - matrix[1, 3] = y; - matrix[2, 3] = z; - - return matrix; - } - - /// - /// Creates a perspective projection based on a field of view. - /// - /// The to use for the calculations. - /// The created perspective projection using the specified . - public static Matrix4x4 PerspectiveFieldOfView(ProjectionData projectionData) - { - // https://msdn.microsoft.com/en-us/library/bb205351%28v=vs.85%29.aspx - /* - xScale 0 0 0 - 0 yScale 0 0 - 0 0 zf/(zn-zf) -1 - 0 0 zn*zf/(zn-zf) 0 - - yScale = cot(fovY/2) - xScale = yScale / aspect ratio - */ - - var yScale = 1.0f / (float) Math.Tan(projectionData.FieldOfView / 2f); - var xScale = yScale / projectionData.AspectRatio; - - var invertedDepth = -projectionData.Depth; - var matrix = Identity; - matrix[0, 0] = xScale; - matrix[1, 1] = yScale; - matrix[2, 2] = projectionData.FarPlane / invertedDepth; - matrix[2, 3] = -1.0f; - matrix[3, 2] = projectionData.NearPlane * projectionData.FarPlane / invertedDepth; - - return matrix; - } - - /// - /// Creates a view using the specified parameters. - /// - /// The camera position. - /// The target position of the camera. - /// The up vector. - /// The created view . - public static Matrix4x4 View(Vector3 cameraPosition, Vector3 cameraTarget, Vector3 upVector) - { - var vector = Vector3.FromVector((cameraPosition - cameraTarget).Normalize()); - var vector2 = Vector3.FromVector(Vector3.VectorProduct(upVector, vector)); - var vector3 = Vector3.FromVector(Vector3.VectorProduct(vector, vector2)); - - var matrix = Identity; - matrix[0, 0] = vector2.X; - matrix[0, 1] = vector3.X; - matrix[0, 2] = vector.X; - - matrix[1, 0] = vector2.Y; - matrix[1, 1] = vector3.Y; - matrix[1, 2] = vector.Y; - - matrix[2, 0] = vector2.Z; - matrix[2, 1] = vector3.Z; - matrix[2, 2] = vector.Z; - - matrix[3, 0] = -vector2.DotProduct(cameraPosition); - matrix[3, 1] = -vector3.DotProduct(cameraPosition); - matrix[3, 2] = -vector.DotProduct(cameraPosition); - matrix[3, 3] = 1f; - - return matrix; - } - - /// - /// Creates a world by using the specified components. - /// - /// The position. - /// The target vector. - /// The up vector. - /// The created world . - public static Matrix4x4 World(Vector3 position, Vector3 forward, Vector3 up) - { - var xVector = Vector3.VectorProduct(forward, up); - var yVector = Vector3.VectorProduct(xVector, forward); - var zVector = Vector3.FromVector(forward.Normalize()); - xVector.Normalize(); - yVector.Normalize(); - - var result = new Matrix4x4 - { - [0, 0] = xVector.X, - [0, 1] = xVector.Y, - [0, 2] = xVector.Z, - [1, 0] = yVector.X, - [1, 1] = yVector.Y, - [1, 2] = yVector.Z, - [2, 0] = -zVector.X, - [2, 1] = -zVector.Y, - [2, 2] = -zVector.Z, - [3, 0] = position.X, - [3, 1] = position.Y, - [3, 2] = position.Z, - [3, 3] = 1f - }; - - return result; - } - - /// - /// Implements the operator +. - /// - /// The first . - /// The second . - /// - /// The result of the operator. - /// - public static Matrix4x4 operator +(Matrix4x4 firstMatrix, Matrix4x4 secondMatrix) - { - return MatrixUtils.Add(firstMatrix, secondMatrix); - } - - /// - /// Implements the operator -. - /// - /// The first . - /// The second . - /// - /// The result of the operator. - /// - public static Matrix4x4 operator -(Matrix4x4 firstMatrix, Matrix4x4 secondMatrix) - { - return MatrixUtils.Subtract(firstMatrix, secondMatrix); - } - - /// - /// Implements the operator * to multiply a with the specified scalar. - /// - /// The scalar. - /// The . - /// - /// The result of the operator. - /// - public static Matrix4x4 operator *(double scalar, Matrix4x4 matrix) - { - return matrix.Multiply(scalar); - } - - /// - /// Implements the operator * to multiply a with the specified scalar. - /// - /// The . - /// The scalar. - /// - /// The result of the operator. - /// - public static Matrix4x4 operator *(Matrix4x4 matrix, double scalar) - { - return matrix.Multiply(scalar); - } - - /// - /// Implements the operator *. - /// - /// The first . - /// The second . - /// - /// The result of the operator. - /// - public static Matrix4x4 operator *(Matrix4x4 firstMatrix, Matrix4x4 secondMatrix) - { - return MatrixUtils.Multiply(firstMatrix, secondMatrix); - } - - /// - /// Implements the operator * to transform a with a . - /// - /// The . - /// The . - /// - /// The result of the operator. - /// - public static Vector3 operator *(Matrix4x4 matrix, Vector3 vector) - { - var resultMatrix = MatrixUtils.Multiply(matrix, - new Vector4(vector.X, vector.Y, vector.Z, 1).AsVerticalMatrix()); - var resultVector = new Vector4(resultMatrix[0], resultMatrix[1], resultMatrix[2], resultMatrix[3]); - resultVector.X /= resultVector.W; - resultVector.Y /= resultVector.W; - resultVector.Z /= resultVector.W; - return resultVector.Convert(); - } - - /// - /// Implements the operator * to transform a with a . - /// - /// The . - /// The . - /// - /// The result of the operator. - /// - public static Vector3 operator *(Vector3 vector, Matrix4x4 matrix) - { - return matrix * vector; - } - - /// - /// Implements the operator * to transform a with a . - /// - /// The . - /// The . - /// - /// The result of the operator. - /// - public static Vector4 operator *(Matrix4x4 matrix, Vector4 vector) - { - var resultMatrix = MatrixUtils.Multiply(matrix, vector.AsVerticalMatrix()); - return new Vector4(resultMatrix[0], resultMatrix[1], resultMatrix[2], resultMatrix[3]); - } - - /// - /// Implements the operator * to transform a with a . - /// - /// The . - /// The . - /// - /// The result of the operator. - /// - public static Vector4 operator *(Vector4 vector, Matrix4x4 matrix) - { - return matrix * vector; - } - - /// - /// Determines whether the specified , is equal to the instance. - /// - /// The to compare with the instance. - /// - /// true if the specified is equal to the instance; otherwise, false. - /// - public override bool Equals(object obj) - { - if (!(obj is Matrix4x4)) - return false; - return this == (Matrix4x4) obj; - } - - /// - /// Returns a hash code for the instance. - /// - /// - /// A hash code for the instance, suitable for use in hashing algorithms and data structures like a hash table. - /// - public override int GetHashCode() - { - unchecked - { - var hash = 17; - for (uint y = 0; y < RowCount; ++y) - for (uint x = 0; x < ColumnCount; ++x) - hash = hash * 23 + this[y, x].GetHashCode(); - return hash; - } - } - - /// - /// Implements the operator ==. - /// - /// The left . - /// The right . - /// - /// The result of the operator. - /// - public static bool operator ==(Matrix4x4 left, Matrix4x4 right) - { - return left.SequenceEqual(right); - } - - /// - /// Implements the operator !=. - /// - /// The left . - /// The right . - /// - /// The result of the operator. - /// - public static bool operator !=(Matrix4x4 left, Matrix4x4 right) - { - return !left.SequenceEqual(right); - } - } -} \ No newline at end of file diff --git a/SharpMath/Geometry/MatrixEnumerator.cs b/SharpMath/Geometry/MatrixEnumerator.cs deleted file mode 100644 index f96fa5c..0000000 --- a/SharpMath/Geometry/MatrixEnumerator.cs +++ /dev/null @@ -1,39 +0,0 @@ -// MatrixEnumerator.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -using System.Collections; -using System.Collections.Generic; - -namespace SharpMath.Geometry -{ - internal class MatrixEnumerator : IEnumerator - { - private readonly IMatrix _matrix; - private int _index; - - internal MatrixEnumerator(IMatrix m) - { - _matrix = m; - _index = -1; - } - - public void Dispose() - { - } - - object IEnumerator.Current => Current; - - public bool MoveNext() - { - _index++; - return _index < _matrix.ColumnCount * _matrix.RowCount; - } - - public void Reset() - { - _index = -1; - } - - public double Current => _matrix[(uint) _index % _matrix.ColumnCount, (uint) _index / _matrix.RowCount]; - } -} diff --git a/SharpMath/Geometry/MatrixUtils.cs b/SharpMath/Geometry/MatrixUtils.cs deleted file mode 100644 index f4b99bc..0000000 --- a/SharpMath/Geometry/MatrixUtils.cs +++ /dev/null @@ -1,400 +0,0 @@ -// MatrixUtils.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -using System; -using System.Collections.Generic; -using System.Linq; -using SharpMath.Equations.Exceptions; -//using SharpMath.Equations; - -namespace SharpMath.Geometry -{ - public static class MatrixUtils - { - /// - /// Adds two instances. - /// - /// The first < see cref="IMatrix" />. - /// The second . - /// The resulting . - public static T Add(T firstMatrix, T secondMatrix) where T : IMatrix - { - for (uint y = 0; y < firstMatrix.RowCount; ++y) - for (uint x = 0; x < firstMatrix.ColumnCount; ++x) - firstMatrix[y, x] += secondMatrix[y, x]; - return firstMatrix; - } - - /// - /// Calculates the cofactor of the specified . - /// - /// The whose cofactor should be calculated. - /// The calculated cofactor . - public static T BuildCofactorMatrix(this T matrix) where T : ISquareMatrix, new() - { - var resultMatrix = new T(); - for (uint y = 0; y < matrix.RowCount; ++y) - for (uint x = 0; x < matrix.ColumnCount; ++x) - resultMatrix[y, x] = matrix.GetCofactor(y, x); - - return resultMatrix; - } - - /// - /// Creates a new object that is a copy of the current instance. - /// - /// - /// A new object that is a copy of this instance. - /// - public static T Clone(this T matrix) where T : IMatrix, new() - { - var cloneMatrix = new T(); - for (uint y = 0; y < matrix.RowCount; ++y) - for (uint x = 0; x < matrix.ColumnCount; ++x) - cloneMatrix[y, x] = matrix[y, x]; - return cloneMatrix; - } - - /// - /// Implements the Gauss-Jordan-algorithm. - /// - /// The left side . - /// The right side . - /// The resulting . - /// The linear equation system cannot be solved clearly. - public static T GaussJordan(T leftSide, T rightSide) where T : IMatrix - { - for (uint x = 0; x < leftSide.ColumnCount; x++) - { - var nextX = x; - while (leftSide[x, x].IsApproximatelyEqualTo(0)) - { - nextX++; - - if (nextX >= leftSide.ColumnCount) - throw new EquationNotSolvableException("The linear equation system cannot be solved clearly."); - - if (Math.Abs(leftSide[x, nextX]) < FloatingNumber.Epsilon) - continue; - - leftSide.InterchangeRows(nextX, x); - rightSide.InterchangeRows(nextX, x); - } - - for (uint y = 0; y < leftSide.RowCount; y++) - if (y != x && Math.Abs(leftSide[y, x]) >= FloatingNumber.Epsilon) - { - var factor = leftSide[y, x] / leftSide[x, x]; - leftSide.SubtractRows(y, x, factor); - rightSide.SubtractRows(y, x, factor); - } - } - - for (uint i = 0; i < leftSide.ColumnCount; i++) - { - var factor = 1 / leftSide[i, i]; - leftSide.MultiplyRow(i, factor); - rightSide.MultiplyRow(i, factor); - } - - return rightSide; - } - - /// - /// Calculates the cofactor of an element at the specified position in the . - /// - /// The . - /// The row of the element. - /// The column of the element. - /// The cofactor of the element in this . - public static double GetCofactor(this ISquareMatrix matrix, uint row, uint column) - { - return Math.Pow(-1, row + column) * matrix.GetSubMatrix(row, column).GetDeterminant(); - } - - //public static TOut GetCore(this ISquareMatrix matrix) where TOut : IVector, new() - //{ - // if (!matrix.Determinant.IsApproximatelyEqualTo(0)) - // throw new InvalidOperationException($"Cannot calculate core of {nameof(matrix)} as its determinant is not 0."); - - // var vector = new TOut(); - // if (matrix.Dimension != vector.Dimension) - // throw new InvalidOperationException($"Type parameter TOut is not an adequate IVector - type as its dimension does not fit the one of the resulting vector. The dimension must be {matrix.Dimension}."); - - // var equations = new List((int)matrix.ColumnCount); - // for (uint y = 0; y < matrix.RowCount; ++y) - // { - // var coefficients = new List((int)matrix.ColumnCount); - // for (uint x = 0; x < matrix.ColumnCount; ++x) - // coefficients.Add(matrix[y, x]); - - // equations.Add(new LinearEquation(coefficients.ToArray(), 0)); - // } - - // var equationSystem = new LinearEquationSystem(equations); - // double[] solutions = equationSystem.Solve(); - // for (uint i = 0; i < matrix.ColumnCount; ++i) - // vector[i] = solutions[i]; - - // return vector; - //} - - /// - /// Calculates the determinant of the . - /// - /// The whose determinant should be calculated. - /// The determinant of the . - public static double GetDeterminant(this ISquareMatrix matrix) - { - switch (matrix.Dimension) - { - case 1: - return matrix[0, 0]; - case 2: - return matrix[0, 0] * matrix[1, 1] - matrix[0, 1] * matrix[1, 0]; - default: - return LaplaceExpansion(matrix); - } - } - - /// - /// Calculates the identity of the specified type. - /// - /// - /// The type whose identity should be - /// calculated. - /// - /// The identity . - public static T GetIdentity() where T : ISquareMatrix, new() - { - var resultMatrix = new T(); - for (uint i = 0; i < resultMatrix.Dimension; ++i) - resultMatrix[i, i] = 1; - return resultMatrix; - } - - //public static T GetInverse(this ISquareMatrix matrix) where T : ISquareMatrix, new() - //{ - // if (matrix.Determinant.IsApproximatelyEqualTo(0)) - // throw new InvalidOperationException("The specified matrix does not have an inverse."); - // return GaussJordan(matrix, GetIdentity()) - //} - - /// - /// Determines whether the is a diagonal , or not. - /// - /// The . - /// - /// true, if the is a diagonal , otherwise - /// false. - /// - public static bool GetIsDiagonal(this ISquareMatrix matrix) - { - if (matrix.Dimension == 1) - return false; - - for (uint y = 0; y < matrix.RowCount; ++y) - for (uint x = 0; x < matrix.ColumnCount; ++x) - if (y == x && FloatingNumber.AreApproximatelyEqual(matrix[y, x], 0) || - y != x && !FloatingNumber.AreApproximatelyEqual(matrix[y, x], 0)) - return false; - return true; - } - - /// - /// Determines whether the is a triangle , or not. - /// - /// The . - /// - /// true, if the is a triangle , otherwise - /// false. - /// - public static bool GetIsTriangle(this ISquareMatrix matrix) - { - if (matrix.Dimension == 1) - return false; - - var upperTriangle = new List(); - var lowerTriangle = new List(); - for (uint y = 0; y < matrix.RowCount; ++y) - { - var isRightSide = false; - for (uint x = 0; x < matrix.ColumnCount; ++x) - { - if (y == x) - { - isRightSide = true; - continue; - } - - if (isRightSide) - upperTriangle.Add(matrix[y, x]); - else - lowerTriangle.Add(matrix[y, x]); - } - } - - if (upperTriangle.All(val => val.IsApproximatelyEqualTo(0)) || - lowerTriangle.All(val => val.IsApproximatelyEqualTo(0))) - return true; - return false; - } - - /// - /// Calculates the negated of the . - /// - /// The whose negated should be calculated. - /// The negated . - public static T GetNegate(this T matrix) where T : IMatrix, new() - { - var resultMatrix = new T(); - for (uint y = 0; y < matrix.RowCount; ++y) - for (uint x = 0; x < matrix.ColumnCount; ++x) - resultMatrix[y, x] = -matrix[y, x]; - return resultMatrix; - } - - /// - /// Calculates the sub of the current by removing the specified column - /// and row. - /// - /// The . - /// The row that should be removed. - /// The column that should be removed. - /// The calculated sub . - public static ISquareMatrix GetSubMatrix(this ISquareMatrix matrix, uint row, uint column) - { - ISquareMatrix resultMatrix; - switch (matrix.Dimension) - { - case 2: - resultMatrix = new Matrix1x1(); - break; - case 3: - resultMatrix = new Matrix2x2(); - break; - case 4: - resultMatrix = new Matrix3x3(); - break; - default: - throw new InvalidOperationException("Cannot build sub matrix of " + nameof(matrix) + - " as there is no lower dimension defined for it."); - } - - uint y = 0; - for (uint cy = 0; cy < matrix.RowCount; cy++) - if (cy != row) - { - uint x = 0; - for (uint cx = 0; cx < matrix.ColumnCount; ++cx) - if (cx != column) - { - resultMatrix[y, x] = matrix[cy, cx]; - x++; - } - - y++; - } - - return resultMatrix; - } - - /// - /// Calculates the transpose of the . - /// - /// The whose transpose should be calculated. - /// The transpose . - public static T GetTranspose(this T matrix) where T : ISquareMatrix, new() - { - var resultMatrix = new T(); - for (uint y = 0; y < matrix.Dimension; ++y) - for (uint x = 0; x < matrix.ColumnCount; ++x) - resultMatrix[y, x] = matrix[x, y]; - return resultMatrix; - } - - internal static void InterchangeRows(this IMatrix matrix, uint firstRowIndex, uint secondRowIndex) - { - for (uint x = 0; x < matrix.ColumnCount; ++x) - { - var firstValue = matrix[firstRowIndex, x]; - matrix[firstRowIndex, x] = matrix[secondRowIndex, x]; - matrix[secondRowIndex, x] = firstValue; - } - } - - internal static double LaplaceExpansion(this ISquareMatrix matrix) - { - double determinant = 0; - for (uint i = 0; i < matrix.Dimension; ++i) - determinant += matrix[i, 0] * matrix.GetCofactor(i, 0); - // The sigma sign is equal to a for-loop with recursion. - return determinant; - } - - /// - /// Multiplies the with a scalar. - /// - /// The to include into the product. - /// The scalar factor that the should be multiplied with. - /// Returns the product. - public static T Multiply(this T matrix, double scalar) where T : IMatrix - { - for (uint y = 0; y < matrix.RowCount; ++y) - for (uint x = 0; x < matrix.ColumnCount; ++x) - matrix[y, x] *= scalar; - return matrix; - } - - /// - /// Multiplies two instances, if they are compatible to each other. - /// - /// The first to include into the product. - /// The second to include into the product. - /// The product. - public static TOut Multiply(IMatrix firstMatrix, IMatrix secondMatrix) where TOut : IMatrix, new() - { - if (firstMatrix.ColumnCount != secondMatrix.RowCount) - throw new ArgumentException( - "The column count of the first matrix does not match the row count of the second matrix."); - - var matrixProduct = new TOut(); - if (matrixProduct.RowCount != firstMatrix.RowCount || matrixProduct.ColumnCount != secondMatrix.ColumnCount) - throw new ArgumentException( - $"Type parameter TOut is not an adequate IMatrix-type as its constraints do not fit those of the resulting matrix. The constraints must be {firstMatrix.RowCount}x{secondMatrix.ColumnCount}."); - - for (uint y = 0; y < matrixProduct.RowCount; ++y) - for (uint x = 0; x < matrixProduct.ColumnCount; ++x) - for (uint i = 0; i < firstMatrix.ColumnCount; ++i) - matrixProduct[y, x] += firstMatrix[y, i] * secondMatrix[i, x]; - - return matrixProduct; - } - - internal static void MultiplyRow(this IMatrix matrix, uint rowIndex, double factor) - { - for (uint x = 0; x < matrix.ColumnCount; ++x) - matrix[rowIndex, x] *= factor; - } - - /// - /// Adds two instances, if they are compatible to each other. - /// - /// The first . - /// The second . - /// The resulting . - public static T Subtract(T firstMatrix, T secondMatrix) where T : IMatrix - { - for (uint y = 0; y < firstMatrix.RowCount; ++y) - for (uint x = 0; x < firstMatrix.ColumnCount; ++x) - firstMatrix[y, x] -= secondMatrix[y, x]; - return firstMatrix; - } - - internal static void SubtractRows(this IMatrix matrix, uint firstRowIndex, uint secondRowIndex, double factor) - { - for (uint x = 0; x < matrix.ColumnCount; x++) - matrix[firstRowIndex, x] -= matrix[secondRowIndex, x] * factor; - } - } -} \ No newline at end of file diff --git a/SharpMath/Geometry/Point2D.cs b/SharpMath/Geometry/Point2D.cs deleted file mode 100644 index 4a55c5f..0000000 --- a/SharpMath/Geometry/Point2D.cs +++ /dev/null @@ -1,215 +0,0 @@ -// Point2D.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; - -// ReSharper disable NonReadonlyMemberInGetHashCode - -namespace SharpMath.Geometry -{ - /// - /// Represents a two-dimensional point. - /// - [Serializable] - public struct Point2D : IEquatable, IEnumerable - { - /// - /// Initializes a new instance of the class. - /// - /// The X-coordinate. - /// The Y-coordinate. - public Point2D(double x, double y) - { - X = x; - Y = y; - } - - /// - /// Initializes a new instance of the class. - /// - /// The exisiting to copy. - public Point2D(Point2D point) - { - X = point.X; - Y = point.Y; - } - - /// - /// Initializes a new instance of the class. - /// - /// The position of the to create. - public Point2D(Vector2 vector) - { - X = vector.X; - Y = vector.Y; - } - - /// - /// Gets or sets the value of the coordinate at the specified index. - /// - /// The index. - /// The value of the coordinate at the specified index. - public double this[uint index] - { - get - { - switch (index) - { - case 0: - return X; - case 1: - return Y; - default: - throw new IndexOutOfRangeException("The index must be between 0 and 1."); - } - } - set - { - switch (index) - { - case 0: - X = value; - break; - case 1: - Y = value; - break; - default: - throw new IndexOutOfRangeException("The index must be between 0 and 1."); - } - } - } - - /// - /// Gets or sets the value of the X-coordinate. - /// - public double X { get; set; } - - /// - /// Gets or sets the value of the Y-coordinate. - /// - public double Y { get; set; } - - /// - /// Gets the position of this . - /// - public Vector2 PositionVector => new Vector2(X, Y); - - public IEnumerator GetEnumerator() - { - for (uint i = 0; i < 2; i++) yield return this[i]; - } - - IEnumerator IEnumerable.GetEnumerator() - { - for (uint i = 0; i < 2; i++) yield return this[i]; - } - - public bool Equals(Point2D other) - { - return this == other; - } - - /// - /// Implements the operator +. - /// - /// The first . - /// The second . - /// - /// The result of the operator. - /// - public static Point2D operator +(Point2D first, Point2D second) - { - var resultPoint = new Point2D(); - for (uint i = 0; i < 2; ++i) - resultPoint[i] = first[i] + second[i]; - return resultPoint; - } - - /// - /// Implements the operator -. - /// - /// The first . - /// The second . - /// - /// The result of the operator. - /// - public static Point2D operator -(Point2D first, Point2D second) - { - var resultPoint = new Point2D(); - for (uint i = 0; i < 2; ++i) - resultPoint[i] = first[i] - second[i]; - return resultPoint; - } - - /// - /// Returns a that represents this instance. - /// - /// - /// A that represents this instance. - /// - public override string ToString() - { - return $"Point2D [X: {this[0]}, Y: {this[1]}]"; - } - - /// - /// Determines whether the specified , is equal to this instance. - /// - /// The to compare with this instance. - /// - /// true if the specified is equal to this instance; otherwise, false. - /// - public override bool Equals(object obj) - { - if (obj is Point2D) - return this == (Point2D) obj; - return false; - } - - /// - /// Returns a hash code for this instance. - /// - /// - /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. - /// - public override int GetHashCode() - { - unchecked - { - var hash = 17; - hash = hash * 23 + X.GetHashCode(); - hash = hash * 23 + Y.GetHashCode(); - return hash; - } - } - - /// - /// Implements the operator ==. - /// - /// The left . - /// The right . - /// - /// The result of the operator. - /// - public static bool operator ==(Point2D left, Point2D right) - { - return left.SequenceEqual(right); - } - - /// - /// Implements the operator !=. - /// - /// The left . - /// The right . - /// - /// The result of the operator. - /// - public static bool operator !=(Point2D left, Point2D right) - { - return !left.SequenceEqual(right); - } - } -} \ No newline at end of file diff --git a/SharpMath/Geometry/Point3D.cs b/SharpMath/Geometry/Point3D.cs deleted file mode 100644 index 9626b32..0000000 --- a/SharpMath/Geometry/Point3D.cs +++ /dev/null @@ -1,227 +0,0 @@ -// Point3D.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; - -// ReSharper disable NonReadonlyMemberInGetHashCode - -namespace SharpMath.Geometry -{ - [Serializable] - public struct Point3D : IEquatable, IEnumerable - { - /// - /// Initializes a new instance of the class. - /// - /// The value of the X-coordinate. - /// The value of the Y-coordinate. - /// The value of the Z-coordinate. - public Point3D(double x, double y, double z) - { - X = x; - Y = y; - Z = z; - } - - /// - /// Initializes a new instance of the class. - /// - /// The exisiting to copy. - public Point3D(Point3D point) - { - X = point.X; - Y = point.Y; - Z = point.Z; - } - - /// - /// Initializes a new instance of the class. - /// - /// The position of the to create. - public Point3D(Vector3 vector) - { - X = vector.X; - Y = vector.Y; - Z = vector.Z; - } - - /// - /// Gets or sets the value of the coordinate at the specified index. - /// - /// The index. - /// The value of the coordinate at the specified index. - public double this[uint index] - { - get - { - switch (index) - { - case 0: - return X; - case 1: - return Y; - case 2: - return Z; - default: - throw new IndexOutOfRangeException("The index must be between 0 and 2."); - } - } - set - { - switch (index) - { - case 0: - X = value; - break; - case 1: - Y = value; - break; - case 2: - Z = value; - break; - default: - throw new IndexOutOfRangeException("The index must be between 0 and 2."); - } - } - } - - /// - /// Gets or sets the value of the X-coordinate. - /// - public double X { get; set; } - - /// - /// Gets or sets the value of the Y-coordinate. - /// - public double Y { get; set; } - - /// - /// Gets or sets the value of the Z-coordinate. - /// - public double Z { get; set; } - - /// - /// Gets the position of this . - /// - public Vector3 PositionVector => new Vector3(X, Y, Z); - - public IEnumerator GetEnumerator() - { - for (uint i = 0; i < 3; i++) yield return this[i]; - } - - IEnumerator IEnumerable.GetEnumerator() - { - for (uint i = 0; i < 3; i++) yield return this[i]; - } - - public bool Equals(Point3D other) - { - return this == other; - } - - /// - /// Implements the operator +. - /// - /// The first . - /// The second . - /// - /// The result of the operator. - /// - public static Point3D operator +(Point3D first, Point3D second) - { - var resultPoint = new Point3D(); - for (uint i = 0; i < 3; ++i) - resultPoint[i] = first[i] + second[i]; - return resultPoint; - } - - /// - /// Implements the operator -. - /// - /// The first . - /// The second . - /// - /// The result of the operator. - /// - public static Point3D operator -(Point3D first, Point3D second) - { - var resultPoint = new Point3D(); - for (uint i = 0; i < 3; ++i) - resultPoint[i] = first[i] - second[i]; - return resultPoint; - } - - /// - /// Returns a that represents this instance. - /// - /// - /// A that represents this instance. - /// - public override string ToString() - { - return $"Point3D [X: {this[0]}, Y: {this[1]}, Z: {this[2]}]"; - } - - /// - /// Determines whether the specified , is equal to this instance. - /// - /// The to compare with this instance. - /// - /// true if the specified is equal to this instance; otherwise, false. - /// - public override bool Equals(object obj) - { - if (obj is Point3D) - return this == (Point3D) obj; - return false; - } - - /// - /// Returns a hash code for this instance. - /// - /// - /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. - /// - public override int GetHashCode() - { - unchecked - { - var hash = 17; - hash = hash * 23 + X.GetHashCode(); - hash = hash * 23 + Y.GetHashCode(); - hash = hash * 23 + Z.GetHashCode(); - return hash; - } - } - - /// - /// Implements the operator ==. - /// - /// The left . - /// The right . - /// - /// The result of the operator. - /// - public static bool operator ==(Point3D left, Point3D right) - { - return left.SequenceEqual(right); - } - - /// - /// Implements the operator !=. - /// - /// The left . - /// The right . - /// - /// The result of the operator. - /// - public static bool operator !=(Point3D left, Point3D right) - { - return !left.SequenceEqual(right); - } - } -} \ No newline at end of file diff --git a/SharpMath/Geometry/Polygon.cs b/SharpMath/Geometry/Polygon.cs deleted file mode 100644 index 6b6cf49..0000000 --- a/SharpMath/Geometry/Polygon.cs +++ /dev/null @@ -1,287 +0,0 @@ -// Polygon.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Drawing; -using System.Linq; - -// ReSharper disable ForCanBeConvertedToForeach - -namespace SharpMath.Geometry -{ - /// - /// Represents a polygon. - /// - public class Polygon : IEnumerable, IEquatable - { - /// - /// Initializes a new instance of the class. - /// - /// The instances that the consists of. - public Polygon(params Point2D[] points) - { - if (points.Length < 3) - throw new ArgumentException("A polygon needs to have at least 3 points."); - Points = points.ToList(); - } - - /// - /// Gets the area of this . - /// - public double Area - { - get - { - double value = 0; - var j = 1; - for (var i = 0; i < Points.Count; ++i) - { - value += Vector2.Area(Points[i].PositionVector, Points[j].PositionVector); - j++; - if (j == Points.Count) - j = 0; - } - - return Math.Abs(value * 0.5); - } - } - - /// - /// Gets the bounding box of this as a . - /// - public RectangleF BoundingBox - { - get - { - double minX = 0; - double maxX = 0; - double minY = 0; - double maxY = 0; - - foreach (var point in Points) - { - if (point.X < minX) - minX = point.X; - else if (point.X > maxX) - maxX = point.X; - - if (point.Y < minY) - minY = point.Y; - else if (point.Y > maxY) - maxY = point.Y; - } - - return RectangleF.FromLTRB((float) minX, (float) minY, (float) maxX, (float) maxY); - } - } - - /// - /// Gets the center of this . - /// - public Point2D Center - { - get - { - double x = 0; - double y = 0; - for (var i = 0; i < Points.Count - 1; ++i) - { - var factor = Points[i].X * Points[i + 1].Y - Points[i + 1].X * Points[i].Y; - x += (Points[i].X + Points[i + 1].X) * factor; - y += (Points[i].Y + Points[i + 1].Y) * factor; - } - - var areaFactor = 1 / 6d * Area; - return new Point2D(areaFactor * x, areaFactor * y); - } - } - - /// - /// Gets the perimeter of this . - /// - public double Perimeter - { - get - { - double perimeter = 0; - var j = 1; - for (var i = 0; i < Points.Count; ++i) - { - perimeter += (Points[j] - Points[i]).PositionVector.Magnitude; - j++; - if (j == Points.Count) - j = 0; - } - - return perimeter; - } - } - - /// - /// Gets the instances that this consists of. - /// - public List Points { get; } - - /// - /// Returns an enumerator that iterates through a collection. - /// - /// - /// An object that can be used to iterate through the collection. - /// - IEnumerator IEnumerable.GetEnumerator() - { - return Points.GetEnumerator(); - } - - /// - /// Returns an enumerator that iterates through the collection. - /// - /// - /// An enumerator that can be used to iterate through the collection. - /// - public IEnumerator GetEnumerator() - { - return Points.GetEnumerator(); - } - - /// - /// Determines whether the specified is equal to this instance. - /// - /// The to compare with this instance. - /// - /// true if the specified is equal to this instance; otherwise, false. - /// - public bool Equals(Polygon other) - { - return ReferenceEquals(other, null) ? ReferenceEquals(this, null) : other.Points.SequenceEqual(Points); - } - - /// - /// Determines whether this fully contains the specified . - /// - /// The other . - /// true if the specified is inside this one; otherwise false. - public bool Contains(Polygon other) - { - return other.Points.All(ContainsPoint); - } - - /// - /// Determines whether this contains the specified . - /// - /// The to check. - /// - /// true if this contains the specified ; otherwise false. - /// - public bool ContainsPoint(Point2D point) - { - double t = -1; - var points = new List(); - points.AddRange(Points); - points.Add(points[0]); - - for (var i = 0; i < points.Count - 1; i++) t = t * ContainsPointInternal(point, points[i], points[i + 1]); - return t >= 0; - } - - // https://de.wikipedia.org/wiki/Punkt-in-Polygon-Test_nach_Jordan - private int ContainsPointInternal(Point2D q, Point2D p1, Point2D p2) - { - if (FloatingNumber.AreApproximatelyEqual(q.Y, p1.Y) && - FloatingNumber.AreApproximatelyEqual(q.Y, p2.Y)) - { - if (p1.X <= q.X && q.X <= p2.X || - p2.X <= q.X && q.X <= p1.X) - return 0; - return 1; - } - - if (p1.Y > p2.Y) - { - var currentP1 = p1; - p1 = p2; - p2 = currentP1; - } - - if (FloatingNumber.AreApproximatelyEqual(q.Y, p1.Y) && - FloatingNumber.AreApproximatelyEqual(q.X, p1.X)) - return 0; - - if (q.Y <= p1.Y || q.Y > p2.Y) - return 1; - - var delta = (p1.X - q.X) * (p2.Y - q.Y) - (p1.Y - q.Y) * (p2.X - q.X); - if (delta > 0) - return -1; - return delta < 0 ? 1 : 0; - } - - /// - /// Determines whether the specified is equal to this instance. - /// - /// The to compare with this instance. - /// - /// true if the specified is equal to this instance; otherwise, false. - /// - public override bool Equals(object obj) - { - return ReferenceEquals(obj, null) - ? ReferenceEquals(this, null) - : obj is Polygon && ((Polygon) obj).Points.SequenceEqual(Points); - } - - /// - /// Returns a hash code for this instance. - /// - /// - /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. - /// - public override int GetHashCode() - { - return Points.GetHashCode(); - } - - /// - /// Determines whether the two specified instances are equal to each other. - /// - /// The first . - /// The to compare with the other . - /// - /// true if the two specified are equal to each other; otherwise, false. - /// - public static bool operator ==(Polygon left, Polygon right) - { - if (ReferenceEquals(left, right)) - return true; - return left != null && left.Equals(right); - } - - /// - /// Determines whether the two specified instances are not equal to each other. - /// - /// The first . - /// The to compare with the other . - /// - /// true if the two specified are not equal to each other; otherwise, false. - /// - public static bool operator !=(Polygon left, Polygon right) - { - if (ReferenceEquals(left, right)) - return false; - return left != null && !left.Equals(right); - } - - /// - /// Returns a that represents this instance. - /// - /// - /// A that represents this instance. - /// - public override string ToString() - { - return $"Polygon {{{string.Join(", ", Points)}}}"; - } - } -} \ No newline at end of file diff --git a/SharpMath/Geometry/ProjectionData.cs b/SharpMath/Geometry/ProjectionData.cs deleted file mode 100644 index 63846fc..0000000 --- a/SharpMath/Geometry/ProjectionData.cs +++ /dev/null @@ -1,87 +0,0 @@ -// ProjectionData.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -using System; -using System.Drawing; - -namespace SharpMath.Geometry -{ - /// - /// Provides configuration data for performing projections in a view frustum. - /// - [Serializable] - public struct ProjectionData - { - /// - /// Initializes a new instance of the struct. - /// - /// The size of the canvas. - /// The aspect ratio. - /// The field of view. - /// The near plane. - /// The far plane. - /// - /// nearPlane must be a value greater than zero. - /// or - /// farPlane must be a value greater than zero. - /// or - /// nearPlane must be a value smaller than farPlane. - /// or - /// FOV must be greater than zero and must not be bigger than 360 degrees (PI). - /// - public ProjectionData(Size canvasSize, float aspectRatio, float fieldOfView, float nearPlane, float farPlane) - { - if (nearPlane <= 0) - throw new ArgumentException("nearPlane must be a value greater than zero."); - if (farPlane <= 0) - throw new ArgumentException("farPlane must be a value greater than zero."); - if (nearPlane >= farPlane) - throw new ArgumentException("nearPlane must be a value smaller than farPlane."); - if (fieldOfView <= 0 || fieldOfView > Math.PI) - throw new ArgumentException( - "FOV must be greater than zero and must not be bigger than 360 degrees (PI)."); - - CanvasSize = canvasSize; - AspectRatio = aspectRatio; - FieldOfView = fieldOfView; - NearPlane = nearPlane; - FarPlane = farPlane; - } - - /// - /// Gets or sets the size of the canvas. - /// - public Size CanvasSize { get; set; } - - /// - /// Gets or sets the aspect ratio. - /// - public float AspectRatio { get; set; } - - /// - /// Gets or sets the field of view (FOV). - /// - public float FieldOfView { get; set; } - - /// - /// Gets or sets the near plane. - /// - public float NearPlane { get; set; } - - /// - /// Gets or sets the far plane. - /// - public float FarPlane { get; set; } - - /// - /// Gets the depth. - /// - public float Depth => FarPlane - NearPlane; - - /// - /// Gets the default . - /// - public static ProjectionData Default - => new ProjectionData(new Size(100, 100), 16f / 9f, (float) Math.PI / 3f, 1f, 100f); - } -} \ No newline at end of file diff --git a/SharpMath/Geometry/VariableMatrix.cs b/SharpMath/Geometry/VariableMatrix.cs deleted file mode 100644 index 05fd83d..0000000 --- a/SharpMath/Geometry/VariableMatrix.cs +++ /dev/null @@ -1,60 +0,0 @@ -// VariableMatrix.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -using System; -using System.ComponentModel; - -namespace SharpMath.Geometry -{ - /// - /// Represents a matrix that can vary in its column and row count. - /// - internal sealed class VariableMatrix : IMatrix - { - private readonly double[,] _fields; - - /// - /// Initializes a new instance of the class. - /// - /// The row count of the . - /// The column count of the . - public VariableMatrix(uint rowCount, uint columnCount) - { - _fields = new double[rowCount, columnCount]; - RowCount = rowCount; - ColumnCount = columnCount; - } - - /// - /// Gets the column count of the . - /// - public uint ColumnCount { get; } - - /// - /// Gets or sets the field value at the specified row and column indices. - /// - /// The row index. - /// The column index. - /// The field value at the specified row and column indices. - public double this[uint row, uint column] - { - get => _fields[row, column]; - set => _fields[row, column] = value; - } - - [EditorBrowsable(EditorBrowsableState.Never)] - [Obsolete("Simple indexer is not supported in this class.", true)] - public double this[uint index] - { - get => double.NaN; - - // ReSharper disable once ValueParameterNotUsed - set { } - } - - /// - /// Gets the row count of the . - /// - public uint RowCount { get; } - } -} \ No newline at end of file diff --git a/SharpMath/Geometry/Vector2.cs b/SharpMath/Geometry/Vector2.cs deleted file mode 100644 index 870eea5..0000000 --- a/SharpMath/Geometry/Vector2.cs +++ /dev/null @@ -1,543 +0,0 @@ -// Vector2.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using SharpMath.Geometry.Exceptions; - -namespace SharpMath.Geometry -{ - /// - /// Represents a two-dimensional vector. - /// - [Serializable] - public struct Vector2 : IVector, IEquatable, IEnumerable - { - /// - /// Initializes a new instance of the class. - /// - /// The existing to copy. - public Vector2(Vector2 vector) - { - X = vector.X; - Y = vector.Y; - } - - /// - /// Initializes a new instance of the class. - /// - /// The value of the X-coordinate (X1 in mathematic coordinate systems). - /// The value of the Y-coordinate (X2 in mathematic coordinate systems). - public Vector2(double x, double y) - { - X = x; - Y = y; - } - - /// - /// Initializes a new instance of the class. - /// - /// The that a position should be created for. - public Vector2(Point2D point) - : this(point.PositionVector) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The tail of the . - /// The head of the . - public Vector2(Point2D bottom, Point2D tip) - : this((tip - bottom).PositionVector) - { - } - - /// - /// Gets or sets the value of the X-coordinate (X1 in mathematic coordinate systems). - /// - public double X { get; set; } - - /// - /// Gets or sets the value of the Y-coordinate (X2 in mathematic coordinate systems). - /// - public double Y { get; set; } - - /// - /// A with all values set to zero. - /// - public static Vector2 Zero => new Vector2(0, 0); - - /// - /// A with all values set to one. - /// - public static Vector2 One => new Vector2(1, 1); - - /// - /// A unit pointing up. - /// - public static Vector2 Up => new Vector2(0, 1); - - /// - /// A unit pointing down. - /// - public static Vector2 Down => new Vector2(0, -1); - - /// - /// A unit pointing to the left. - /// - public static Vector2 Left => new Vector2(-1, 0); - - /// - /// A unit pointing to the right. - /// - public static Vector2 Right => new Vector2(1, 0); - - /// - /// A unit with coordinates 1, 0. - /// - public static Vector2 UnitX => new Vector2(1, 0); - - /// - /// A unit with coordinates 0, 1. - /// - public static Vector2 UnitY => new Vector2(0, 1); - - /// - /// Gets the that is perpendicular to the . - /// - public Vector2 Perpendicular => new Vector2(-Y, X); - - /// - /// Gets or sets the value of the coordinate at the specified index. - /// - /// The index. - /// The value of the coordinate at the specified index. - public double this[uint index] - { - get - { - switch (index) - { - case 0: - return X; - case 1: - return Y; - default: - throw new IndexOutOfRangeException("The index must be between 0 and 1."); - } - } - set - { - switch (index) - { - case 0: - X = value; - break; - case 1: - Y = value; - break; - default: - throw new IndexOutOfRangeException("The index must be between 0 and 1."); - } - } - } - - /// - /// Gets the dimension of the . - /// - public uint Dimension => 2; - - /// - /// Gets the length of the . - /// - public double Magnitude => Math.Sqrt(SquareMagnitude); - - /// - /// Gets the squared length of the . - /// - public double SquareMagnitude - { - get - { - double result = 0; - for (uint i = 0; i < 2; ++i) - result += Math.Pow(this[i], 2); - return result; - } - } - - /// - /// Gets a value indicating whether the is normalized, or not. - /// - public bool IsNormalized => Magnitude.IsApproximatelyEqualTo(1); - - /// - /// Gets a value indicating whether the has all of its components set to zero, or not. - /// - public bool IsZero => this.All(c => FloatingNumber.AreApproximatelyEqual(c, 0)); - - /// - /// Gets the LaTeX-string representing this vector graphically. - /// - public string ToLaTeXString() - { - return @"\left( \begin{array}{c} " + this[0] + @" \\ " + this[1] + @" \end{array} \right)"; - } - - /// - /// Generates a from an object that implements the interface, if the - /// dimension is correct. - /// - /// The to generate a from. - /// The generated . - /// The dimension of the given vector is invalid. It must be 2. - public static Vector2 FromVector(IVector vector) - { - if (vector.Dimension != 2) - throw new DimensionException("The dimension of the given vector is invalid. It must be 2."); - return new Vector2(vector[0], vector[1]); - } - - /// - /// Calculates the angle between two instances. - /// - /// The first . - /// The second . - /// The angle between the instances. - public static double Angle(Vector2 firstVector, Vector2 secondVector) - { - return firstVector.Angle(secondVector); - } - - /// - /// Calculates the area of the parallelogram that this and the specified instances span. - /// - /// The other . - /// The area of the spanned parallelogram. - public double Area(Vector2 other) - { - return Math.Abs(VectorProduct(other)); - } - - /// - /// Calculates the area of the parallelogram that the two specified instances span. - /// - /// The first . - /// The second . - /// The area of the spanned parallelogram. - public static double Area(Vector2 firstVector, Vector2 secondVector) - { - return firstVector.Area(secondVector); - } - - /// - /// Determines whether two instances are orthogonal to each other, or not. - /// - /// The first . - /// The second . - /// true, if the instances are orthogonal to each other, otherwise false. - public static bool AreOrthogonal(Vector2 firstVector, Vector2 secondVector) - { - return firstVector.CheckForOrthogonality(secondVector); - } - - /// - /// Determines whether two instances are orthonormal to each other, or not. - /// - /// The first . - /// The second . - /// true if the instances are orthonormal to each other, otherwise false. - public static bool AreOrthonormal(Vector2 firstVector, Vector2 secondVector) - { - return firstVector.CheckForOrthonormality(secondVector); - } - - /// - /// Determines whether two instances are parallel to each other, or not. - /// - /// The first . - /// The second . - /// true if the instances are parallel to each other, otherwise false. - public static bool AreParallel(Vector2 firstVector, Vector2 secondVector) - { - return firstVector.CheckForParallelism(secondVector); - } - - /// - /// Calculates the distance between two points. - /// - /// The source point. - /// The target point. - /// The distance between specified points. - public static double Distance(Vector2 source, Vector2 target) - { - return source.Distance(target); - } - - /// - /// Divides a by multipling it with the reciprocal of the scalar. - /// - /// The . - /// The scalar whose reciprocal will be calculated. - /// The resulting . - public static Vector2 Divide(Vector2 vector, double scalar) - { - return vector * (1 / scalar); - } - - /// - /// Calculates the dot product of the specified instances. - /// - /// The first that should be included into the calculation. - /// The second that should be included into the calculation. - /// The calculated scalar as a . - public static double DotProduct(Vector2 firstVector, Vector2 secondVector) - { - return firstVector.DotProduct(secondVector); - } - - /// - /// Linearly interpolates between two instances. - /// - /// The source point. - /// The target point. - /// The fraction. - /// The position of the new point. - public static Vector2 Lerp(Vector2 source, Vector2 target, double fraction) - { - return source.Lerp(target, fraction); - } - - /// - /// Linearly interpolates between two instances. - /// - /// The source point. - /// The target point. - /// The fraction. - /// The position of the new point. - public static Vector2 LerpUnclamped(Vector2 source, Vector2 target, double fraction) - { - return source.LerpUnclamped(target, fraction); - } - - /// - /// Moves a source point in a straight line towards a target point by adding the given distance delta and returns its - /// new position. - /// - /// The source point. - /// The target point. - /// The distance delta that the source point is moved by in all directions. - /// The position of the new point. - public static Vector2 MoveTowards(Vector2 source, Vector2 target, double maxDistanceDelta) - { - return source.MoveTowards(target, maxDistanceDelta); - } - - /// - /// Calculates the vector product of the current and the specified instance. - /// - /// The other . - /// The vector product of the current and the specified . - public double VectorProduct(Vector2 other) - { - return X * other.Y - Y * other.X; - } - - /// - /// Calculates the vector product of the specified instances. - /// - /// The first . - /// The second . - /// The vector product of the specified instances. - public static double VectorProduct(Vector2 firstVector, Vector2 secondVector) - { - return firstVector.VectorProduct(secondVector); - } - - /// - /// Implements the operator +. - /// - /// The first . - /// The second . - /// - /// The resulting . - /// - public static Vector2 operator +(Vector2 firstVector, Vector2 secondVector) - { - return firstVector.Add(secondVector); - } - - /// - /// Implements the operator -. - /// - /// The first . - /// The second . - /// - /// The resulting . - /// - public static Vector2 operator -(Vector2 firstVector, Vector2 secondVector) - { - return firstVector.Subtract(secondVector); - } - - /// - /// Implements the operator -. - /// - /// The to negate. - /// - /// The negated . - /// - public static Vector2 operator -(Vector2 current) - { - return current.Negate(); - } - - /// - /// Implements the operator *. - /// - /// The . - /// The scalar. - /// - /// The resulting . - /// - public static Vector2 operator *(Vector2 vector, double scalar) - { - return vector.Multiply(scalar); - } - - /// - /// Implements the operator * for calculating the dot product of two instances. - /// - /// The first . - /// The second . - /// - /// The scalar that has been calculated. - /// - public static double operator *(Vector2 firstVector, Vector2 secondVector) - { - return firstVector.DotProduct(secondVector); - } - - /// - /// Transforms this with the specified . - /// - /// The transformation . - /// The transformed . - public Vector2 Transform(Matrix3x3 matrix) - { - return matrix * this; - } - - /// - /// Transforms the specified with the specified . - /// - /// The that should be transformed. - /// The transformation . - /// The transformed . - public static Vector2 Transform(Vector2 vector, Matrix3x3 matrix) - { - return vector.Transform(matrix); - } - - /// - /// Transforms the specified with the specified . - /// - /// The transformation . - /// The that should be transformed. - /// The transformed . - public static Vector2 Transform(Matrix3x3 matrix, Vector2 vector) - { - return vector.Transform(matrix); - } - - /// - /// Returns a that represents this instance. - /// - /// - /// A that represents this instance. - /// - public override string ToString() - { - return $"Vector2 {{X: {this[0]}, Y: {this[1]}}}"; - } - - /// - /// Determines whether the specified , is equal to the current instance. - /// - /// The to compare with the current instance. - /// - /// true if the specified is equal to the current instance; otherwise, false. - /// - public override bool Equals(object obj) - { - if (obj is Vector2) - return this == (Vector2) obj; - var vector = obj as IVector; - if (Dimension != vector?.Dimension) - return false; - return this == FromVector(vector); - } - - /// - /// Returns a hash code for this instance. - /// - /// - /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. - /// - public override int GetHashCode() - { - unchecked - { - var hash = 17; - // ReSharper disable NonReadonlyMemberInGetHashCode - hash = hash * 23 + X.GetHashCode(); - hash = hash * 23 + Y.GetHashCode(); - return hash; - } - } - - public bool Equals(Vector2 other) - { - return this == other; - } - - public IEnumerator GetEnumerator() - { - for (uint i = 0; i < 2; i++) yield return this[i]; - } - - IEnumerator IEnumerable.GetEnumerator() - { - for (uint i = 0; i < 2; i++) yield return this[i]; - } - - /// - /// Implements the operator ==. - /// - /// The left . - /// The right . - /// - /// The result of the operator. - /// - public static bool operator ==(Vector2 left, Vector2 right) - { - return left.SequenceEqual(right); - } - - /// - /// Implements the operator !=. - /// - /// The left . - /// The right . - /// - /// The result of the operator. - /// - public static bool operator !=(Vector2 left, Vector2 right) - { - return !left.SequenceEqual(right); - } - } -} \ No newline at end of file diff --git a/SharpMath/Geometry/Vector3.cs b/SharpMath/Geometry/Vector3.cs deleted file mode 100644 index 4fd994b..0000000 --- a/SharpMath/Geometry/Vector3.cs +++ /dev/null @@ -1,493 +0,0 @@ -// Vector3.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; - -// ReSharper disable NonReadonlyMemberInGetHashCode - -namespace SharpMath.Geometry -{ - /// - /// Represents a three-dimensional vector. - /// - [Serializable] - public struct Vector3 : IVector, IEquatable, IEnumerable - { - /// - /// Initializes a new instance of the class. - /// - /// The existing to copy. - public Vector3(Vector3 vector) - { - X = vector.X; - Y = vector.Y; - Z = vector.Z; - } - - /// - /// Initializes a new instance of the class. - /// - /// The value of the X-coordinate (X2 in mathematic coordinate systems). - /// The value of the Y-coordinate (X3 in mathematic coordinate systems). - /// The value of the Z-coordinate (X1 in mathematic coordinate systems). - public Vector3(double x, double y, double z) - { - X = x; - Y = y; - Z = z; - } - - /// - /// Initializes a new instance of the class. - /// - /// The that a position should be created for. - public Vector3(Point3D point) - { - X = point.X; - Y = point.Y; - Z = point.Z; - } - - /// - /// Initializes a new instance of the class. - /// - /// The tail of the . - /// The head of the . - public Vector3(Point3D bottom, Point3D tip) - : this((tip - bottom).PositionVector) - { - } - - /// - /// Gets or sets the value of the X-coordinate (X2 in mathematic coordinate systems). - /// - public double X { get; set; } - - /// - /// Gets or sets the value of the Y-coordinate (X3 in mathematic coordinate systems). - /// - public double Y { get; set; } - - /// - /// Gets or sets the value of the Z-coordinate (X1 in mathematic coordinate systems). - /// - public double Z { get; set; } - - /// - /// A with all values set to zero. - /// - public static Vector3 Zero => new Vector3(0, 0, 0); - - /// - /// A with all values set to one. - /// - public static Vector3 One => new Vector3(1, 1, 1); - - /// - /// A unit pointing up. - /// - public static Vector3 Up => new Vector3(0, 1, 0); - - /// - /// A unit pointing down. - /// - public static Vector3 Down => new Vector3(0, -1, 0); - - /// - /// A unit pointing to the left. - /// - public static Vector3 Left => new Vector3(-1, 0, 0); - - /// - /// A unit pointing to the right. - /// - public static Vector3 Right => new Vector3(1, 0, 0); - - /// - /// A unit pointing forward. - /// - public static Vector3 Forward => new Vector3(0, 0, 1); - - /// - /// A unit pointing backward. - /// - public static Vector3 Back => new Vector3(0, 0, -1); - - /// - /// A unit with coordinates 1, 0, 0. - /// - public static Vector3 UnitX => new Vector3(1, 0, 0); - - /// - /// A unit with coordinates 0, 1, 0. - /// - public static Vector3 UnitY => new Vector3(0, 1, 0); - - /// - /// A unit with coordinates 0, 0, 1. - /// - public static Vector3 UnitZ => new Vector3(0, 0, 1); - - public IEnumerator GetEnumerator() - { - for (uint i = 0; i < 3; i++) yield return this[i]; - } - - IEnumerator IEnumerable.GetEnumerator() - { - for (uint i = 0; i < 3; i++) yield return this[i]; - } - - public bool Equals(Vector3 other) - { - return this == other; - } - - /// - /// Gets or sets the value of the coordinate at the specified index. - /// - /// The index. - /// The value of the coordinate at the specified index. - public double this[uint index] - { - get - { - switch (index) - { - case 0: - return X; - case 1: - return Y; - case 2: - return Z; - default: - throw new IndexOutOfRangeException("The index must be between 0 and 2."); - } - } - set - { - switch (index) - { - case 0: - X = value; - break; - case 1: - Y = value; - break; - case 2: - Z = value; - break; - default: - throw new IndexOutOfRangeException("The index must be between 0 and 2."); - } - } - } - - /// - /// Gets the dimension of the . - /// - public uint Dimension => 3; - - /// - /// Gets the length of the . - /// - public double Magnitude => Math.Sqrt(SquareMagnitude); - - /// - /// Gets the squared length of the . - /// - public double SquareMagnitude - { - get - { - double result = 0; - for (uint i = 0; i < 3; ++i) - result += Math.Pow(this[i], 2); - return result; - } - } - - /// - /// Gets a value indicating whether the is normalized, or not. - /// - public bool IsNormalized => Magnitude.IsApproximatelyEqualTo(1); - - /// - /// Gets a value indicating whether the has all of its components set to zero, or not. - /// - public bool IsZero => this.All(c => FloatingNumber.AreApproximatelyEqual(c, 0)); - - /// - /// Gets the LaTeX-string representing this vector graphically. - /// - public string ToLaTeXString() - { - return @"\left( \begin{array}{c} " + this[0] + @" \\ " + this[1] + @" \\ " + this[2] + - @" \end{array} \right)"; - } - - /// - /// Generates a from an object implementing the interface, if the - /// dimension is correct. - /// - /// The to generate a from. - /// The generated . - /// The dimension of the given vector is invalid. It must be 3. - public static Vector3 FromVector(IVector vector) - { - if (vector.Dimension != 3) - throw new ArgumentException("The dimension of the given vector is invalid. It must be 3."); - return new Vector3(vector[0], vector[1], vector[2]); - } - - /// - /// Calculates the area of the parallelogram that this and the specified instances span. - /// - /// The other . - /// The area of the spanned parallelogram. - public double Area(Vector3 other) - { - return VectorProduct(this, other).Magnitude; - } - - /// - /// Calculates the area of the parallelogram that the two specified instances span. - /// - /// The first . - /// The second . - /// The area of the spanned parallelogram. - public static double Area(Vector3 firstVector, Vector3 secondVector) - { - return firstVector.Area(secondVector); - } - - /// - /// Linearly interpolates between two instances. - /// - /// The source point. - /// The target point. - /// The fraction. - /// The position of the new point. - public static Vector3 Lerp(Vector3 source, Vector3 target, double fraction) - { - return source.Lerp(target, fraction); - } - - /// - /// Linearly interpolates between two instances. - /// - /// The source point. - /// The target point. - /// The fraction. - /// The position of the new point. - public static Vector3 LerpUnclamped(Vector3 source, Vector3 target, double fraction) - { - return source.LerpUnclamped(target, fraction); - } - - /// - /// Moves a source point in a straight line towards a target point by adding the given distance delta and returns its - /// new position. - /// - /// The source point. - /// The target point. - /// The distance delta that the source point is moved by in all directions. - /// The position of the new point. - public static Vector3 MoveTowards(Vector3 source, Vector3 target, double maxDistanceDelta) - { - return source.MoveTowards(target, maxDistanceDelta); - } - - /// - /// Calculates the volume of the parallelepiped that is being created by the three specified - /// instances. - /// - /// The first . - /// The second . - /// The third . - /// Returns the calculated volume. - public static double ScalarTripleProduct(Vector3 firstVector, Vector3 secondVector, Vector3 thirdVector) - { - //var matrix = SquareMatrix.FromMatrix(firstVector.AsHorizontalMatrix().AugmentVertically(secondVector.AsHorizontalMatrix()).AugmentVertically(thirdVector.AsHorizontalMatrix())); - //return Math.Abs(matrix.Determinant); - - return Math.Abs(VectorProduct(firstVector, secondVector).DotProduct(thirdVector)); - } - - /// - /// Calculates the vector product of the current and the specified . - /// - /// The other . - /// The calculated . - public Vector3 VectorProduct(Vector3 other) - { - return new Vector3(Y * other.Z - Z * other.Y, Z * other.X - X * other.Z, X * other.Y - Y * other.X); - } - - /// - /// Calculates the that is perpendicular to the specified instances. - /// - /// The first that should be included into the calculation. - /// The second that should be included into the calculation. - /// Returns the calculated . - public static Vector3 VectorProduct(Vector3 firstVector, Vector3 secondVector) - { - return firstVector.VectorProduct(secondVector); - } - - /// - /// Implements the operator +. - /// - /// The first . - /// The second . - /// - /// The resulting . - /// - public static Vector3 operator +(Vector3 firstVector, Vector3 secondVector) - { - return firstVector.Add(secondVector); - } - - /// - /// Implements the operator -. - /// - /// The first . - /// The second . - /// - /// The resulting . - /// - public static Vector3 operator -(Vector3 firstVector, Vector3 secondVector) - { - return firstVector.Subtract(secondVector); - } - - /// - /// Implements the operator -. - /// - /// The to negate. - /// - /// The negated . - /// - public static Vector3 operator -(Vector3 current) - { - return current.Negate(); - } - - /// - /// Implements the operator *. - /// - /// The . - /// The scalar. - /// - /// The resulting . - /// - public static Vector3 operator *(Vector3 vector, double scalar) - { - return vector.Multiply(scalar); - } - - /// - /// Implements the operator * for calculating the scalar product of two instances. - /// - /// The first . - /// The second . - /// - /// The scalar that has been calculated. - /// - public static double operator *(Vector3 firstVector, Vector3 secondVector) - { - return firstVector.DotProduct(secondVector); - } - - /// - /// Transforms the specified with the specified . - /// - /// The that should be transformed. - /// The transformation . - /// The transformed . - public static Vector3 Transform(Vector3 vector, Matrix4x4 matrix) - { - var result = matrix * new Vector4(vector.X, vector.Y, vector.Z, 1); - result.X /= result.W; - result.Y /= result.W; - result.Z /= result.W; - result.Z /= result.W; - return result.Convert(); - } - - /// - /// Returns a that represents this instance. - /// - /// - /// A that represents this instance. - /// - public override string ToString() - { - return $"X: {this[0]}, Y: {this[1]}, Z: {this[2]}"; - } - - /// - /// Determines whether the specified , is equal to this instance. - /// - /// The to compare with this instance. - /// - /// true if the specified is equal to this instance; otherwise, false. - /// - public override bool Equals(object obj) - { - if (obj is Vector3) - return this == (Vector3) obj; - var vector = obj as IVector; - if (Dimension != vector?.Dimension) - return false; - return this == FromVector(vector); - } - - /// - /// Returns a hash code for this instance. - /// - /// - /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. - /// - public override int GetHashCode() - { - unchecked - { - var hash = 17; - hash = hash * 23 + X.GetHashCode(); - hash = hash * 23 + Y.GetHashCode(); - hash = hash * 23 + Z.GetHashCode(); - return hash; - } - } - - /// - /// Implements the operator ==. - /// - /// The left . - /// The right . - /// - /// The result of the operator. - /// - public static bool operator ==(Vector3 left, Vector3 right) - { - return left.SequenceEqual(right); - } - - /// - /// Implements the operator !=. - /// - /// The left . - /// The right . - /// - /// The result of the operator. - /// - public static bool operator !=(Vector3 left, Vector3 right) - { - return !left.SequenceEqual(right); - } - } -} \ No newline at end of file diff --git a/SharpMath/Geometry/Vector4.cs b/SharpMath/Geometry/Vector4.cs deleted file mode 100644 index 4e59cd6..0000000 --- a/SharpMath/Geometry/Vector4.cs +++ /dev/null @@ -1,297 +0,0 @@ -// Vector4.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; - -// ReSharper disable NonReadonlyMemberInGetHashCode - -namespace SharpMath.Geometry -{ - /// - /// Represents a four-dimensional vector with a homogeneous coordinate. - /// - [Serializable] - public struct Vector4 : IVector, IEquatable, IEnumerable - { - /// - /// Initializes a new instance of the class. - /// - /// The value of the X-coordinate (X2 in mathematic coordinate systems). - /// The value of the Y-coordinate (X3 in mathematic coordinate systems). - /// The value of the Z-coordinate (X1 in mathematic coordinate systems). - /// The value of the homogeneous coordinate. - public Vector4(double x, double y, double z, double w) - { - X = x; - Y = y; - Z = z; - W = w; - } - - /// - /// Initializes a new instance of the class. - /// - /// The existing to copy. - public Vector4(Vector4 vector) - { - X = vector.X; - Y = vector.Y; - Z = vector.Z; - W = vector.W; - } - - /// - /// Gets or sets the value of the X-coordinate (X2 in mathematic coordinate systems). - /// - public double X { get; set; } - - /// - /// Gets or sets the value of the Y-coordinate (X3 in mathematic coordinate systems). - /// - public double Y { get; set; } - - /// - /// Gets or sets the value of the Z-coordinate (X1 in mathematic coordinate systems). - /// - public double Z { get; set; } - - /// - /// Gets or sets the value of the homogenous coordinate. - /// - public double W { get; set; } - - /// - /// A with all values set to zero. - /// - public static Vector4 Zero => new Vector4(); - - /// - /// A unit with all values set to one. - /// - public static Vector4 One => new Vector4(1, 1, 1, 1); - - /// - /// A unit with coordinates 1, 0, 0, 0. - /// - public static Vector4 UnitX => new Vector4(1, 0, 0, 0); - - /// - /// A unit with coordinates 0, 1, 0, 0. - /// - public static Vector4 UnitY => new Vector4(0, 1, 0, 0); - - /// - /// A unit with coordinates 0, 0, 1, 0. - /// - public static Vector4 UnitZ => new Vector4(0, 0, 1, 0); - - /// - /// A unit with coordinates 0, 0, 0, 1. - /// - public static Vector4 UnitW => new Vector4(0, 0, 1, 0); - - public IEnumerator GetEnumerator() - { - for (uint i = 0; i < 4; i++) yield return this[i]; - } - - IEnumerator IEnumerable.GetEnumerator() - { - for (uint i = 0; i < 4; i++) yield return this[i]; - } - - public bool Equals(Vector4 other) - { - return this == other; - } - - /// - /// Gets or sets the value of the coordinate at the specified index. - /// - /// The index. - /// The value of the coordinate at the specified index. - public double this[uint index] - { - get - { - switch (index) - { - case 0: - return X; - case 1: - return Y; - case 2: - return Z; - case 3: - return W; - default: - throw new IndexOutOfRangeException("The index must be between 0 and 3."); - } - } - set - { - switch (index) - { - case 0: - X = value; - break; - case 1: - Y = value; - break; - case 2: - Z = value; - break; - case 3: - W = value; - break; - default: - throw new IndexOutOfRangeException("The index must be between 0 and 3."); - } - } - } - - /// - /// Gets the dimension of the . - /// - public uint Dimension => 4; - - /// - /// Gets the length of the . - /// - public double Magnitude => Math.Sqrt(SquareMagnitude); - - /// - /// Gets the squared length of the . - /// - public double SquareMagnitude - { - get - { - double result = 0; - for (uint i = 0; i < 4; ++i) - result += Math.Pow(this[i], 2); - return result; - } - } - - /// - /// Gets a value indicating whether the is normalized, or not. - /// - public bool IsNormalized => Magnitude.IsApproximatelyEqualTo(1); - - /// - /// Gets a value indicating whether the has all of its components set to zero, or not. - /// - public bool IsZero => this.All(c => FloatingNumber.AreApproximatelyEqual(c, 0)); - - /// - /// Gets the LaTeX-string representing this vector graphically. - /// - public string ToLaTeXString() - { - return @"\left( \begin{array}{c} " + this[0] + @" \\ " + this[1] + @" \\ " + this[2] + @" \\ " + this[3] + - @" \end{array} \right)"; - } - - /// - /// Generates a from an object implementing the interface, if the - /// dimension is correct. - /// - /// The to generate a from. - /// The generated . - /// The dimension of the given vector is invalid. It must be 4. - public static Vector4 FromVector(IVector vector) - { - if (vector.Dimension != 4) - throw new ArgumentException("The dimension of the given vector is invalid. It must be 4."); - return new Vector4(vector[0], vector[1], vector[2], vector[3]); - } - - /// - /// Transforms the specified with the specified . - /// - /// The that should be transformed. - /// The transformation . - /// The transformed . - public static Vector4 Transform(Vector4 vector, Matrix4x4 matrix) - { - return matrix * vector; - } - - /// - /// Returns a that represents this instance. - /// - /// - /// A that represents this instance. - /// - public override string ToString() - { - return $"X: {this[0]}, Y: {this[1]}, Z: {this[2]}, W: {this[3]}"; - } - - /// - /// Determines whether the specified , is equal to this instance. - /// - /// The to compare with this instance. - /// - /// true if the specified is equal to this instance; otherwise, false. - /// - public override bool Equals(object obj) - { - if (obj is Vector4) - return this == (Vector4) obj; - var vector = obj as IVector; - if (Dimension != vector?.Dimension) - return false; - return this == FromVector(vector); - } - - /// - /// Returns a hash code for this instance. - /// - /// - /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. - /// - public override int GetHashCode() - { - unchecked - { - var hash = 17; - hash = hash * 23 + X.GetHashCode(); - hash = hash * 23 + Y.GetHashCode(); - hash = hash * 23 + Z.GetHashCode(); - hash = hash * 23 + W.GetHashCode(); - return hash; - } - } - - /// - /// Implements the operator ==. - /// - /// The left . - /// The right . - /// - /// The result of the operator. - /// - public static bool operator ==(Vector4 left, Vector4 right) - { - return left.SequenceEqual(right); - } - - /// - /// Implements the operator !=. - /// - /// The left . - /// The right . - /// - /// The result of the operator. - /// - public static bool operator !=(Vector4 left, Vector4 right) - { - return !left.SequenceEqual(right); - } - } -} \ No newline at end of file diff --git a/SharpMath/Geometry/VectorUtils.cs b/SharpMath/Geometry/VectorUtils.cs deleted file mode 100644 index 9420493..0000000 --- a/SharpMath/Geometry/VectorUtils.cs +++ /dev/null @@ -1,289 +0,0 @@ -// VectorUtils.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -using System; -using System.Diagnostics; - -namespace SharpMath.Geometry -{ - /// - /// Defines static methods and extensions for working with instances. - /// - public static class VectorUtils - { - /// - /// Adds two instances. - /// - /// The first . - /// The second . - /// The resulting . - public static T Add(this T firstVector, T secondVector) where T : IVector, new() - { - var resultVector = new T(); - for (uint i = 0; i < resultVector.Dimension; ++i) - resultVector[i] = firstVector[i] + secondVector[i]; - return resultVector; - } - - /// - /// Calculates the angle between two instances. - /// - /// The first . - /// The second . - /// The angle between the current and the specified instance. - /// Cannot calculate the angle between two vectors, if one is zero. - public static double Angle(this T first, T second) where T : IVector - { - if (first.IsZero || second.IsZero) - throw new InvalidOperationException("Cannot calculate the angle between two vectors, if one is zero."); - return Math.Acos(first.DotProduct(second) / (first.Magnitude * second.Magnitude)); - } - - /// - /// Represents an as a horizontal whose column count is equal to its - /// dimension. - /// - /// The represented as horizontal . - public static TOut AsHorizontalMatrix(this IVector vector) where TOut : IMatrix, new() - { - var matrix = new TOut(); - if (matrix.RowCount != 1 && matrix.ColumnCount != vector.Dimension) - throw new ArgumentException( - $"Type parameter TOut is not an adequate IMatrix-type as its constraints do not fit those of the resulting matrix. The constraints must be 1x{vector.Dimension}."); - - for (uint i = 0; i < vector.Dimension; ++i) - matrix[0, i] = vector[i]; - return matrix; - } - - /// - /// Represents an as a vertical whose row count is equal to its - /// dimension. - /// - /// The represented as vertical . - public static TOut AsVerticalMatrix(this IVector vector) where TOut : IMatrix, new() - { - var matrix = new TOut(); - if (matrix.RowCount != vector.Dimension && matrix.ColumnCount != 1) - throw new ArgumentException( - $"Type parameter TOut is not an adequate IMatrix-type as its constraints do not fit those of the resulting matrix. The constraints must be {vector.Dimension}x1."); - - for (uint i = 0; i < vector.Dimension; ++i) - matrix[i, 0] = vector[i]; - return matrix; - } - - /// - /// Determines whether two instances are orthogonal to each other, or not. - /// - /// The first . - /// The second . - /// true, if the instances are orthogonal to each other, otherwise false. - public static bool CheckForOrthogonality(this T first, T second) where T : IVector - { - return !first.IsZero && !second.IsZero && - FloatingNumber.AreApproximatelyEqual(DotProduct(first, second), 0); - } - - /// - /// Determines whether two instances are orthonormal to each other, or not. - /// - /// The first . - /// The second . - /// true, if the instances are orthonormal to each other, otherwise false. - public static bool CheckForOrthonormality(this T first, T second) where T : IVector - { - return first.CheckForOrthogonality(second) && first.IsNormalized && second.IsNormalized; - } - - /// - /// Determines whether two instances are parallel to each other, or not. - /// - /// The first . - /// The second . - /// true if the instances are parallel to each other, otherwise false. - public static bool CheckForParallelism(this T first, T second) where T : IVector - { - if (first.IsZero || second.IsZero) - return false; - - double firstResult = 0; - for (uint i = 0; i < first.Dimension; ++i) - if (i == 0) - { - firstResult = second[i] / first[i]; - } - else - { - if (!FloatingNumber.AreApproximatelyEqual(second[i] / first[i], firstResult)) - return false; - } - - return true; - } - - /// - /// Creates a new object that is a copy of the current instance. - /// - /// - /// A new object that is a copy of this instance. - /// - public static T Clone(this T vector) where T : IVector, new() - { - var cloneVector = new T(); - for (uint i = 0; i < vector.Dimension; ++i) - cloneVector[i] = vector[i]; - return cloneVector; - } - - /// - /// Converts an into an of another dimension. - /// - /// The type that the should be converted to. - /// The converted into the given type. - public static TOut Convert(this IVector vector) where TOut : IVector, new() - { - var resultVector = new TOut(); - if (resultVector.Dimension == vector.Dimension) - Debug.Print( - $"Vector conversion method (Vector{vector.Dimension}.To()) is currently used to convert a vector into one of the same dimension. Please check if this has been your intention."); - for (uint i = 0; i < Math.Min(vector.Dimension, resultVector.Dimension); ++i) - resultVector[i] = vector[i]; - return resultVector; - } - - /// - /// Calculates the distance between two instances that are the position s - /// of two points. - /// - /// The source . - /// The target . - /// The distance between the two instances. - public static double Distance(this T source, T target) where T : IVector, new() - { - return Subtract(source, target).Magnitude; - } - - /// - /// Divides a by multipling it with the reciprocal of the scalar. - /// - /// The . - /// The scalar whose reciprocal will be calculated. - /// The resulting . - public static T Divide(this T vector, double scalar) where T : IVector, new() - { - var resultVector = new T(); - for (uint i = 0; i < resultVector.Dimension; ++i) - resultVector[i] = vector[i] * (1 / scalar); - return resultVector; - } - - /// - /// Calculates the dot product of two instances. - /// - /// The first . - /// The second . - /// The calculated scalar as a . - public static double DotProduct(this T first, T second) where T : IVector - { - double result = 0; - for (uint i = 0; i < first.Dimension; ++i) - result += first[i] * second[i]; - return result; - } - - /// - /// Linearly interpolates between two instances. - /// - /// The source point. - /// The target point. - /// The fraction. - /// The position of the new point. - public static T Lerp(this T current, T target, double interpolant) where T : IVector, new() - { - if (interpolant > 1) - interpolant = 1; - else if (interpolant < 0) - interpolant = 0; - return LerpUnclamped(current, target, interpolant); - } - - /// - /// Linearly interpolates between two instances. - /// - /// The source point. - /// The target point. - /// The interpolant factor. - /// The position of the new point. - public static T LerpUnclamped(this T current, T target, double interpolant) where T : IVector, new() - { - // source + (target - source) * fraction - return Add(current, Multiply(Subtract(target, current), interpolant)); - } - - /// - /// Moves the source point in a straight line towards a target point by adding the given distance delta and returns - /// its new position. - /// - /// The source point. - /// The target point. - /// The distance delta that this source point is moved by in all directions. - /// The position of the new point. - public static T MoveTowards(this T source, T target, double maxDistanceDelta) where T : IVector, new() - { - return source.LerpUnclamped(target, maxDistanceDelta / source.Distance(target)); - } - - /// - /// Multiplies a with a specified scalar. - /// - /// The . - /// The scalar. - /// The resulting . - public static T Multiply(this T vector, double scalar) where T : IVector, new() - { - var resultVector = new T(); - for (uint i = 0; i < resultVector.Dimension; ++i) - resultVector[i] = vector[i] * scalar; - return resultVector; - } - - /// - /// Negates the specified . - /// - /// The negated . - public static T Negate(this T vector) where T : IVector, new() - { - var resultVector = new T(); - for (uint i = 0; i < vector.Dimension; ++i) - resultVector[i] = -vector[i]; - return resultVector; - } - - /// - /// Calculates the normalized of the . - /// - /// The normalized . - public static T Normalize(this T vector) where T : IVector, new() - { - var resultVector = new T(); - for (uint i = 0; i < vector.Dimension; ++i) - resultVector[i] = vector[i] / vector.Magnitude; - return resultVector; - } - - /// - /// Subtracts two instances. - /// - /// The first . - /// The second . - /// The resulting . - public static T Subtract(this T firstVector, T secondVector) where T : IVector, new() - { - var resultVector = new T(); - for (uint i = 0; i < resultVector.Dimension; ++i) - resultVector[i] = firstVector[i] - secondVector[i]; - return resultVector; - } - } -} \ No newline at end of file diff --git a/SharpMath/MathHelper.cs b/SharpMath/MathHelper.cs deleted file mode 100644 index 5d598b6..0000000 --- a/SharpMath/MathHelper.cs +++ /dev/null @@ -1,70 +0,0 @@ -// MathHelper.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -using System; - -// ReSharper disable InconsistentNaming - -namespace SharpMath -{ - /// - /// Provides constants and methods for logarithmic, trigonometric and other useful mathematical functions. - /// - public static class MathHelper - { - /// - /// Represents the base of the natural logarithm. - /// - public const float E = (float) Math.E; - - /// - /// Represents the base-10 logarithm of e, the base of natural logarithms. - /// - public const float Log10E = 0.4342945f; - - /// - /// Represents the base-2 logarithm of e, the base of natural logarithms. - /// - public const float Log2E = 1.442695f; - - /// - /// Represents the ratio of the circumference of a circle to its diameter, specified by the constant, π. - /// - public const float Pi = (float) Math.PI; - - /// - /// Represents over 2 (90 degrees). - /// - public const float PiOver2 = (float) (Math.PI / 2.0); - - /// - /// Represents over 4 (45 degrees). - /// - public const float PiOver4 = (float) (Math.PI / 4.0); - - /// - /// Represents 2 (360 degrees). - /// - public const float TwoPi = (float) (Math.PI * 2.0); - - /// - /// Converts degrees to radians. - /// - /// The angle in degrees. - /// The angle in radians. - public static double DegreesToRadians(double degrees) - { - return degrees * (Math.PI / 180); - } - - /// - /// Converts radians to degrees. - /// - /// The angle in radians. - /// The angle in degrees. - public static double RadiansToDegrees(double radians) - { - return radians * (180 / Math.PI); - } - } -} \ No newline at end of file diff --git a/SharpMath/Presentation/Canvas2D.cs b/SharpMath/Presentation/Canvas2D.cs deleted file mode 100644 index 1154ea1..0000000 --- a/SharpMath/Presentation/Canvas2D.cs +++ /dev/null @@ -1,298 +0,0 @@ -// Canvas2D.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Drawing; -using System.Drawing.Drawing2D; -using System.Globalization; -using System.Linq; -using System.Windows.Forms; -using SharpMath.Geometry; - -namespace SharpMath.Presentation -{ - public sealed class Canvas2D : Control - { - private bool _isMouseDown; - private PointF _lastOrigin; - private Point _previousLocation; - public BindingList Functions = new BindingList(); - - public Canvas2D() - { - DoubleBuffered = true; - - GridSize = new SizeF(20, 20); - GridOrigin = PointF.Empty; - - Font = new Font("Segoe UI", 8f, FontStyle.Regular); - ForeColor = Color.Gray; - - Paint += DrawCanvas; - Resize += CanvasResized; - MouseWheel += HandleMouseWheel; - - MouseDown += HandleMouseDown; - MouseMove += HandleMouseMove; - MouseUp += HandleMouseUp; - } - - public PointF GridOrigin { get; set; } - public SizeF GridScale { get; set; } - - public SizeF GridSize { get; set; } - public bool TrackingLines { get; set; } - public bool ValueIndicator { get; set; } - public BindingList Vectors { get; } = new BindingList(); - - private void CanvasResized(object sender, EventArgs e) - { - Invalidate(); - } - - private void DrawCanvas(object sender, PaintEventArgs e) - { - if (GridOrigin == PointF.Empty || DesignMode) - GridOrigin = new PointF(Width / 2, Height / 2); - - _lastOrigin = GridOrigin; - - var g = e.Graphics; - g.FillRectangle(Brushes.White, ClientRectangle); - - DrawCanvasLines(g); - DrawVertices(g); - DrawFunctions(g); - DrawFeatures(g); - } - - private void DrawCanvasLines(Graphics g) - { - #region BackgroundLines - - // Draw horizontal lines - for (var i = 0f; i <= Height; i += GridSize.Height) - { - var startPoint = Height / 2 + (GridOrigin.Y - Height / 2) % GridSize.Height; - g.DrawLine(Pens.LightGray, new PointF(0, startPoint + i), new PointF(Width, startPoint + i)); - g.DrawLine(Pens.LightGray, new PointF(0, startPoint - i), new PointF(Width, startPoint - i)); - } - - // Draw vertical lines - for (var i = 0f; i <= Width; i += GridSize.Width) - { - var startPoint = Width / 2 + (GridOrigin.X - Width / 2) % GridSize.Width; - g.DrawLine(Pens.LightGray, new PointF(startPoint + i, 0), new PointF(startPoint + i, Height)); - g.DrawLine(Pens.LightGray, new PointF(startPoint - i, 0), new PointF(startPoint - i, Height)); - } - - #endregion - - #region AxisScale - - // Draw vertical axis scale - var scaleVariantY = GetNearestScale(Height / GridSize.Height / 8.0); - - for (var i = 0.0; i <= Height; i += GridSize.Height * scaleVariantY) - { - var startPoint = Height / 2 + (GridOrigin.Y - Height / 2) % (GridSize.Height * scaleVariantY); - var value = Math.Round((GridOrigin.Y - startPoint) / GridSize.Height, 1); - var valueOffset = i / GridSize.Height; - - g.DrawString(Math.Round(value - valueOffset, 2).ToString(), Font, Brushes.Black, - new PointF(GridOrigin.X + GridSize.Width / 10, (float) (startPoint + i))); - g.DrawString(Math.Round(value + valueOffset, 2).ToString(), Font, Brushes.Black, - new PointF(GridOrigin.X + GridSize.Width / 10, (float) (startPoint - i))); - } - - // Draw horizontal axis scale - var scaleVariantX = GetNearestScale(Width / GridSize.Width / 8.0); - - for (var i = 0.0; i <= Width; i += GridSize.Width * scaleVariantX) - { - var startPoint = Width / 2 + (GridOrigin.X - Width / 2) % (GridSize.Width * scaleVariantX); - var value = GridOrigin.X > startPoint - ? -Math.Round((GridOrigin.X - startPoint) / GridSize.Width, 1) - : Math.Round(Math.Abs(GridOrigin.X - startPoint) / GridSize.Width, 1); - var valueOffset = i / GridSize.Width; - - if (!Math.Round(value + valueOffset, 2).IsApproximatelyEqualTo(0)) - g.DrawString(Math.Round(value + valueOffset, 2).ToString(CultureInfo.InvariantCulture), Font, - Brushes.Black, - new PointF((float) (startPoint + i), GridOrigin.Y + GridSize.Height / 10)); - - if (!Math.Round(value - valueOffset, 2).IsApproximatelyEqualTo(0)) - g.DrawString(Math.Round(value - valueOffset, 2).ToString(CultureInfo.InvariantCulture), Font, - Brushes.Black, - new PointF((float) (startPoint - i), GridOrigin.Y + GridSize.Height / 10)); - } - - #endregion - - #region AxisLines - - // TODO: Fix overflows - var axisPen = new Pen(new SolidBrush(Color.FromArgb(255, 245, 125, 0))); - g.DrawLine(axisPen, new PointF(0, GridOrigin.Y), new PointF(Width, GridOrigin.Y)); - g.DrawLine(axisPen, new PointF(GridOrigin.X, 0), new PointF(GridOrigin.X, Height)); - - #endregion - } - - private void DrawFeatures(Graphics g) - { - #region TrackingLines - - if (TrackingLines) - { - g.DrawLine(Pens.Gray, new PointF(_previousLocation.X, 0), new PointF(_previousLocation.X, Height)); - g.DrawLine(Pens.Gray, new PointF(0, _previousLocation.Y), new PointF(Width, _previousLocation.Y)); - } - - #endregion - - #region ValueIndicator - - if (!ValueIndicator) - return; - - var currentX = (_previousLocation.X - GridOrigin.X) / GridSize.Width; - var currentY = (GridOrigin.Y - _previousLocation.Y) / GridSize.Height; - - var resultString = $"{Math.Round(currentX, 2)} | {Math.Round(currentY, 2)}"; - var stringDimensions = g.MeasureString(resultString, Font); - - var toolTipRect = new Rectangle(_previousLocation.X + 15, _previousLocation.Y + 15, 80, 25); - g.FillRectangle(Brushes.White, toolTipRect); - g.DrawRectangle(Pens.LightGray, toolTipRect); - - g.DrawString(resultString, Font, Brushes.LightGray, - new PointF(_previousLocation.X + 15 + (80 - stringDimensions.Width) / 2, - _previousLocation.Y + 17 + (25 - stringDimensions.Height) / 2)); - - #endregion - } - - private void DrawFunctions(Graphics g) - { - foreach (var func in Functions) - { - var lastPos = PointF.Empty; - for (var i = 0; i < Width; i++) - try - { - var currentX = (-GridOrigin.X + i) / GridSize.Width; - var currentY = GridOrigin.Y - func.GetValue(currentX) * GridSize.Height; - var currentPos = new PointF(currentX * GridSize.Width + GridOrigin.X, (float) currentY); - - if (lastPos != PointF.Empty) - g.DrawLine(Pens.Black, lastPos, currentPos); - - lastPos = currentPos; - } - catch (OverflowException) - { - // Content will follow - } - } - } - - private void DrawVertices(Graphics g) - { - var vectorPen = new Pen(new SolidBrush(Color.FromArgb(255, 122, 125, 201))) {EndCap = LineCap.ArrowAnchor}; - foreach (var v in Vectors) - g.DrawLine(vectorPen, - GridOrigin, - new PointF(GridOrigin.X + (float) v.X * GridSize.Width, - GridOrigin.Y - (float) v.Y * GridSize.Height)); - } - - private double GetNearestScale(double value) - { - var scaleVariants = new List {10, 5, 2}; - var dimension = (int) Math.Floor(Math.Log10(Math.Abs(value)) + 1); - - if (dimension > 1) value = value / ((dimension - 1) * 10); - if (dimension == 0) value = value * ((dimension - 1) * 10); - if (dimension < 0) value = value * (Math.Abs(dimension) * Math.Pow(10, Math.Abs(dimension) + 1)); - value = Math.Abs(value); - - var normalizedValue = scaleVariants.Aggregate((x, y) => Math.Abs(x - value) < Math.Abs(y - value) ? x : y); - if (dimension == 1) return normalizedValue; - if (dimension > 1) return normalizedValue * ((dimension - 1) * 10); - if (dimension < 0) return normalizedValue / Math.Pow(10, Math.Abs(dimension) + 1); - if (dimension < 1) return normalizedValue / ((dimension + 1) * 10); - return 0; - } - - private void HandleMouseDown(object sender, MouseEventArgs e) - { - _isMouseDown = true; - _previousLocation = e.Location; - } - - private void HandleMouseMove(object sender, MouseEventArgs e) - { - if (TrackingLines || ValueIndicator) - Invalidate(); - - if (_isMouseDown) - { - var deltaX = e.X - _previousLocation.X; - var deltaY = e.Y - _previousLocation.Y; - - var newX = GridOrigin.X + deltaX; - var newY = GridOrigin.Y + deltaY; - - GridOrigin = new PointF(newX, newY); - Invalidate(); - } - - _previousLocation = e.Location; - } - - private void HandleMouseUp(object sender, MouseEventArgs e) - { - _isMouseDown = false; - } - - private void HandleMouseWheel(object sender, MouseEventArgs e) - { - // Needs work - if (e.Delta > 0) - { - GridSize = new SizeF((float) (GridSize.Width * 1.25), (float) (GridSize.Height * 1.25)); - GridOrigin = new PointF( - GridOrigin.X + - (e.X > _lastOrigin.X ? -1.25f * (e.X - _lastOrigin.X) : 1.25f * (_lastOrigin.X - e.X)), - GridOrigin.Y + - (e.Y > _lastOrigin.Y ? -1.25f * (e.Y - _lastOrigin.Y) : 1.25f * (_lastOrigin.Y - e.Y)) - ); - _lastOrigin = GridOrigin; - } - else - { - var newSize = new SizeF((float) (GridSize.Width / 1.25), (float) (GridSize.Height / 1.25)); - if (newSize.Width > 3 && newSize.Height > 3) - { - GridSize = newSize; - GridOrigin = new PointF( - GridOrigin.X + - (_lastOrigin.X + e.X > _lastOrigin.X - ? (e.X - _lastOrigin.X) / 1.25f - : (_lastOrigin.X - e.X) / -1.25f), - GridOrigin.Y + - (_lastOrigin.Y + e.Y > _lastOrigin.Y - ? (e.Y - _lastOrigin.Y) / 1.25f - : (_lastOrigin.Y - e.Y) / -1.25f) - ); - _lastOrigin = GridOrigin; - } - } - - Invalidate(); - } - } -} \ No newline at end of file diff --git a/SharpMath/Presentation/FunctionWrapper.cs b/SharpMath/Presentation/FunctionWrapper.cs deleted file mode 100644 index 649fc57..0000000 --- a/SharpMath/Presentation/FunctionWrapper.cs +++ /dev/null @@ -1,29 +0,0 @@ -// FunctionWrapper.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -using System; -using System.Collections.Generic; - -namespace SharpMath.Presentation -{ - public class FunctionWrapper - { - public FunctionWrapper(Func function) - { - Values = new Dictionary(); - BaseFunction = function; - } - - public Func BaseFunction { get; } - public Dictionary Values { get; } - - public double GetValue(double x) - { - if (Values.ContainsKey(x)) - return Values[x]; - var resultValue = BaseFunction(x); - Values.Add(x, resultValue); - return resultValue; - } - } -} \ No newline at end of file diff --git a/SharpMath/Properties/AssemblyInfo.cs b/SharpMath/Properties/AssemblyInfo.cs deleted file mode 100644 index 3a24fd8..0000000 --- a/SharpMath/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,44 +0,0 @@ -// AssemblyInfo.cs, 07.11.2019 -// Copyright (C) Dominic Beger 07.11.2019 - -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. - -[assembly: AssemblyTitle("SharpMath")] -[assembly: - AssemblyDescription( - "A C# math library supporting vectors, matrices, linear equation systems and expression parsing.")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("SharpMath")] -[assembly: AssemblyCopyright("Copyright © Dominic Beger (Trade/ProgTrade) 2015-2016")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. - -[assembly: ComVisible(true)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM - -[assembly: Guid("4706ae0f-30c8-460b-bbbf-5f189be4eb76")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] - -[assembly: AssemblyVersion("1.14.7.0")] -[assembly: AssemblyFileVersion("1.14.7.0")] \ No newline at end of file diff --git a/SharpMath/SharpMath.cd b/SharpMath/SharpMath.cd deleted file mode 100644 index 072b6de..0000000 --- a/SharpMath/SharpMath.cd +++ /dev/null @@ -1,316 +0,0 @@ - - - - - - AAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= - Algorithms.cs - - - - - - AAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAICAAAA= - Extensions.cs - - - - - - AAAABAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAA= - FloatingNumber.cs - - - - - - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAA= - FloatingNumberExtensions.cs - - - - - - CAAAJAAAAAAAAAAQBAACAAAAAAQAAAAAAAEAAgAAAAA= - MathHelper.cs - - - - - - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= - Equations\Exceptions\EquationNotSolvableException.cs - - - - - - AAAAAAAAAAAAAAAAAAAAAAQAAEAAAAAAAAAAAAAAAAA= - Equations\LinearEquation.cs - - - - - - AAAAAAAAAAAAAAAAAAAAAAAIEAAAAAAAAAAAAAAAAAA= - Equations\LinearEquationSystem.cs - - - - - - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAAAA= - Expressions\CharEx.cs - - - - - - AAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAQAAAAAAAAAAA= - Expressions\Parser.cs - - - - - - AAAEAAAAAAAAAIAAAAEAAgAAIAAAAAAAEQAAAAAAAAA= - Expressions\Token.cs - - - - - - AAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAQgAAAAAAA= - Expressions\Token.cs - - - - - - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= - Expressions\Exceptions\ParserException.cs - - - - - - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= - Geometry\Exceptions\DimensionException.cs - - - - - - AAAAAAAAACAABAAAAACgAAAAAAAAAAAAoAAAAQAAAAA= - Geometry\MatrixEnumerator.cs - - - - - - - ACZCQAAAAAAAAACAIAADCAAAAAAEACAAAAAAICAADBA= - Geometry\MatrixUtils.cs - - - - - - AAAAAAAAAQAAgBAEgAAAAAAAACAAAIAEAACAAQAYAjA= - Geometry\Polygon.cs - - - - - - - AAAAAAAAAAAAAAAACACAAAAAQAAAAAAAAAAAAAAAAgA= - Geometry\VariableMatrix.cs - - - - - - - AAIAQAAAAQAEACAAEYADAAAAQABAAABAgEAEAAEAABA= - Geometry\VectorUtils.cs - - - - - - AAgAADACAJABAACQAAASAAIAAAQAIAIAEABAAAJAAgA= - Presentation\Canvas2D.cs - - - - - - AAAAAAAAIAAAAAAAAAAgAAAAAAAAAAAAAAAgAAAAAAA= - Presentation\FunctionWrapper.cs - - - - - - AAAAIIAAAMAgAAAEgAAAAAAAAAAQIIAAAAAAAAAAgmA= - Geometry\Line2D.cs - - - - - - - AAAAIAAAAIAggAAEgCAAAAAAAAAAAIAAAAAAAAAAAiA= - Geometry\Line3D.cs - - - - - - - AACQAAAgAAAAABAA6CMAAAAAQQIASLAEAEAEgAgAAiQ= - Geometry\Matrix1x1.cs - - - - - - - AAAAAAAAAAAAABAACAAAAAAAQAAAABAEAAAAAAAAAgA= - Geometry\Matrix1x3.cs - - - - - - - AAAAAAAAAAAAABAACAAAAAAAQAAAABAEAAAAAAAAAgA= - Geometry\Matrix1x4.cs - - - - - - - AACQAAAgAAAAABAA6CMAACAAQQIASLAEAEAEgAgAAiQ= - Geometry\Matrix2x2.cs - - - - - - - AABAAAAAAAAAABAACAAAACAAQAAAABAEAAAAAAAAAgA= - Geometry\Matrix3x1.cs - - - - - - - AATQAAAgAAAAAJAC6CMAACAAQQIASLAMAEAEgAgAAiQ= - Geometry\Matrix3x3.cs - - - - - - - AABAAAAAAAAAABAACAAAACAAQAAAABAEAAAAAAgAAgA= - Geometry\Matrix4x1.cs - - - - - - - AATQAAAgABAAAJAC6CMAACAAQQIBSLAMAEAEgAgAAiQ= - Geometry\Matrix4x4.cs - - - - - - - AAAAAAAAAAAAABAEyAEAQAAAAAAAAIAEAAAAAAAAYiA= - Geometry\Point2D.cs - - - - - - - AAAAAAAAAAAAABAEyAEAQAAAAAAAAIAEAAAAAAAAYiE= - Geometry\Point3D.cs - - - - - - - AAAAAAAAAAAAAAAABkAAIAQAAAAAAAAiAAAAAAAAAAA= - Geometry\ProjectionData.cs - - - - - - AAAAYAIAAQAAgBAE6AEAQQQAAQBAQIhEiECAAQGAcuI= - Geometry\Vector2.cs - - - - - - - AQAAIgIAAAAAgBAE6AEAQQQAAQBAQIgEiBCAAQGAYqM= - Geometry\Vector3.cs - - - - - - - AAAAIgAAAAAAABAEiAABQQQAAQAAQIgEgAAAAAGIYqE= - Geometry\Vector4.cs - - - - - - - AAAAAAAAAAAAAAAACAAAAAAAQAAAAAAAAAAAAAAAAgA= - Geometry\IMatrix.cs - - - - - - AACAAAAgAAAAAAAACCAAAAAAAAIASDAAAAAAgAAAAAQ= - Geometry\ISquareMatrix.cs - - - - - - AAAQAAAAAAAAAAAAACIAAAAAAAAAAAAAAAAEAAgAAAA= - Geometry\ISquareMatrix.cs - - - - - - AAAAAAAAAAAAAAAACAAAAAAAAAAAQAgAgAAAAAEAACA= - Geometry\IVector.cs - - - - - - AAAAEAAAAAAAAAAAAEAAAAIAAAAAAAAAAAAAAAAAIAA= - Expressions\TokenType.cs - - - - \ No newline at end of file diff --git a/SharpMath/SharpMath.csproj b/SharpMath/SharpMath.csproj deleted file mode 100644 index bdafff6..0000000 --- a/SharpMath/SharpMath.csproj +++ /dev/null @@ -1,97 +0,0 @@ - - - - - Debug - AnyCPU - {4706AE0F-30C8-460B-BBBF-5F189BE4EB76} - Library - Properties - SharpMath - SharpMath - v4.0 - 512 - - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Component - - - - - - - - - - - - - - \ No newline at end of file diff --git a/SharpMath/SharpMath.csproj.DotSettings b/SharpMath/SharpMath.csproj.DotSettings deleted file mode 100644 index 827beb4..0000000 --- a/SharpMath/SharpMath.csproj.DotSettings +++ /dev/null @@ -1,2 +0,0 @@ - - Experimental \ No newline at end of file diff --git a/benchmarks/Directory.Build.props b/benchmarks/Directory.Build.props new file mode 100644 index 0000000..f3304d8 --- /dev/null +++ b/benchmarks/Directory.Build.props @@ -0,0 +1,9 @@ + + + + + + true + $(NoWarn);CS1591;CA1822;CA1515 + + diff --git a/benchmarks/SharpMath.Benchmarks/CanvasBenchmarks.cs b/benchmarks/SharpMath.Benchmarks/CanvasBenchmarks.cs new file mode 100644 index 0000000..c4aa3d5 --- /dev/null +++ b/benchmarks/SharpMath.Benchmarks/CanvasBenchmarks.cs @@ -0,0 +1,39 @@ +using BenchmarkDotNet.Attributes; +using SharpMath.Canvas; +using SharpMath.Geometry; + +namespace SharpMath.Benchmarks; + +[MemoryDiagnoser] +public class CanvasBenchmarks +{ + private readonly Canvas2D _canvas = new(); + private readonly NullDrawingContext _context = new(); + + [GlobalSetup] + public void Setup() + { + _canvas.AddFunction(x => x * x * x / 10); + _canvas.AddFunction("1.5sin(x)"); + _canvas.AddFunction(Math.Tan); + _canvas.AddVector(new Vector2(4, 3)); + _canvas.AddPolygon(new Polygon(new Point2D(2, -2), new Point2D(6, -2), new Point2D(4, -5))); + } + + [Benchmark] + public void Render800x600() => _canvas.Render(_context, 800, 600); + + private sealed class NullDrawingContext : IDrawingContext + { + public void Clear(Color color) { } + public void DrawLine(CanvasPoint startPoint, CanvasPoint endPoint, Stroke stroke) { } + public void DrawPolyline(ReadOnlySpan points, Stroke stroke) { } + public void DrawPolygon(ReadOnlySpan points, Stroke? stroke, Color? fill) { } + public void DrawRectangle(CanvasRect rect, Stroke? stroke, Color? fill) { } + public void DrawEllipse(CanvasPoint center, double radiusX, double radiusY, Stroke? stroke, Color? fill) { } + public void DrawText(string text, CanvasPoint position, FontSpec font, Color color, TextAlignment alignment) { } + public TextSize MeasureText(string text, FontSpec font) => new(text.Length * 6, 12); + public void PushClip(CanvasRect rect) { } + public void PopClip() { } + } +} diff --git a/benchmarks/SharpMath.Benchmarks/ExpressionBenchmarks.cs b/benchmarks/SharpMath.Benchmarks/ExpressionBenchmarks.cs new file mode 100644 index 0000000..f7c9a21 --- /dev/null +++ b/benchmarks/SharpMath.Benchmarks/ExpressionBenchmarks.cs @@ -0,0 +1,21 @@ +using BenchmarkDotNet.Attributes; +using SharpMath.Expressions; + +namespace SharpMath.Benchmarks; + +[MemoryDiagnoser] +public class ExpressionBenchmarks +{ + private const string Text = "3*4^2+sin(1)/2-7*(2+cos(0))"; + private readonly MathExpression _parsed = MathExpression.Parse("3*x^2+sin(x)/2-7*(2+cos(x))"); + private readonly Func _function = MathExpression.Parse("3*x^2+sin(x)/2-7*(2+cos(x))").ToFunc("x"); + + [Benchmark] + public double ParseAndEvaluate() => MathExpression.Parse(Text).Evaluate(); + + [Benchmark] + public double EvaluateParsed() => _parsed.Evaluate("x", 1.5); + + [Benchmark] + public double EvaluateFunc() => _function(1.5); +} diff --git a/benchmarks/SharpMath.Benchmarks/GeometryBenchmarks.cs b/benchmarks/SharpMath.Benchmarks/GeometryBenchmarks.cs new file mode 100644 index 0000000..c7539c2 --- /dev/null +++ b/benchmarks/SharpMath.Benchmarks/GeometryBenchmarks.cs @@ -0,0 +1,106 @@ +using BenchmarkDotNet.Attributes; +using SharpMath.Geometry; + +namespace SharpMath.Benchmarks; + +[MemoryDiagnoser] +public class VectorBenchmarks +{ + private readonly Vector3 _a = new(1, 2, 3); + private readonly Vector3 _b = new(-2, 0.5, 4); + + [Benchmark] + public Vector3 AddAndScale() => _a + _b * 2; + + [Benchmark] + public Vector3 Normalize() => _a.Normalize(); + + [Benchmark] + public double Length() => _a.Length; + + [Benchmark] + public Vector3 Cross() => _a.Cross(_b); + + [Benchmark] + public bool Equality() => _a == _b; + + [Benchmark] + public bool IsZero() => _a.IsZero; + + [Benchmark] + public int HashCode() => _a.GetHashCode(); + + [Benchmark] + public bool IsParallelTo() => _a.IsParallelTo(_b); +} + +[MemoryDiagnoser] +public class MatrixBenchmarks +{ + private readonly Matrix2x2 _m2 = new(8, 3, 4, 2); + private readonly Matrix3x3 _m3 = new(2, 5, 2, 3, -3, 1, 1, 4, -4); + private readonly Matrix4x4 _m4 = new(1, 0, 0, 1, 1, 2, 3, 2, 2, 3, 4, 0, 1, 2, -1, -2); + private readonly Matrix4x4 _rotation = Matrix4x4.Rotation(0.3, 0.5, 0.7); + private readonly Vector3 _vector = new(1, 2, 3); + + [Benchmark] + public double Determinant2x2() => _m2.Determinant; + + [Benchmark] + public double Determinant3x3() => _m3.Determinant; + + [Benchmark] + public double Determinant4x4() => _m4.Determinant; + + [Benchmark] + public Matrix2x2 Adjugate2x2() => _m2.Adjugate(); + + [Benchmark] + public Matrix3x3 Adjugate3x3() => _m3.Adjugate(); + + [Benchmark] + public Matrix4x4 Adjugate4x4() => _m4.Adjugate(); + + [Benchmark] + public Matrix2x2 Inverse2x2() => _m2.Inverse(); + + [Benchmark] + public Matrix3x3 Inverse3x3() => _m3.Inverse(); + + [Benchmark] + public Matrix4x4 Inverse4x4() => _m4.Inverse(); + + [Benchmark] + public bool IsSingular4x4() => _m4.IsSingular; + + [Benchmark] + public Matrix3x3 Multiply3x3() => _m3 * _m3; + + [Benchmark] + public Matrix4x4 Multiply4x4() => _m4 * _rotation; + + [Benchmark] + public Vector3 TransformVector() => _rotation * _vector; + + [Benchmark] + public bool Equality4x4() => _m4 == _rotation; + + [Benchmark] + public int HashCode4x4() => _m4.GetHashCode(); + + [Benchmark] + public bool IsOrthogonal4x4() => _rotation.IsOrthogonal; +} + +[MemoryDiagnoser] +public class PolygonBenchmarks +{ + private readonly Polygon _polygon = new(Enumerable.Range(0, 64) + .Select(i => new Point2D(Math.Cos(i * Math.Tau / 64) * (i % 2 == 0 ? 10 : 7), Math.Sin(i * Math.Tau / 64) * (i % 2 == 0 ? 10 : 7)))); + + [Benchmark] + public bool Contains() => _polygon.Contains(new Point2D(1, 2)); + + [Benchmark] + public double Area() => _polygon.Area; +} diff --git a/benchmarks/SharpMath.Benchmarks/Program.cs b/benchmarks/SharpMath.Benchmarks/Program.cs new file mode 100644 index 0000000..f9d6cf3 --- /dev/null +++ b/benchmarks/SharpMath.Benchmarks/Program.cs @@ -0,0 +1,6 @@ +// Run all benchmarks: dotnet run -c Release --project benchmarks/SharpMath.Benchmarks -- --filter "*" +// See doc/benchmarks.md for results and the comparison with SharpMath 1.x. + +using BenchmarkDotNet.Running; + +BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); diff --git a/benchmarks/SharpMath.Benchmarks/SharpMath.Benchmarks.csproj b/benchmarks/SharpMath.Benchmarks/SharpMath.Benchmarks.csproj new file mode 100644 index 0000000..d0915cc --- /dev/null +++ b/benchmarks/SharpMath.Benchmarks/SharpMath.Benchmarks.csproj @@ -0,0 +1,15 @@ + + + + Exe + + + + + + + + + + + diff --git a/benchmarks/SharpMath.Benchmarks/SolverBenchmarks.cs b/benchmarks/SharpMath.Benchmarks/SolverBenchmarks.cs new file mode 100644 index 0000000..941f619 --- /dev/null +++ b/benchmarks/SharpMath.Benchmarks/SolverBenchmarks.cs @@ -0,0 +1,33 @@ +using BenchmarkDotNet.Attributes; +using SharpMath.Equations; +using SharpMath.Geometry; + +namespace SharpMath.Benchmarks; + +[MemoryDiagnoser] +public class LinearSystemBenchmarks +{ + private MatrixMxN _matrix = null!; + private double[] _rightHandSide = null!; + private LinearEquationSystem _system = null!; + + [Params(3, 10, 50)] + public int Size { get; set; } + + [GlobalSetup] + public void Setup() + { + var random = new Random(42); + _system = new LinearEquationSystem(Enumerable.Range(0, Size).Select(row => + new LinearEquation(Enumerable.Range(0, Size).Select(column => + column == row ? Size + random.NextDouble() : random.NextDouble()), random.NextDouble()))); + _matrix = new MatrixMxN(Size, Size, _system.Equations.SelectMany(equation => equation.Coefficients).ToArray()); + _rightHandSide = _system.Equations.Select(equation => equation.Result).ToArray(); + } + + [Benchmark] + public double[] Solve() => _system.Solve(); + + [Benchmark] + public double[] MatrixSolve() => _matrix.Solve(_rightHandSide); +} diff --git a/doc/benchmarks.md b/doc/benchmarks.md new file mode 100644 index 0000000..e16f913 --- /dev/null +++ b/doc/benchmarks.md @@ -0,0 +1,99 @@ +# Benchmarks + +Measured with [BenchmarkDotNet](https://benchmarkdotnet.org/) 0.15.8 on 2026-10-11: + +``` +Linux Ubuntu 22.04.5 LTS, 13th Gen Intel Core i7-13700H (laptop), 20 logical cores +.NET SDK 10.0.401, .NET 10.0.12, X64 RyuJIT x86-64-v3 +``` + +**1.x** is the last 1.x commit (`66339fd`) compiled unchanged for `net10.0`, measured with equivalent benchmarks. The 1.x +sources are not part of the repository, so that column cannot be reproduced from this branch. **2.0** is this branch: + +```shell +dotnet run -c Release --project benchmarks/SharpMath.Benchmarks -- --filter "*" +``` + +> **How to read the numbers.** The machine is a laptop with aggressive power management. Separate runs of the same +> benchmark varied by up to ±30 %, and one run that coincided with throttling was three times slower. The 10× to +> 1000× ratios below are robust. Where 1.x and 2.0 are close (polygon, parser), the numbers come from the default job, +> with 1.x and 2.0 run alternately twice; the averages are shown. The other rows come from `--job short` runs. +> Results below about 1 ns mean the JIT inlined the operation completely; they cannot be resolved more precisely. + +## Matrices + +| Benchmark | 1.x mean | 1.x allocated | 2.0 mean | 2.0 allocated | +|---|---:|---:|---:|---:| +| `Matrix2x2.Determinant` | 4.8 ns | 48 B | 0.3 ns | – | +| `Matrix3x3.Determinant` | 336 ns | 232 B | 0.3 ns | – | +| `Matrix4x4.Determinant` | 1,648 ns | 1,072 B | 2.7 ns | – | +| `Matrix2x2.Adjugate` | 111 ns | 288 B | 0.3 ns | – | +| `Matrix3x3.Adjugate` | 455 ns | 1,224 B | 1.6 ns | – | +| `Matrix4x4.Adjugate` | 5,344 ns | 6,016 B | 22 ns | – | +| `Matrix2x2.Inverse` ¹ | 140 ns | 384 B | 17 ns | – | +| `Matrix3x3.Inverse` ¹ | 493 ns | 1,584 B | 43 ns | – | +| `Matrix4x4.Inverse` ¹ | 946 ns | 3,744 B | 121 ns | – | +| `Matrix4x4.IsSingular` ² | 1,566 ns | 1,072 B | 87 ns | – | +| `Matrix3x3 * Matrix3x3` | 253 ns | 176 B | 4.1 ns | – | +| `Matrix4x4 * Matrix4x4` | 535 ns | 288 B | 23 ns | – | +| `Matrix4x4 * Vector3` | 177 ns | 288 B | 2.4 ns | – | +| `Matrix4x4 ==` | 42 ns | 608 B | 3.2 ns | – | +| `Matrix4x4.GetHashCode` | 47 ns | – | 40 ns | – | +| `Matrix4x4.IsOrthogonal` | 668 ns | 896 B | 57 ns | – | + +¹ In 1.x, `Inverse` returned the identity matrix instead of the inverse (see the CHANGELOG), so those numbers measure +a wrong result. In 2.0, `Inverse` also checks whether the matrix is singular. + +² 1.x compared the determinant with an absolute epsilon. 2.0 runs a pivoted LU decomposition, which also gives +correct results for badly scaled matrices. + +## Vectors + +| Benchmark | 1.x mean | 1.x allocated | 2.0 mean | 2.0 allocated | +|---|---:|---:|---:|---:| +| `a + b * 2` (`Vector3`) | 20 ns | – | 0.3 ns | – | +| `Vector3.Normalize` | 101 ns | – | 6.8 ns | – | +| `Vector3.Length` (`Magnitude`) | 31 ns | – | 0.3 ns | – | +| `Vector3 ==` | 24 ns | 192 B | 0.3 ns | – | +| `Vector3.IsZero` | 14 ns | 96 B | < 0.1 ns | – | +| `Vector3.GetHashCode` | 1.2 ns | – | 2.3 ns | – | +| `Vector3.IsParallelTo` (`CheckForParallelism`) ³ | 30 ns | 192 B | 18 ns | – | + +³ 1.x divided the components and was wrong for vectors with zero components. 2.0 normalizes both vectors, which +makes the result independent of their lengths. + +## Geometry and equations + +| Benchmark | 1.x mean | 1.x allocated | 2.0 mean | 2.0 allocated | +|---|---:|---:|---:|---:| +| `Polygon.Contains` (64 vertices) | 416 ns | 3,152 B | 322 ns | – | +| `Polygon.Area` (64 vertices) | 84 ns | – | 89 ns | – | +| `LinearEquationSystem.Solve`, n = 3 | 174 ns | 360 B | 139 ns | 432 B | +| `LinearEquationSystem.Solve`, n = 10 | 1,916 ns | 1,200 B | 945 ns | 2,080 B | +| `LinearEquationSystem.Solve`, n = 50 | 183,208 ns | 21,040 B | 50,521 ns | 41,600 B | +| `MatrixMxN.Solve`, n = 50 | n/a | | 47,256 ns | 21,152 B | + +In 2.0, `Polygon.Contains` also treats points on the border consistently as inside, and `Solve` uses partial +pivoting for numerical stability. `LinearEquationSystem.Solve` copies the coefficients into a matrix first, which +accounts for the extra allocation compared with `MatrixMxN.Solve`. + +## Expressions + +| Benchmark | 1.x mean | 1.x allocated | 2.0 mean | 2.0 allocated | +|---|---:|---:|---:|---:| +| Parse and evaluate `3*4^2+sin(1)/2-7*(2+cos(0))` | 2,013 ns | 3,272 B | 1,301 ns | 3,104 B | +| Evaluate a parsed expression with a variable | n/a ⁴ | | 51 ns | – | +| Call `ToFunc("x")` delegate | n/a ⁴ | | 51 ns | – | + +⁴ 1.x had no variables and no way to evaluate a parsed expression again; plotting `f(x)` from a string meant parsing +it once per point. + +## Canvas + +| Benchmark | 2.0 mean | 2.0 allocated | +|---|---:|---:| +| `Canvas2D.Render` into a no-op renderer, 800×600, three functions (including `tan`), a vector and a polygon | 375 µs | 2.2 KB | + +The remaining allocations are mostly the label strings and the clipped polygon. The 1.x canvas drew directly with +GDI+ inside a WinForms control and cannot be measured without a window. Its function sampling cached every sampled +value forever and allocated new GDI objects on every paint. diff --git a/doc/images/canvas-functions.png b/doc/images/canvas-functions.png new file mode 100644 index 0000000..0bb9a6d Binary files /dev/null and b/doc/images/canvas-functions.png differ diff --git a/doc/images/canvas-functions.svg b/doc/images/canvas-functions.svg new file mode 100644 index 0000000..76905a1 --- /dev/null +++ b/doc/images/canvas-functions.svg @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +-8 +-6 +-4 +-2 +2 +4 +6 +8 +-4 +-2 +0 +2 +4 + + + + + + + + + + + + + diff --git a/doc/images/canvas-geometry.png b/doc/images/canvas-geometry.png new file mode 100644 index 0000000..4c52c46 Binary files /dev/null and b/doc/images/canvas-geometry.png differ diff --git a/doc/images/canvas-geometry.svg b/doc/images/canvas-geometry.svg new file mode 100644 index 0000000..d8af0ce --- /dev/null +++ b/doc/images/canvas-geometry.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +-10 +-5 +5 +10 +-5 +0 +5 + + + + + + + + + + + + + +6.67 | 2 + + diff --git a/eng/CheckCoverage.cs b/eng/CheckCoverage.cs new file mode 100644 index 0000000..f328cff --- /dev/null +++ b/eng/CheckCoverage.cs @@ -0,0 +1,44 @@ +#:property GenerateDocumentationFile=true +#:property NoWarn=$(NoWarn);CS1591 + +// Fails if the line coverage of an assembly in a Cobertura report is below a threshold. +// Usage: dotnet run eng/CheckCoverage.cs -- = [...] + +using System.Globalization; +using System.Xml.Linq; + +if (args.Length < 2 || !double.TryParse(args[0], NumberStyles.Float, CultureInfo.InvariantCulture, out var threshold)) +{ + Console.Error.WriteLine("Usage: CheckCoverage = [...]"); + return 2; +} + +var failed = false; +foreach (var argument in args[1..]) +{ + var separator = argument.LastIndexOf('='); + if (separator <= 0 || separator == argument.Length - 1) + { + Console.Error.WriteLine($"Expected =, got '{argument}'."); + failed = true; + continue; + } + + var (path, assembly) = (argument[..separator], argument[(separator + 1)..]); + var package = XDocument.Load(path).Descendants("package") + .FirstOrDefault(element => (string?) element.Attribute("name") == assembly); + if (package is null) + { + Console.Error.WriteLine($"{assembly}: not found in {path}"); + failed = true; + continue; + } + + var coverage = 100 * double.Parse((string) package.Attribute("line-rate")!, CultureInfo.InvariantCulture); + var passed = coverage >= threshold; + failed |= !passed; + Console.WriteLine(string.Create(CultureInfo.InvariantCulture, + $"{assembly}: {coverage:F1} % line coverage (threshold {threshold:0.##} %) {(passed ? "OK" : "FAILED")}")); +} + +return failed ? 1 : 0; diff --git a/global.json b/global.json new file mode 100644 index 0000000..1d364c6 --- /dev/null +++ b/global.json @@ -0,0 +1,9 @@ +{ + "sdk": { + "version": "10.0.100", + "rollForward": "latestFeature" + }, + "test": { + "runner": "Microsoft.Testing.Platform" + } +} diff --git a/samples/Directory.Build.props b/samples/Directory.Build.props new file mode 100644 index 0000000..06810b5 --- /dev/null +++ b/samples/Directory.Build.props @@ -0,0 +1,9 @@ + + + + + + true + $(NoWarn);CS1591 + + diff --git a/samples/SharpMath.Samples.Canvas.Avalonia/MainWindow.cs b/samples/SharpMath.Samples.Canvas.Avalonia/MainWindow.cs new file mode 100644 index 0000000..ee74fc3 --- /dev/null +++ b/samples/SharpMath.Samples.Canvas.Avalonia/MainWindow.cs @@ -0,0 +1,72 @@ +using Avalonia.Controls; +using Avalonia.Layout; +using SharpMath.Canvas; +using SharpMath.Canvas.Avalonia; +using SharpMath.Expressions; +using SharpMath.Geometry; + +namespace SharpMath.Samples.Canvas.Avalonia; + +/// +/// Shows the canvas on Windows, Linux and macOS. Drag to pan, use the wheel to zoom, and enter expressions such as +/// 2x^2 - 3x to plot them. +/// +internal sealed class MainWindow : Window +{ + private readonly TextBox _expression = new() { Text = "x^2 / 4 - 3", Width = 220 }; + private readonly TextBlock _status = new() { VerticalAlignment = VerticalAlignment.Center }; + private readonly Canvas2DView _view = new(); + + public MainWindow() + { + Title = "SharpMath Canvas (Avalonia, Skia)"; + Width = 900; + Height = 560; + + var plot = new Button { Content = "Plot", IsDefault = true }; + plot.Click += (_, _) => PlotExpression(); + var clear = new Button { Content = "Clear" }; + clear.Click += (_, _) => _view.Canvas.Items.Clear(); + var tracking = new CheckBox { Content = "Tracking lines" }; + tracking.IsCheckedChanged += (_, _) => _view.ShowTrackingLines = tracking.IsChecked == true; + var indicator = new CheckBox { Content = "Value indicator" }; + indicator.IsCheckedChanged += (_, _) => _view.ShowValueIndicator = indicator.IsChecked == true; + + var toolbar = new StackPanel + { + Orientation = Orientation.Horizontal, + Spacing = 8, + Margin = new global::Avalonia.Thickness(8), + Children = + { + new TextBlock { Text = "f(x) =", VerticalAlignment = VerticalAlignment.Center }, + _expression, plot, clear, tracking, indicator, _status + } + }; + + var root = new DockPanel(); + DockPanel.SetDock(toolbar, Dock.Top); + root.Children.Add(toolbar); + root.Children.Add(_view); + Content = root; + + _view.Canvas.AddFunction(x => Math.Pow(x, 3)); + _view.Canvas.AddFunction(x => 1.5 * Math.Sin(x), new ItemStyle(new Stroke(Color.FromRgb(200, 0, 0), 2))); + _view.Canvas.AddVector(new Vector2(4, 8)); + _view.Canvas.AddPolygon(new Polygon(new Point2D(2, -2), new Point2D(6, -2), new Point2D(4, -5))); + } + + private void PlotExpression() + { + try + { + _view.Canvas.AddFunction(_expression.Text ?? string.Empty, + new ItemStyle(new Stroke(Color.FromRgb(0, 90, 200), 2))); + _status.Text = string.Empty; + } + catch (Exception exception) when (exception is ParserException or ArgumentException) + { + _status.Text = exception.Message; + } + } +} diff --git a/samples/SharpMath.Samples.Canvas.Avalonia/Program.cs b/samples/SharpMath.Samples.Canvas.Avalonia/Program.cs new file mode 100644 index 0000000..ffd47fd --- /dev/null +++ b/samples/SharpMath.Samples.Canvas.Avalonia/Program.cs @@ -0,0 +1,25 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Themes.Fluent; + +namespace SharpMath.Samples.Canvas.Avalonia; + +internal static class Program +{ + [STAThread] + public static void Main(string[] args) => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); + + public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure().UsePlatformDetect(); +} + +internal sealed class App : Application +{ + public override void Initialize() => Styles.Add(new FluentTheme()); + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + desktop.MainWindow = new MainWindow(); + base.OnFrameworkInitializationCompleted(); + } +} diff --git a/samples/SharpMath.Samples.Canvas.Avalonia/SharpMath.Samples.Canvas.Avalonia.csproj b/samples/SharpMath.Samples.Canvas.Avalonia/SharpMath.Samples.Canvas.Avalonia.csproj new file mode 100644 index 0000000..e0d1d7e --- /dev/null +++ b/samples/SharpMath.Samples.Canvas.Avalonia/SharpMath.Samples.Canvas.Avalonia.csproj @@ -0,0 +1,16 @@ + + + + WinExe + + + + + + + + + + + + diff --git a/samples/SharpMath.Samples.Canvas.WinForms/CanvasForm.cs b/samples/SharpMath.Samples.Canvas.WinForms/CanvasForm.cs new file mode 100644 index 0000000..b13a67d --- /dev/null +++ b/samples/SharpMath.Samples.Canvas.WinForms/CanvasForm.cs @@ -0,0 +1,55 @@ +using SharpMath.Canvas; +using SharpMath.Canvas.WinForms; +using SharpMath.Expressions; +using SharpMath.Geometry; + +namespace SharpMath.Samples.Canvas.WinForms; + +/// +/// Shows the canvas with a few functions and a vector. Drag to pan, use the wheel to zoom, and enter expressions +/// such as 2x^2 - 3x to plot them. +/// +internal sealed class CanvasForm : Form +{ + private readonly Canvas2DControl _canvas = new() { Dock = DockStyle.Fill }; + private readonly TextBox _expression = new() { Width = 220, Text = "x^2 / 4 - 3" }; + + public CanvasForm() + { + Text = "SharpMath Canvas (WinForms, GDI+)"; + ClientSize = new Size(900, 560); + + var plot = new Button { Text = "Plot", AutoSize = true }; + plot.Click += (_, _) => PlotExpression(); + var clear = new Button { Text = "Clear", AutoSize = true }; + clear.Click += (_, _) => _canvas.Canvas.Items.Clear(); + var tracking = new CheckBox { Text = "Tracking lines", AutoSize = true }; + tracking.CheckedChanged += (_, _) => _canvas.ShowTrackingLines = tracking.Checked; + var indicator = new CheckBox { Text = "Value indicator", AutoSize = true }; + indicator.CheckedChanged += (_, _) => _canvas.ShowValueIndicator = indicator.Checked; + + var toolbar = new FlowLayoutPanel { Dock = DockStyle.Top, AutoSize = true, Padding = new Padding(4) }; + toolbar.Controls.AddRange([new Label { Text = "f(x) =", AutoSize = true, Margin = new Padding(3, 7, 0, 0) }, + _expression, plot, clear, tracking, indicator]); + + Controls.Add(_canvas); + Controls.Add(toolbar); + AcceptButton = plot; + + _canvas.Canvas.AddFunction(x => Math.Pow(x, 3)); + _canvas.Canvas.AddFunction(x => 1.5 * Math.Sin(x), new ItemStyle(new Stroke(SharpMath.Canvas.Color.FromRgb(200, 0, 0), 2))); + _canvas.Canvas.AddVector(new Vector2(4, 8)); + } + + private void PlotExpression() + { + try + { + _canvas.Canvas.AddFunction(_expression.Text, new ItemStyle(new Stroke(SharpMath.Canvas.Color.FromRgb(0, 90, 200), 2))); + } + catch (Exception exception) when (exception is ParserException or ArgumentException) + { + MessageBox.Show(this, exception.Message, "Invalid expression", MessageBoxButtons.OK, MessageBoxIcon.Warning); + } + } +} diff --git a/samples/SharpMath.Samples.Canvas.WinForms/Program.cs b/samples/SharpMath.Samples.Canvas.WinForms/Program.cs new file mode 100644 index 0000000..d50bf65 --- /dev/null +++ b/samples/SharpMath.Samples.Canvas.WinForms/Program.cs @@ -0,0 +1,11 @@ +namespace SharpMath.Samples.Canvas.WinForms; + +internal static class Program +{ + [STAThread] + private static void Main() + { + ApplicationConfiguration.Initialize(); + Application.Run(new CanvasForm()); + } +} diff --git a/samples/SharpMath.Samples.Canvas.WinForms/SharpMath.Samples.Canvas.WinForms.csproj b/samples/SharpMath.Samples.Canvas.WinForms/SharpMath.Samples.Canvas.WinForms.csproj new file mode 100644 index 0000000..a7fc645 --- /dev/null +++ b/samples/SharpMath.Samples.Canvas.WinForms/SharpMath.Samples.Canvas.WinForms.csproj @@ -0,0 +1,14 @@ + + + + WinExe + net10.0-windows + true + true + + + + + + + diff --git a/samples/SharpMath.Samples.Export/Program.cs b/samples/SharpMath.Samples.Export/Program.cs new file mode 100644 index 0000000..6cd1be7 --- /dev/null +++ b/samples/SharpMath.Samples.Export/Program.cs @@ -0,0 +1,38 @@ +// Renders two example plots to PNG (SkiaSharp) and SVG (no dependencies) without any UI. +// Usage: dotnet run --project samples/SharpMath.Samples.Export -- [output directory] +// The images in doc/images are generated with: dotnet run --project samples/SharpMath.Samples.Export -- doc/images + +using SharpMath.Canvas; +using SharpMath.Canvas.Skia; +using SharpMath.Geometry; + +var output = args.Length > 0 ? args[0] : "output"; +Directory.CreateDirectory(output); + +var functions = new Canvas2D(); +functions.Viewport.SetScale(40, 40); +functions.AddFunction("x^3 / 10 - x"); +functions.AddFunction(x => 1.5 * Math.Sin(2 * x), new ItemStyle(new Stroke(Color.FromRgb(200, 0, 0), 2))); +functions.AddFunction("tan(x)", new ItemStyle(new Stroke(Color.FromRgb(0, 90, 200), 2, DashStyle.Dashed))); +Export(functions, "canvas-functions"); + +var geometry = new Canvas2D { ShowTrackingLines = true, ShowValueIndicator = true }; +geometry.Viewport.SetScale(30, 30); +geometry.AddPolygon(new Polygon(new Point2D(1, 1), new Point2D(5, 1), new Point2D(6, 4), new Point2D(2, 5))); +geometry.AddLine(Line2D.FromSlopeIntercept(-0.5, -1)); +geometry.AddSegment(new LineSegment2D(new Point2D(-7, -2), new Point2D(-2, -4))); +geometry.AddVector(new Vector2(-4, 3)); +geometry.AddVector(new Vector2(3, -2), new Point2D(-4, 3)); +geometry.AddPoint(new Point2D(-1, 3)); +geometry.PointerMoved(new CanvasPoint(560, 140)); +Export(geometry, "canvas-geometry"); + +void Export(Canvas2D canvas, string name) +{ + const int width = 720; + const int height = 400; + using (var png = File.Create(Path.Combine(output, name + ".png"))) + canvas.RenderPng(png, width, height); + File.WriteAllText(Path.Combine(output, name + ".svg"), canvas.RenderSvg(width, height)); + Console.WriteLine($"Wrote {Path.Combine(output, name)}.png and .svg"); +} diff --git a/samples/SharpMath.Samples.Export/SharpMath.Samples.Export.csproj b/samples/SharpMath.Samples.Export/SharpMath.Samples.Export.csproj new file mode 100644 index 0000000..32bb99a --- /dev/null +++ b/samples/SharpMath.Samples.Export/SharpMath.Samples.Export.csproj @@ -0,0 +1,11 @@ + + + + Exe + + + + + + + diff --git a/samples/SharpMath.Samples.Perspective.WinForms/PerspectiveForm.cs b/samples/SharpMath.Samples.Perspective.WinForms/PerspectiveForm.cs new file mode 100644 index 0000000..8c2e0c2 --- /dev/null +++ b/samples/SharpMath.Samples.Perspective.WinForms/PerspectiveForm.cs @@ -0,0 +1,110 @@ +using SharpMath.Geometry; + +namespace SharpMath.Samples.Perspective.WinForms; + +/// +/// Projects a rotating wireframe cube with : world transformations, +/// and a right-handed perspective projection, composed as projection · view · model (column vectors). +/// +internal sealed class PerspectiveForm : Form +{ + private static readonly Vector3[] Vertices = + [ + new(-0.5, -0.5, 0.5), new(0.5, -0.5, 0.5), new(0.5, 0.5, 0.5), new(-0.5, 0.5, 0.5), + new(-0.5, -0.5, -0.5), new(0.5, -0.5, -0.5), new(0.5, 0.5, -0.5), new(-0.5, 0.5, -0.5) + ]; + + private static readonly (int From, int To)[] Edges = + [ + (0, 1), (1, 2), (2, 3), (3, 0), (4, 5), (5, 6), (6, 7), (7, 4), (0, 4), (1, 5), (2, 6), (3, 7) + ]; + + private readonly CheckBox _autoRotate = new() { Text = "Auto rotation", AutoSize = true }; + private readonly DoubleBufferedPanel _panel = new() { Dock = DockStyle.Fill, BackColor = System.Drawing.Color.Black }; + private readonly TrackBar _rotationX = CreateTrackBar(30); + private readonly TrackBar _rotationY = CreateTrackBar(40); + private readonly TrackBar _rotationZ = CreateTrackBar(0); + private readonly TrackBar _scaling = CreateTrackBar(360); + private readonly System.Windows.Forms.Timer _timer = new() { Interval = 40 }; + + public PerspectiveForm() + { + Text = "SharpMath Perspective"; + ClientSize = new Size(900, 700); + + var controls = new TableLayoutPanel { Dock = DockStyle.Bottom, AutoSize = true, ColumnCount = 5 }; + AddRow(controls, "Rotation X:", _rotationX); + AddRow(controls, "Rotation Y:", _rotationY); + AddRow(controls, "Rotation Z:", _rotationZ); + AddRow(controls, "Scaling:", _scaling); + controls.Controls.Add(_autoRotate, 2, 0); + + Controls.Add(_panel); + Controls.Add(controls); + + foreach (var trackBar in new[] { _rotationX, _rotationY, _rotationZ, _scaling }) + trackBar.Scroll += (_, _) => _panel.Invalidate(); + _panel.Paint += (_, e) => DrawCube(e.Graphics); + _autoRotate.CheckedChanged += (_, _) => _timer.Enabled = _autoRotate.Checked; + _timer.Tick += (_, _) => + { + foreach (var trackBar in new[] { _rotationX, _rotationY, _rotationZ }) + trackBar.Value = (trackBar.Value + 1) % (trackBar.Maximum + 1); + _panel.Invalidate(); + }; + } + + protected override void Dispose(bool disposing) + { + if (disposing) + _timer.Dispose(); + base.Dispose(disposing); + } + + private static TrackBar CreateTrackBar(int value) => + new() { Maximum = 360, Value = value, TickFrequency = 30, Width = 260 }; + + private static void AddRow(TableLayoutPanel table, string text, TrackBar trackBar) + { + var row = table.RowCount++; + table.Controls.Add(new Label { Text = text, AutoSize = true, Anchor = AnchorStyles.Left }, 0, row); + table.Controls.Add(trackBar, 1, row); + } + + private void DrawCube(Graphics graphics) + { + var width = _panel.ClientSize.Width; + var height = _panel.ClientSize.Height; + if (width == 0 || height == 0) + return; + + // Transformations compose from right to left: scale, then rotate, then view and project. + var model = Matrix4x4.Rotation(double.DegreesToRadians(_rotationX.Value), + double.DegreesToRadians(_rotationY.Value), double.DegreesToRadians(_rotationZ.Value)) * + Matrix4x4.Scaling(Math.Max(0.05, _scaling.Value / 360.0)); + var view = Matrix4x4.LookAt(new Vector3(0, 0, 3), Vector3.Zero, Vector3.Up); + var projection = Matrix4x4.PerspectiveFieldOfView(Math.PI / 3, (double) width / height, 0.1, 100); + var transformation = projection * view * model; + + // Matrix4x4 * Vector3 performs the perspective divide; map normalized device coordinates to pixels. + var screen = Vertices.Select(vertex => + { + var ndc = transformation * vertex; + return new PointF((float) ((ndc.X + 1) / 2 * width), (float) ((1 - ndc.Y) / 2 * height)); + }).ToArray(); + + graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias; + using var pen = new Pen(System.Drawing.Color.White, 3); + foreach (var (from, to) in Edges) + graphics.DrawLine(pen, screen[from], screen[to]); + } + + private sealed class DoubleBufferedPanel : Panel + { + public DoubleBufferedPanel() + { + SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, + true); + } + } +} diff --git a/samples/SharpMath.Samples.Perspective.WinForms/Program.cs b/samples/SharpMath.Samples.Perspective.WinForms/Program.cs new file mode 100644 index 0000000..7302288 --- /dev/null +++ b/samples/SharpMath.Samples.Perspective.WinForms/Program.cs @@ -0,0 +1,11 @@ +namespace SharpMath.Samples.Perspective.WinForms; + +internal static class Program +{ + [STAThread] + private static void Main() + { + ApplicationConfiguration.Initialize(); + Application.Run(new PerspectiveForm()); + } +} diff --git a/samples/SharpMath.Samples.Perspective.WinForms/SharpMath.Samples.Perspective.WinForms.csproj b/samples/SharpMath.Samples.Perspective.WinForms/SharpMath.Samples.Perspective.WinForms.csproj new file mode 100644 index 0000000..bf490f6 --- /dev/null +++ b/samples/SharpMath.Samples.Perspective.WinForms/SharpMath.Samples.Perspective.WinForms.csproj @@ -0,0 +1,14 @@ + + + + WinExe + net10.0-windows + true + true + + + + + + + diff --git a/samples/SharpMath.Samples.Polygon.WinForms/ContainsForm.cs b/samples/SharpMath.Samples.Polygon.WinForms/ContainsForm.cs new file mode 100644 index 0000000..1ebad72 --- /dev/null +++ b/samples/SharpMath.Samples.Polygon.WinForms/ContainsForm.cs @@ -0,0 +1,42 @@ +using SharpMath.Geometry; + +namespace SharpMath.Samples.Polygon.WinForms; + +/// +/// Colors a polygon depending on whether the mouse pointer is inside it (). +/// +internal sealed class ContainsForm : Form +{ + private static readonly PointF[] Points = [new(20, 20), new(220, 100), new(420, 220), new(120, 80), new(60, 300)]; + + private readonly Geometry.Polygon _polygon = new(Points.Select(point => new Point2D(point.X, point.Y))); + private bool _containsPointer; + + public ContainsForm() + { + Text = "SharpMath Polygon.Contains"; + ClientSize = new Size(560, 420); + DoubleBuffered = true; + BackColor = System.Drawing.Color.Black; + } + + protected override void OnMouseMove(MouseEventArgs e) + { + ArgumentNullException.ThrowIfNull(e); + base.OnMouseMove(e); + var contains = _polygon.Contains(new Point2D(e.X, e.Y)); + Text = $"SharpMath Polygon.Contains: {e.Location} is {(contains ? "inside" : "outside")}"; + if (contains == _containsPointer) + return; + _containsPointer = contains; + Invalidate(); + } + + protected override void OnPaint(PaintEventArgs e) + { + ArgumentNullException.ThrowIfNull(e); + base.OnPaint(e); + e.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias; + e.Graphics.FillPolygon(_containsPointer ? Brushes.AliceBlue : Brushes.Red, Points); + } +} diff --git a/samples/SharpMath.Samples.Polygon.WinForms/Program.cs b/samples/SharpMath.Samples.Polygon.WinForms/Program.cs new file mode 100644 index 0000000..98c3f16 --- /dev/null +++ b/samples/SharpMath.Samples.Polygon.WinForms/Program.cs @@ -0,0 +1,11 @@ +namespace SharpMath.Samples.Polygon.WinForms; + +internal static class Program +{ + [STAThread] + private static void Main() + { + ApplicationConfiguration.Initialize(); + Application.Run(new ContainsForm()); + } +} diff --git a/samples/SharpMath.Samples.Polygon.WinForms/SharpMath.Samples.Polygon.WinForms.csproj b/samples/SharpMath.Samples.Polygon.WinForms/SharpMath.Samples.Polygon.WinForms.csproj new file mode 100644 index 0000000..bf490f6 --- /dev/null +++ b/samples/SharpMath.Samples.Polygon.WinForms/SharpMath.Samples.Polygon.WinForms.csproj @@ -0,0 +1,14 @@ + + + + WinExe + net10.0-windows + true + true + + + + + + + diff --git a/src/Directory.Build.props b/src/Directory.Build.props new file mode 100644 index 0000000..0f62a7a --- /dev/null +++ b/src/Directory.Build.props @@ -0,0 +1,18 @@ + + + + + true + true + README.md + true + snupkg + true + true + + + + + + + diff --git a/src/SharpMath.Canvas.Avalonia/Canvas2DView.cs b/src/SharpMath.Canvas.Avalonia/Canvas2DView.cs new file mode 100644 index 0000000..f6ab68d --- /dev/null +++ b/src/SharpMath.Canvas.Avalonia/Canvas2DView.cs @@ -0,0 +1,208 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Media; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Skia; +using Avalonia.Threading; +using SharpMath.Canvas.Skia; +using SkiaSharp; + +namespace SharpMath.Canvas.Avalonia; + +/// +/// An Avalonia control that displays a . Drag with the left mouse button to pan and use the +/// mouse wheel to zoom. Requires Avalonia's Skia renderer (the default on all desktop platforms). +/// +public class Canvas2DView : Control +{ + /// + /// Defines the property. + /// + public static readonly StyledProperty ShowTrackingLinesProperty = + AvaloniaProperty.Register(nameof(ShowTrackingLines)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ShowValueIndicatorProperty = + AvaloniaProperty.Register(nameof(ShowValueIndicator)); + + // Text is measured with Skia on the UI thread through a context that is kept while the view is attached, so + // fonts are looked up once rather than every frame. + private TextMeasurer? _measurer; + + /// + /// Initializes a new instance of the class. + /// + public Canvas2DView() + { + ClipToBounds = true; + Focusable = true; + Canvas.Changed += OnCanvasChanged; + } + + /// + /// Gets the canvas model: add items, change the style or move the viewport through it. + /// + public Canvas2D Canvas { get; } = new(); + + /// + /// Gets or sets a value indicating whether horizontal and vertical lines are drawn through the pointer. + /// + public bool ShowTrackingLines + { + get => GetValue(ShowTrackingLinesProperty); + set => SetValue(ShowTrackingLinesProperty, value); + } + + /// + /// Gets or sets a value indicating whether the world coordinates of the pointer are shown next to it. + /// + public bool ShowValueIndicator + { + get => GetValue(ShowValueIndicatorProperty); + set => SetValue(ShowValueIndicatorProperty, value); + } + + /// + public override void Render(DrawingContext context) + { + ArgumentNullException.ThrowIfNull(context); + var size = Bounds.Size; + _measurer ??= new TextMeasurer(); + var displayList = new DisplayList(_measurer.Context); + Canvas.Render(displayList, size.Width, size.Height); + context.Custom(new CanvasDrawOperation(new Rect(size), displayList)); + } + + /// + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + _measurer?.Dispose(); + _measurer = null; + } + + /// + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + ArgumentNullException.ThrowIfNull(change); + base.OnPropertyChanged(change); + if (change.Property == ShowTrackingLinesProperty) + Canvas.ShowTrackingLines = change.GetNewValue(); + else if (change.Property == ShowValueIndicatorProperty) + Canvas.ShowValueIndicator = change.GetNewValue(); + } + + /// + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + ArgumentNullException.ThrowIfNull(e); + base.OnPointerPressed(e); + var point = e.GetCurrentPoint(this); + if (!point.Properties.IsLeftButtonPressed) + return; + Canvas.PointerPressed(ToCanvas(point.Position)); + e.Pointer.Capture(this); + e.Handled = true; + } + + /// + protected override void OnPointerMoved(PointerEventArgs e) + { + ArgumentNullException.ThrowIfNull(e); + base.OnPointerMoved(e); + Canvas.PointerMoved(ToCanvas(e.GetPosition(this))); + } + + /// + protected override void OnPointerReleased(PointerReleasedEventArgs e) + { + ArgumentNullException.ThrowIfNull(e); + base.OnPointerReleased(e); + if (e.InitialPressMouseButton != MouseButton.Left) + return; + Canvas.PointerReleased(ToCanvas(e.GetPosition(this))); + e.Pointer.Capture(null); + e.Handled = true; + } + + /// + protected override void OnPointerExited(PointerEventArgs e) + { + base.OnPointerExited(e); + Canvas.PointerExited(); + } + + /// + protected override void OnPointerWheelChanged(PointerWheelEventArgs e) + { + ArgumentNullException.ThrowIfNull(e); + base.OnPointerWheelChanged(e); + Canvas.Wheel(ToCanvas(e.GetPosition(this)), e.Delta.Y); + e.Handled = true; + } + + private static CanvasPoint ToCanvas(Point point) => new(point.X, point.Y); + + private void OnCanvasChanged(object? sender, EventArgs e) + { + if (Dispatcher.UIThread.CheckAccess()) + InvalidateVisual(); + else + Dispatcher.UIThread.Post(InvalidateVisual); + } + + /// + /// Replays the recorded canvas on Skia's canvas on the render thread. + /// + private sealed class CanvasDrawOperation(Rect bounds, DisplayList displayList) : ICustomDrawOperation + { + public Rect Bounds => bounds; + + public bool HitTest(Point p) => bounds.Contains(p); + + public void Render(ImmediateDrawingContext context) + { + if (context.TryGetFeature(typeof(ISkiaSharpApiLeaseFeature)) is not ISkiaSharpApiLeaseFeature feature) + return; + + using var lease = feature.Lease(); + using var skia = new SkiaDrawingContext(lease.SkCanvas); + skia.PushClip(new CanvasRect(0, 0, bounds.Width, bounds.Height)); + displayList.Replay(skia); + skia.PopClip(); + } + + public bool Equals(ICustomDrawOperation? other) => ReferenceEquals(this, other); + + public void Dispose() + { + } + } + + /// + /// A Skia context on a 1x1 bitmap, used only to measure text. + /// + private sealed class TextMeasurer : IDisposable + { + private readonly SKBitmap _bitmap = new(1, 1); + private readonly SKCanvas _canvas; + + public TextMeasurer() + { + _canvas = new SKCanvas(_bitmap); + Context = new SkiaDrawingContext(_canvas); + } + + public SkiaDrawingContext Context { get; } + + public void Dispose() + { + Context.Dispose(); + _canvas.Dispose(); + _bitmap.Dispose(); + } + } +} diff --git a/src/SharpMath.Canvas.Avalonia/DisplayList.cs b/src/SharpMath.Canvas.Avalonia/DisplayList.cs new file mode 100644 index 0000000..20b1abb --- /dev/null +++ b/src/SharpMath.Canvas.Avalonia/DisplayList.cs @@ -0,0 +1,52 @@ +namespace SharpMath.Canvas.Avalonia; + +/// +/// Records drawing calls so the canvas can be drawn on Avalonia's render thread without touching the model there. +/// Text is measured by , which should use the same engine that later replays the +/// commands. +/// +/// The context used for . +internal sealed class DisplayList(IDrawingContext measurer) : IDrawingContext +{ + private readonly List> _commands = []; + + public int Count => _commands.Count; + + public void Clear(Color color) => _commands.Add(context => context.Clear(color)); + + public void DrawLine(CanvasPoint startPoint, CanvasPoint endPoint, Stroke stroke) => + _commands.Add(context => context.DrawLine(startPoint, endPoint, stroke)); + + public void DrawPolyline(ReadOnlySpan points, Stroke stroke) + { + var copy = points.ToArray(); + _commands.Add(context => context.DrawPolyline(copy, stroke)); + } + + public void DrawPolygon(ReadOnlySpan points, Stroke? stroke, Color? fill) + { + var copy = points.ToArray(); + _commands.Add(context => context.DrawPolygon(copy, stroke, fill)); + } + + public void DrawRectangle(CanvasRect rect, Stroke? stroke, Color? fill) => + _commands.Add(context => context.DrawRectangle(rect, stroke, fill)); + + public void DrawEllipse(CanvasPoint center, double radiusX, double radiusY, Stroke? stroke, Color? fill) => + _commands.Add(context => context.DrawEllipse(center, radiusX, radiusY, stroke, fill)); + + public void DrawText(string text, CanvasPoint position, FontSpec font, Color color, TextAlignment alignment) => + _commands.Add(context => context.DrawText(text, position, font, color, alignment)); + + public TextSize MeasureText(string text, FontSpec font) => measurer.MeasureText(text, font); + + public void PushClip(CanvasRect rect) => _commands.Add(context => context.PushClip(rect)); + + public void PopClip() => _commands.Add(context => context.PopClip()); + + public void Replay(IDrawingContext context) + { + foreach (var command in _commands) + command(context); + } +} diff --git a/src/SharpMath.Canvas.Avalonia/SharpMath.Canvas.Avalonia.csproj b/src/SharpMath.Canvas.Avalonia/SharpMath.Canvas.Avalonia.csproj new file mode 100644 index 0000000..53e18eb --- /dev/null +++ b/src/SharpMath.Canvas.Avalonia/SharpMath.Canvas.Avalonia.csproj @@ -0,0 +1,16 @@ + + + + Avalonia control for the SharpMath 2D canvas: interactive plotting on Windows, Linux and macOS. + + + + + + + + + + + + diff --git a/src/SharpMath.Canvas.Skia/SharpMath.Canvas.Skia.csproj b/src/SharpMath.Canvas.Skia/SharpMath.Canvas.Skia.csproj new file mode 100644 index 0000000..0a4f09c --- /dev/null +++ b/src/SharpMath.Canvas.Skia/SharpMath.Canvas.Skia.csproj @@ -0,0 +1,16 @@ + + + + SkiaSharp renderer for the SharpMath 2D canvas: draw on any SKCanvas and export PNG images on Windows, Linux and macOS. + + + + + + + + + + + + diff --git a/src/SharpMath.Canvas.Skia/SkiaDrawingContext.cs b/src/SharpMath.Canvas.Skia/SkiaDrawingContext.cs new file mode 100644 index 0000000..5cce437 --- /dev/null +++ b/src/SharpMath.Canvas.Skia/SkiaDrawingContext.cs @@ -0,0 +1,166 @@ +using SkiaSharp; + +namespace SharpMath.Canvas.Skia; + +/// +/// An that draws on a SkiaSharp . +/// +/// +/// Paints, fonts and typefaces are cached for the lifetime of the context; dispose it after drawing. The canvas +/// itself is not owned by the context. +/// +public sealed class SkiaDrawingContext : IDrawingContext, IDisposable +{ + private readonly SKCanvas _canvas; + private readonly Dictionary _fills = []; + private readonly SkiaFonts _fonts = new(); + private readonly Dictionary _strokes = []; + private readonly SKPath _path = new(); + + /// + /// Initializes a new instance of the class. + /// + /// The canvas to draw on. + public SkiaDrawingContext(SKCanvas canvas) + { + ArgumentNullException.ThrowIfNull(canvas); + _canvas = canvas; + } + + /// + public void Clear(Color color) => _canvas.Clear(ToSkia(color)); + + /// + public void DrawLine(CanvasPoint startPoint, CanvasPoint endPoint, Stroke stroke) => + _canvas.DrawLine((float) startPoint.X, (float) startPoint.Y, (float) endPoint.X, (float) endPoint.Y, + StrokePaint(stroke)); + + /// + public void DrawPolyline(ReadOnlySpan points, Stroke stroke) + { + if (points.Length < 2) + return; + BuildPath(points, false); + _canvas.DrawPath(_path, StrokePaint(stroke)); + } + + /// + public void DrawPolygon(ReadOnlySpan points, Stroke? stroke, Color? fill) + { + if (points.Length < 3) + return; + BuildPath(points, true); + if (fill is { } fillColor) + _canvas.DrawPath(_path, FillPaint(fillColor)); + if (stroke is { } outline) + _canvas.DrawPath(_path, StrokePaint(outline)); + } + + /// + public void DrawRectangle(CanvasRect rect, Stroke? stroke, Color? fill) + { + var skRect = SKRect.Create((float) rect.X, (float) rect.Y, (float) rect.Width, (float) rect.Height); + if (fill is { } fillColor) + _canvas.DrawRect(skRect, FillPaint(fillColor)); + if (stroke is { } outline) + _canvas.DrawRect(skRect, StrokePaint(outline)); + } + + /// + public void DrawEllipse(CanvasPoint center, double radiusX, double radiusY, Stroke? stroke, Color? fill) + { + if (fill is { } fillColor) + _canvas.DrawOval((float) center.X, (float) center.Y, (float) radiusX, (float) radiusY, FillPaint(fillColor)); + if (stroke is { } outline) + _canvas.DrawOval((float) center.X, (float) center.Y, (float) radiusX, (float) radiusY, StrokePaint(outline)); + } + + /// + public void DrawText(string text, CanvasPoint position, FontSpec font, Color color, TextAlignment alignment) + { + ArgumentNullException.ThrowIfNull(text); + var skFont = _fonts.Get(font); + var align = alignment switch + { + TextAlignment.Center => SKTextAlign.Center, + TextAlignment.Right => SKTextAlign.Right, + _ => SKTextAlign.Left + }; + + // Skia positions text at its baseline; the ascent is negative. + var baseline = (float) position.Y - skFont.Metrics.Ascent; + _canvas.DrawText(text, (float) position.X, baseline, align, skFont, FillPaint(color)); + } + + /// + public TextSize MeasureText(string text, FontSpec font) => _fonts.Measure(text, font); + + /// + public void PushClip(CanvasRect rect) + { + _canvas.Save(); + _canvas.ClipRect(SKRect.Create((float) rect.X, (float) rect.Y, (float) rect.Width, (float) rect.Height)); + } + + /// + public void PopClip() => _canvas.Restore(); + + /// + public void Dispose() + { + foreach (var paint in _strokes.Values.Concat(_fills.Values)) + { + paint.PathEffect?.Dispose(); + paint.Dispose(); + } + _fonts.Dispose(); + _path.Dispose(); + _strokes.Clear(); + _fills.Clear(); + } + + private static SKColor ToSkia(Color color) => new(color.R, color.G, color.B, color.A); + + private void BuildPath(ReadOnlySpan points, bool close) + { + _path.Reset(); + _path.MoveTo((float) points[0].X, (float) points[0].Y); + foreach (var point in points[1..]) + _path.LineTo((float) point.X, (float) point.Y); + if (close) + _path.Close(); + } + + private SKPaint StrokePaint(Stroke stroke) + { + if (_strokes.TryGetValue(stroke, out var paint)) + return paint; + + var width = (float) stroke.Width; + paint = new SKPaint + { + Color = ToSkia(stroke.Color), + StrokeWidth = width, + Style = SKPaintStyle.Stroke, + IsAntialias = true, + PathEffect = stroke.Dash switch + { + DashStyle.Dashed => SKPathEffect.CreateDash([4 * width, 2 * width], 0), + DashStyle.Dotted => SKPathEffect.CreateDash([width, 2 * width], 0), + _ => null + } + }; + _strokes.Add(stroke, paint); + return paint; + } + + private SKPaint FillPaint(Color color) + { + if (_fills.TryGetValue(color, out var paint)) + return paint; + + paint = new SKPaint { Color = ToSkia(color), Style = SKPaintStyle.Fill, IsAntialias = true }; + _fills.Add(color, paint); + return paint; + } +} diff --git a/src/SharpMath.Canvas.Skia/SkiaExport.cs b/src/SharpMath.Canvas.Skia/SkiaExport.cs new file mode 100644 index 0000000..3d1026c --- /dev/null +++ b/src/SharpMath.Canvas.Skia/SkiaExport.cs @@ -0,0 +1,57 @@ +using SkiaSharp; + +namespace SharpMath.Canvas.Skia; + +/// +/// Renders a to raster images with SkiaSharp. +/// +public static class SkiaExport +{ + /// + /// Renders the canvas onto a SkiaSharp canvas. + /// + /// The canvas to render. + /// The SkiaSharp canvas to draw on. + /// The width of the drawing area in pixels. + /// The height of the drawing area in pixels. + public static void Render(this Canvas2D canvas, SKCanvas target, double width, double height) + { + ArgumentNullException.ThrowIfNull(canvas); + using var context = new SkiaDrawingContext(target); + canvas.Render(context, width, height); + } + + /// + /// Renders the canvas into a new image. + /// + /// The canvas to render. + /// The width in pixels. Must be positive. + /// The height in pixels. Must be positive. + /// The image; the caller disposes it. + public static SKImage RenderImage(this Canvas2D canvas, int width, int height) + { + ArgumentNullException.ThrowIfNull(canvas); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(width); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(height); + + using var surface = SKSurface.Create(new SKImageInfo(width, height, SKColorType.Rgba8888, SKAlphaType.Premul)) ?? + throw new InvalidOperationException("Skia could not create a drawing surface."); + canvas.Render(surface.Canvas, width, height); + return surface.Snapshot(); + } + + /// + /// Renders the canvas as a PNG image and writes it to a stream. + /// + /// The canvas to render. + /// The stream to write to. + /// The width in pixels. Must be positive. + /// The height in pixels. Must be positive. + public static void RenderPng(this Canvas2D canvas, Stream stream, int width, int height) + { + ArgumentNullException.ThrowIfNull(stream); + using var image = canvas.RenderImage(width, height); + using var data = image.Encode(SKEncodedImageFormat.Png, 100); + data.SaveTo(stream); + } +} diff --git a/src/SharpMath.Canvas.Skia/SkiaFonts.cs b/src/SharpMath.Canvas.Skia/SkiaFonts.cs new file mode 100644 index 0000000..901545f --- /dev/null +++ b/src/SharpMath.Canvas.Skia/SkiaFonts.cs @@ -0,0 +1,45 @@ +using SkiaSharp; + +namespace SharpMath.Canvas.Skia; + +/// +/// Creates and caches Skia fonts and measures text. Not thread-safe; use one instance per thread. +/// +internal sealed class SkiaFonts : IDisposable +{ + private readonly Dictionary _fonts = []; + private readonly Dictionary _typefaces = []; + + public SKFont Get(FontSpec font) + { + if (_fonts.TryGetValue(font, out var skFont)) + return skFont; + + if (!_typefaces.TryGetValue(font.Family, out var typeface)) + { + typeface = SKTypeface.FromFamilyName(font.Family) ?? SKTypeface.CreateDefault(); + _typefaces.Add(font.Family, typeface); + } + + skFont = new SKFont(typeface, (float) font.Size) { Subpixel = true }; + _fonts.Add(font, skFont); + return skFont; + } + + public TextSize Measure(string text, FontSpec font) + { + ArgumentNullException.ThrowIfNull(text); + var skFont = Get(font); + return new TextSize(skFont.MeasureText(text), skFont.Spacing); + } + + public void Dispose() + { + foreach (var font in _fonts.Values) + font.Dispose(); + foreach (var typeface in _typefaces.Values) + typeface.Dispose(); + _fonts.Clear(); + _typefaces.Clear(); + } +} diff --git a/src/SharpMath.Canvas.WinForms/Canvas2DControl.cs b/src/SharpMath.Canvas.WinForms/Canvas2DControl.cs new file mode 100644 index 0000000..8fb5942 --- /dev/null +++ b/src/SharpMath.Canvas.WinForms/Canvas2DControl.cs @@ -0,0 +1,114 @@ +using System.ComponentModel; + +namespace SharpMath.Canvas.WinForms; + +/// +/// A WinForms control that displays a . Drag with the left mouse button to pan and use the +/// mouse wheel to zoom. +/// +[ToolboxItem(true)] +[DesignerCategory("Code")] +public class Canvas2DControl : Control +{ + /// + /// Initializes a new instance of the class. + /// + public Canvas2DControl() + { + SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.UserPaint | + ControlStyles.ResizeRedraw, true); + Canvas.Changed += OnCanvasChanged; + } + + /// + /// Gets the canvas model: add items, change the style or move the viewport through it. + /// + [Browsable(false)] + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public Canvas2D Canvas { get; } = new(); + + /// + /// Gets or sets a value indicating whether horizontal and vertical lines are drawn through the mouse pointer. + /// + [Category("Behavior")] + [DefaultValue(false)] + [Description("Draws horizontal and vertical lines through the mouse pointer.")] + public bool ShowTrackingLines + { + get => Canvas.ShowTrackingLines; + set => Canvas.ShowTrackingLines = value; + } + + /// + /// Gets or sets a value indicating whether the world coordinates of the mouse pointer are shown next to it. + /// + [Category("Behavior")] + [DefaultValue(false)] + [Description("Shows the world coordinates of the mouse pointer.")] + public bool ShowValueIndicator + { + get => Canvas.ShowValueIndicator; + set => Canvas.ShowValueIndicator = value; + } + + /// + protected override void OnPaint(PaintEventArgs e) + { + ArgumentNullException.ThrowIfNull(e); + base.OnPaint(e); + using var context = new GdiDrawingContext(e.Graphics); + Canvas.Render(context, ClientSize.Width, ClientSize.Height); + } + + /// + protected override void OnMouseDown(MouseEventArgs e) + { + ArgumentNullException.ThrowIfNull(e); + base.OnMouseDown(e); + if (e.Button == MouseButtons.Left) + Canvas.PointerPressed(new CanvasPoint(e.X, e.Y)); + Focus(); + } + + /// + protected override void OnMouseMove(MouseEventArgs e) + { + ArgumentNullException.ThrowIfNull(e); + base.OnMouseMove(e); + Canvas.PointerMoved(new CanvasPoint(e.X, e.Y)); + } + + /// + protected override void OnMouseUp(MouseEventArgs e) + { + ArgumentNullException.ThrowIfNull(e); + base.OnMouseUp(e); + if (e.Button == MouseButtons.Left) + Canvas.PointerReleased(new CanvasPoint(e.X, e.Y)); + } + + /// + protected override void OnMouseLeave(EventArgs e) + { + base.OnMouseLeave(e); + Canvas.PointerExited(); + } + + /// + protected override void OnMouseWheel(MouseEventArgs e) + { + ArgumentNullException.ThrowIfNull(e); + base.OnMouseWheel(e); + Canvas.Wheel(new CanvasPoint(e.X, e.Y), (double) e.Delta / SystemInformation.MouseWheelScrollDelta); + } + + /// + protected override void Dispose(bool disposing) + { + if (disposing) + Canvas.Changed -= OnCanvasChanged; + base.Dispose(disposing); + } + + private void OnCanvasChanged(object? sender, EventArgs e) => Invalidate(); +} diff --git a/src/SharpMath.Canvas.WinForms/GdiDrawingContext.cs b/src/SharpMath.Canvas.WinForms/GdiDrawingContext.cs new file mode 100644 index 0000000..1fae996 --- /dev/null +++ b/src/SharpMath.Canvas.WinForms/GdiDrawingContext.cs @@ -0,0 +1,203 @@ +using System.Drawing.Drawing2D; +using System.Drawing.Text; +using DrawingColor = System.Drawing.Color; +using GdiDashStyle = System.Drawing.Drawing2D.DashStyle; + +namespace SharpMath.Canvas.WinForms; + +/// +/// An that draws with GDI+ on a (Windows only). +/// +/// +/// Pens, brushes and fonts are cached for the lifetime of the context; dispose it after drawing. The rendering +/// settings of the are changed for anti-aliasing and restored on disposal; the +/// itself is not owned by the context. +/// +public sealed class GdiDrawingContext : IDrawingContext, IDisposable +{ + private readonly Dictionary _brushes = []; + private readonly Stack _clips = new(); + private readonly Dictionary _fonts = []; + private readonly Graphics _graphics; + private readonly PixelOffsetMode _originalPixelOffsetMode; + private readonly SmoothingMode _originalSmoothingMode; + private readonly TextRenderingHint _originalTextRenderingHint; + private readonly Dictionary _pens = []; + private bool _disposed; + + /// + /// Initializes a new instance of the class. + /// + /// The GDI+ surface to draw on. + public GdiDrawingContext(Graphics graphics) + { + ArgumentNullException.ThrowIfNull(graphics); + _graphics = graphics; + _originalSmoothingMode = graphics.SmoothingMode; + _originalPixelOffsetMode = graphics.PixelOffsetMode; + _originalTextRenderingHint = graphics.TextRenderingHint; + + // Pixel centers at .5 like the other renderers, so snapped grid lines stay crisp. + graphics.SmoothingMode = SmoothingMode.AntiAlias; + graphics.PixelOffsetMode = PixelOffsetMode.Half; + graphics.TextRenderingHint = TextRenderingHint.AntiAliasGridFit; + } + + /// + public void Clear(Color color) => _graphics.Clear(ToGdi(color)); + + /// + public void DrawLine(CanvasPoint startPoint, CanvasPoint endPoint, Stroke stroke) => + _graphics.DrawLine(Pen(stroke), ToGdi(startPoint), ToGdi(endPoint)); + + /// + public void DrawPolyline(ReadOnlySpan points, Stroke stroke) + { + if (points.Length >= 2) + _graphics.DrawLines(Pen(stroke), ToGdi(points)); + } + + /// + public void DrawPolygon(ReadOnlySpan points, Stroke? stroke, Color? fill) + { + if (points.Length < 3) + return; + var gdiPoints = ToGdi(points); + if (fill is { } fillColor) + _graphics.FillPolygon(Brush(fillColor), gdiPoints); + if (stroke is { } outline) + _graphics.DrawPolygon(Pen(outline), gdiPoints); + } + + /// + public void DrawRectangle(CanvasRect rect, Stroke? stroke, Color? fill) + { + var bounds = new RectangleF((float) rect.X, (float) rect.Y, (float) rect.Width, (float) rect.Height); + if (fill is { } fillColor) + _graphics.FillRectangle(Brush(fillColor), bounds); + if (stroke is { } outline) + _graphics.DrawRectangle(Pen(outline), bounds.X, bounds.Y, bounds.Width, bounds.Height); + } + + /// + public void DrawEllipse(CanvasPoint center, double radiusX, double radiusY, Stroke? stroke, Color? fill) + { + var bounds = new RectangleF((float) (center.X - radiusX), (float) (center.Y - radiusY), (float) (2 * radiusX), + (float) (2 * radiusY)); + if (fill is { } fillColor) + _graphics.FillEllipse(Brush(fillColor), bounds); + if (stroke is { } outline) + _graphics.DrawEllipse(Pen(outline), bounds); + } + + /// + public void DrawText(string text, CanvasPoint position, FontSpec font, Color color, TextAlignment alignment) + { + ArgumentNullException.ThrowIfNull(text); + using var format = (StringFormat) StringFormat.GenericTypographic.Clone(); + format.Alignment = alignment switch + { + TextAlignment.Center => StringAlignment.Center, + TextAlignment.Right => StringAlignment.Far, + _ => StringAlignment.Near + }; + _graphics.DrawString(text, Font(font), Brush(color), ToGdi(position), format); + } + + /// + public TextSize MeasureText(string text, FontSpec font) + { + ArgumentNullException.ThrowIfNull(text); + var gdiFont = Font(font); + var size = _graphics.MeasureString(text, gdiFont, PointF.Empty, StringFormat.GenericTypographic); + return new TextSize(size.Width, gdiFont.GetHeight(_graphics)); + } + + /// + public void PushClip(CanvasRect rect) + { + _clips.Push(_graphics.Save()); + _graphics.SetClip(new RectangleF((float) rect.X, (float) rect.Y, (float) rect.Width, (float) rect.Height), + CombineMode.Intersect); + } + + /// + public void PopClip() + { + if (_clips.Count == 0) + throw new InvalidOperationException("PopClip was called without a matching PushClip."); + _graphics.Restore(_clips.Pop()); + } + + /// + public void Dispose() + { + if (_disposed) + return; + _disposed = true; + + while (_clips.Count > 0) + _graphics.Restore(_clips.Pop()); + _graphics.SmoothingMode = _originalSmoothingMode; + _graphics.PixelOffsetMode = _originalPixelOffsetMode; + _graphics.TextRenderingHint = _originalTextRenderingHint; + + foreach (var pen in _pens.Values) + pen.Dispose(); + foreach (var brush in _brushes.Values) + brush.Dispose(); + foreach (var font in _fonts.Values) + font.Dispose(); + } + + private static DrawingColor ToGdi(Color color) => DrawingColor.FromArgb(color.A, color.R, color.G, color.B); + + private static PointF ToGdi(CanvasPoint point) => new((float) point.X, (float) point.Y); + + private static PointF[] ToGdi(ReadOnlySpan points) + { + var result = new PointF[points.Length]; + for (var i = 0; i < points.Length; i++) + result[i] = ToGdi(points[i]); + return result; + } + + private Pen Pen(Stroke stroke) + { + if (_pens.TryGetValue(stroke, out var pen)) + return pen; + + pen = new Pen(ToGdi(stroke.Color), (float) stroke.Width) + { + DashStyle = stroke.Dash switch + { + DashStyle.Dashed => GdiDashStyle.Dash, + DashStyle.Dotted => GdiDashStyle.Dot, + _ => GdiDashStyle.Solid + } + }; + _pens.Add(stroke, pen); + return pen; + } + + private SolidBrush Brush(Color color) + { + if (_brushes.TryGetValue(color, out var brush)) + return brush; + + brush = new SolidBrush(ToGdi(color)); + _brushes.Add(color, brush); + return brush; + } + + private Font Font(FontSpec font) + { + if (_fonts.TryGetValue(font, out var gdiFont)) + return gdiFont; + + // GDI+ falls back to a default font if the family is not installed. + gdiFont = new Font(font.Family, (float) font.Size, GraphicsUnit.Pixel); + _fonts.Add(font, gdiFont); + return gdiFont; + } +} diff --git a/src/SharpMath.Canvas.WinForms/SharpMath.Canvas.WinForms.csproj b/src/SharpMath.Canvas.WinForms/SharpMath.Canvas.WinForms.csproj new file mode 100644 index 0000000..f811029 --- /dev/null +++ b/src/SharpMath.Canvas.WinForms/SharpMath.Canvas.WinForms.csproj @@ -0,0 +1,14 @@ + + + + net10.0-windows + true + true + WinForms control and GDI+ renderer for the SharpMath 2D canvas (Windows only). + + + + + + + diff --git a/src/SharpMath.Canvas/Canvas2D.cs b/src/SharpMath.Canvas/Canvas2D.cs new file mode 100644 index 0000000..d188804 --- /dev/null +++ b/src/SharpMath.Canvas/Canvas2D.cs @@ -0,0 +1,309 @@ +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using SharpMath.Expressions; +using SharpMath.Geometry; + +namespace SharpMath.Canvas; + +/// +/// A platform-neutral two-dimensional coordinate system that plots functions, vectors, points, lines, segments and +/// polygons. Hosts (e.g. a WinForms or Avalonia control) forward input to it and call +/// with a renderer. +/// +public sealed class Canvas2D +{ + private const double IndicatorPadding = 3; + private const double IndicatorOffset = 15; + + private readonly ItemRenderer _itemRenderer = new(); + private CanvasPoint? _dragPosition; + private CanvasPoint? _pointer; + private bool _rendering; + private bool _showTrackingLines; + private bool _showValueIndicator; + private CanvasStyle _style = CanvasStyle.Default; + + /// + /// Initializes a new instance of the class. + /// + public Canvas2D() + { + Items.CollectionChanged += OnItemsChanged; + Viewport.Changed += (_, _) => RaiseChanged(); + } + + /// + /// Occurs when the canvas needs to be redrawn, e.g. because an item, the style or the viewport changed. + /// + public event EventHandler? Changed; + + /// + /// Gets the items to draw, in drawing order. + /// + public ObservableCollection Items { get; } = []; + + /// + /// Gets the mapping between world and screen coordinates. + /// + public Viewport Viewport { get; } = new(); + + /// + /// Gets or sets the appearance. + /// + public CanvasStyle Style + { + get => _style; + set + { + ArgumentNullException.ThrowIfNull(value); + if (ReferenceEquals(_style, value)) + return; + _style = value; + RaiseChanged(); + } + } + + /// + /// Gets or sets a value indicating whether horizontal and vertical lines are drawn through the pointer. + /// + public bool ShowTrackingLines + { + get => _showTrackingLines; + set + { + if (_showTrackingLines == value) + return; + _showTrackingLines = value; + RaiseChanged(); + } + } + + /// + /// Gets or sets a value indicating whether the world coordinates of the pointer are shown next to it. + /// + public bool ShowValueIndicator + { + get => _showValueIndicator; + set + { + if (_showValueIndicator == value) + return; + _showValueIndicator = value; + RaiseChanged(); + } + } + + /// + /// Gets the last known pointer position in screen coordinates, or if the pointer is outside. + /// + public CanvasPoint? PointerPosition => _pointer; + + /// + /// Adds the graph of a function. + /// + /// The function. + /// The style, or for the default function style. + /// The added item. + public FunctionItem AddFunction(Func function, ItemStyle? style = null) => + Add(new FunctionItem(function) { Style = style }); + + /// + /// Adds the graph of an expression in the variable x, e.g. "2x^2 - 3x". + /// + /// The expression. + /// The style, or for the default function style. + /// The functions, constants and variables to use; the built-ins if . + /// The added item. + /// The expression is invalid. + /// The expression uses variables other than x. + public FunctionItem AddFunction(string expression, ItemStyle? style = null, ExpressionContext? context = null) => + Add(FunctionItem.FromExpression(expression, context) with { Style = style }); + + /// + /// Adds a vector drawn as an arrow. + /// + /// The vector. + /// The start of the arrow; the origin if . + /// The style, or for the default vector style. + /// The added item. + public VectorItem AddVector(Vector2 vector, Point2D? tail = null, ItemStyle? style = null) => + Add(new VectorItem(vector) { Tail = tail ?? Point2D.Origin, Style = style }); + + /// + /// Adds a point. + /// + /// The point. + /// The style, or for the default point style. + /// The added item. + public PointItem AddPoint(Point2D point, ItemStyle? style = null) => Add(new PointItem(point) { Style = style }); + + /// + /// Adds an infinite line. + /// + /// The line. + /// The style, or for the default line style. + /// The added item. + public LineItem AddLine(Line2D line, ItemStyle? style = null) => Add(new LineItem(line) { Style = style }); + + /// + /// Adds a line segment. + /// + /// The segment. + /// The style, or for the default segment style. + /// The added item. + public SegmentItem AddSegment(LineSegment2D segment, ItemStyle? style = null) => + Add(new SegmentItem(segment) { Style = style }); + + /// + /// Adds a polygon. + /// + /// The polygon. + /// The style, or for the default polygon style. + /// The added item. + public PolygonItem AddPolygon(Polygon polygon, ItemStyle? style = null) + { + ArgumentNullException.ThrowIfNull(polygon); + return Add(new PolygonItem(polygon) { Style = style }); + } + + /// + /// Starts panning at the specified position (e.g. on mouse button down). + /// + /// The pointer position in screen coordinates. + public void PointerPressed(CanvasPoint position) + { + _dragPosition = position; + UpdatePointer(position); + } + + /// + /// Updates the pointer position and pans if a drag is in progress. + /// + /// The pointer position in screen coordinates. + public void PointerMoved(CanvasPoint position) + { + if (_dragPosition is { } previous) + { + _dragPosition = position; + Viewport.Pan(position.X - previous.X, position.Y - previous.Y); + } + + UpdatePointer(position); + } + + /// + /// Ends panning (e.g. on mouse button up). + /// + /// The pointer position in screen coordinates. + public void PointerReleased(CanvasPoint position) + { + PointerMoved(position); + _dragPosition = null; + } + + /// + /// Notifies the canvas that the pointer left it. + /// + public void PointerExited() + { + _dragPosition = null; + if (_pointer is null) + return; + _pointer = null; + if (ShowTrackingLines || ShowValueIndicator) + RaiseChanged(); + } + + /// + /// Zooms around the pointer (e.g. on mouse wheel). Each notch zooms by a factor of 1.25. + /// + /// The pointer position in screen coordinates. + /// The number of wheel notches; positive values zoom in. + public void Wheel(CanvasPoint position, double notches) + { + if (notches == 0 || !double.IsFinite(notches)) + return; + Viewport.ZoomAt(position, Math.Pow(1.25, notches)); + } + + /// + /// Draws the canvas. + /// + /// The renderer. + /// The width of the drawing area in pixels. + /// The height of the drawing area in pixels. + public void Render(IDrawingContext context, double width, double height) + { + ArgumentNullException.ThrowIfNull(context); + _rendering = true; + try + { + Viewport.Resize(width, height); + } + finally + { + _rendering = false; + } + + var bounds = new CanvasRect(0, 0, width, height); + var guardBand = new CanvasRect(-10 * width - 10, -10 * height - 10, 21 * width + 20, 21 * height + 20); + + context.Clear(Style.Background); + context.PushClip(bounds); + var gridStep = GridRenderer.Draw(context, Viewport, Style); + foreach (var item in Items) + _itemRenderer.Draw(context, item, Viewport, Style, guardBand); + DrawPointerFeatures(context, gridStep); + context.PopClip(); + } + + private T Add(T item) where T : CanvasItem + { + Items.Add(item); + return item; + } + + private void OnItemsChanged(object? sender, NotifyCollectionChangedEventArgs e) => RaiseChanged(); + + private void UpdatePointer(CanvasPoint position) + { + if (_pointer == position) + return; + _pointer = position; + if (ShowTrackingLines || ShowValueIndicator) + RaiseChanged(); + } + + private void RaiseChanged() + { + if (!_rendering) + Changed?.Invoke(this, EventArgs.Empty); + } + + private void DrawPointerFeatures(IDrawingContext context, (double X, double Y) gridStep) + { + if (_pointer is not { } pointer) + return; + + if (ShowTrackingLines) + { + var x = GridRenderer.Snap(pointer.X, Style.TrackingStroke); + var y = GridRenderer.Snap(pointer.Y, Style.TrackingStroke); + context.DrawLine(new CanvasPoint(x, 0), new CanvasPoint(x, Viewport.Height), Style.TrackingStroke); + context.DrawLine(new CanvasPoint(0, y), new CanvasPoint(Viewport.Width, y), Style.TrackingStroke); + } + + if (!ShowValueIndicator) + return; + + // One decimal more than the grid step needs, so the value changes visibly while moving at any zoom level. + var world = Viewport.ScreenToWorld(pointer); + var text = Ticks.FormatValue(world.X, gridStep.X) + " | " + Ticks.FormatValue(world.Y, gridStep.Y); + var size = context.MeasureText(text, Style.LabelFont); + var box = new CanvasRect(pointer.X + IndicatorOffset, pointer.Y + IndicatorOffset, + Math.Max(80, size.Width + 2 * IndicatorPadding), Math.Max(25, size.Height + 2 * IndicatorPadding)); + context.DrawRectangle(box, Style.IndicatorBorder, Style.IndicatorBackground); + context.DrawText(text, new CanvasPoint(box.X + box.Width / 2, box.Y + (box.Height - size.Height) / 2), + Style.LabelFont, Style.IndicatorText, TextAlignment.Center); + } +} diff --git a/src/SharpMath.Canvas/CanvasItems.cs b/src/SharpMath.Canvas/CanvasItems.cs new file mode 100644 index 0000000..1e730e1 --- /dev/null +++ b/src/SharpMath.Canvas/CanvasItems.cs @@ -0,0 +1,79 @@ +using SharpMath.Expressions; +using SharpMath.Geometry; + +namespace SharpMath.Canvas; + +/// +/// The base class of everything a draws. Items are immutable; replace an item in +/// to change it. +/// +public abstract record CanvasItem +{ + /// + /// Gets the style of this item, or to use the default style for its type from + /// . + /// + public ItemStyle? Style { get; init; } +} + +/// +/// The graph of a function y = f(x). +/// +/// The function. Non-finite results and arithmetic exceptions leave gaps in the graph. +public sealed record FunctionItem(Func Function) : CanvasItem +{ + /// + /// Gets the parsed expression if the item was created from one; otherwise, . + /// + public MathExpression? Expression { get; init; } + + /// + /// Creates the graph of an expression in the variable x, e.g. "2x^2 - 3x". + /// + /// The expression. + /// The functions, constants and variables to use; the built-ins if . + /// The item. + /// The expression is invalid. + /// The expression uses variables other than x. + public static FunctionItem FromExpression(string expression, ExpressionContext? context = null) + { + var parsed = MathExpression.Parse(expression, context); + return new FunctionItem(parsed.ToFunc("x")) { Expression = parsed }; + } +} + +/// +/// A vector drawn as an arrow from to Tail + Vector. +/// +/// The vector. +public sealed record VectorItem(Vector2 Vector) : CanvasItem +{ + /// + /// Gets the start of the arrow; the origin by default. + /// + public Point2D Tail { get; init; } +} + +/// +/// A point drawn as a dot. +/// +/// The point. +public sealed record PointItem(Point2D Point) : CanvasItem; + +/// +/// An infinite line, clipped to the visible area. +/// +/// The line. +public sealed record LineItem(Line2D Line) : CanvasItem; + +/// +/// A line segment. +/// +/// The segment. +public sealed record SegmentItem(LineSegment2D Segment) : CanvasItem; + +/// +/// A polygon with an outline and an optional fill. +/// +/// The polygon. +public sealed record PolygonItem(Polygon Polygon) : CanvasItem; diff --git a/src/SharpMath.Canvas/CanvasStyle.cs b/src/SharpMath.Canvas/CanvasStyle.cs new file mode 100644 index 0000000..12d9501 --- /dev/null +++ b/src/SharpMath.Canvas/CanvasStyle.cs @@ -0,0 +1,116 @@ +namespace SharpMath.Canvas; + +/// +/// Describes how an item is drawn. +/// +/// The stroke for lines and outlines. +/// The fill color for polygons, points and arrowheads, or for none. +/// The radius of points in pixels. +public sealed record ItemStyle(Stroke Stroke, Color? Fill = null, double PointRadius = 3); + +/// +/// Describes the appearance of a . The defaults reproduce the look of the original canvas. +/// +/// +/// The style is immutable; change it with a expression and assign it to +/// , e.g. canvas.Style = canvas.Style with { Background = Color.Black };. +/// +public sealed record CanvasStyle +{ + /// + /// Gets the default style. + /// + public static CanvasStyle Default { get; } = new(); + + /// + /// Gets the background color. + /// + public Color Background { get; init; } = Color.White; + + /// + /// Gets the stroke of the fine grid lines. + /// + public Stroke MinorGridStroke { get; init; } = new(Color.LightGray); + + /// + /// Gets the stroke of the grid lines at labelled ticks. + /// + public Stroke MajorGridStroke { get; init; } = new(Color.LightGray); + + /// + /// Gets the stroke of the coordinate axes. + /// + public Stroke AxisStroke { get; init; } = new(Color.FromRgb(245, 125, 0)); + + /// + /// Gets the font of the axis labels. + /// + public FontSpec LabelFont { get; init; } = new("Segoe UI", 11); + + /// + /// Gets the color of the axis labels. + /// + public Color LabelColor { get; init; } = Color.Black; + + /// + /// Gets the minimum distance between fine grid lines in pixels. + /// + public double MinGridSpacing { get; init; } = 20; + + /// + /// Gets the minimum distance between labelled ticks in pixels. + /// + public double MinLabelSpacing { get; init; } = 80; + + /// + /// Gets the stroke of the tracking lines through the pointer. + /// + public Stroke TrackingStroke { get; init; } = new(Color.Gray); + + /// + /// Gets the background of the value indicator. + /// + public Color IndicatorBackground { get; init; } = Color.White; + + /// + /// Gets the border of the value indicator. + /// + public Stroke IndicatorBorder { get; init; } = new(Color.LightGray); + + /// + /// Gets the text color of the value indicator. + /// + public Color IndicatorText { get; init; } = Color.LightGray; + + /// + /// Gets the default style of function graphs. + /// + public ItemStyle FunctionStyle { get; init; } = new(new Stroke(Color.Black)); + + /// + /// Gets the default style of vectors; the fill is used for the arrowhead. + /// + public ItemStyle VectorStyle { get; init; } = + new(new Stroke(Color.FromRgb(122, 125, 201)), Color.FromRgb(122, 125, 201)); + + /// + /// Gets the default style of points. + /// + public ItemStyle PointStyle { get; init; } = new(new Stroke(Color.Black), Color.Black); + + /// + /// Gets the default style of infinite lines. + /// + public ItemStyle LineStyle { get; init; } = new(new Stroke(Color.FromRgb(0, 128, 128))); + + /// + /// Gets the default style of line segments. + /// + public ItemStyle SegmentStyle { get; init; } = new(new Stroke(Color.FromRgb(0, 128, 128), 2)); + + /// + /// Gets the default style of polygons. + /// + public ItemStyle PolygonStyle { get; init; } = + new(new Stroke(Color.FromRgb(46, 139, 87)), Color.FromArgb(48, 46, 139, 87)); +} diff --git a/src/SharpMath.Canvas/Clipping.cs b/src/SharpMath.Canvas/Clipping.cs new file mode 100644 index 0000000..7de03b3 --- /dev/null +++ b/src/SharpMath.Canvas/Clipping.cs @@ -0,0 +1,97 @@ +namespace SharpMath.Canvas; + +/// +/// Clips geometry in screen coordinates to a rectangle, so renderers never receive huge coordinates. +/// +internal static class Clipping +{ + /// + /// Clips a segment with the Liang–Barsky algorithm. + /// + /// if a part of the segment lies inside the rectangle. + public static bool ClipSegment(CanvasRect bounds, ref CanvasPoint from, ref CanvasPoint to) + { + if (!IsFinite(from) || !IsFinite(to)) + return false; + + double t0 = 0, t1 = 1; + var dx = to.X - from.X; + var dy = to.Y - from.Y; + if (!Clip(-dx, from.X - bounds.X, ref t0, ref t1) || !Clip(dx, bounds.Right - from.X, ref t0, ref t1) || + !Clip(-dy, from.Y - bounds.Y, ref t0, ref t1) || !Clip(dy, bounds.Bottom - from.Y, ref t0, ref t1)) + return false; + + var start = from; + if (t1 < 1) + to = new CanvasPoint(start.X + t1 * dx, start.Y + t1 * dy); + if (t0 > 0) + from = new CanvasPoint(start.X + t0 * dx, start.Y + t0 * dy); + return true; + } + + /// + /// Clips a polygon with the Sutherland–Hodgman algorithm. + /// + public static List ClipPolygon(CanvasRect bounds, IReadOnlyList polygon) + { + var output = polygon.Where(IsFinite).ToList(); + output = ClipEdge(output, p => p.X >= bounds.X, (a, b) => AtX(a, b, bounds.X)); + output = ClipEdge(output, p => p.X <= bounds.Right, (a, b) => AtX(a, b, bounds.Right)); + output = ClipEdge(output, p => p.Y >= bounds.Y, (a, b) => AtY(a, b, bounds.Y)); + output = ClipEdge(output, p => p.Y <= bounds.Bottom, (a, b) => AtY(a, b, bounds.Bottom)); + return output; + } + + public static bool IsFinite(CanvasPoint point) => double.IsFinite(point.X) && double.IsFinite(point.Y); + + private static bool Clip(double denominator, double numerator, ref double t0, ref double t1) + { + if (denominator == 0) + return numerator >= 0; + + var t = numerator / denominator; + if (denominator < 0) + { + if (t > t1) + return false; + t0 = Math.Max(t0, t); + } + else + { + if (t < t0) + return false; + t1 = Math.Min(t1, t); + } + + return true; + } + + private static List ClipEdge(List input, Func inside, + Func intersect) + { + var output = new List(input.Count + 4); + for (var i = 0; i < input.Count; i++) + { + var current = input[i]; + var previous = input[(i + input.Count - 1) % input.Count]; + if (inside(current)) + { + if (!inside(previous)) + output.Add(intersect(previous, current)); + output.Add(current); + } + else if (inside(previous)) + { + output.Add(intersect(previous, current)); + } + } + + return output; + } + + private static CanvasPoint AtX(CanvasPoint a, CanvasPoint b, double x) => + new(x, a.Y + (b.Y - a.Y) * (x - a.X) / (b.X - a.X)); + + private static CanvasPoint AtY(CanvasPoint a, CanvasPoint b, double y) => + new(a.X + (b.X - a.X) * (y - a.Y) / (b.Y - a.Y), y); +} diff --git a/src/SharpMath.Canvas/Color.cs b/src/SharpMath.Canvas/Color.cs new file mode 100644 index 0000000..df49287 --- /dev/null +++ b/src/SharpMath.Canvas/Color.cs @@ -0,0 +1,66 @@ +using System.Globalization; + +namespace SharpMath.Canvas; + +/// +/// Represents an immutable RGBA color with 8 bits per channel. +/// +/// The red channel. +/// The green channel. +/// The blue channel. +/// The alpha channel (255 is opaque). +public readonly record struct Color(byte R, byte G, byte B, byte A = 255) +{ + /// + /// Gets opaque white. + /// + public static Color White => new(255, 255, 255); + + /// + /// Gets opaque black. + /// + public static Color Black => new(0, 0, 0); + + /// + /// Gets the gray used for tracking lines (#808080). + /// + public static Color Gray => new(128, 128, 128); + + /// + /// Gets the light gray used for grid lines (#D3D3D3). + /// + public static Color LightGray => new(211, 211, 211); + + /// + /// Gets a fully transparent color. + /// + public static Color Transparent => new(0, 0, 0, 0); + + /// + /// Creates an opaque color. + /// + /// The red channel. + /// The green channel. + /// The blue channel. + /// The color. + public static Color FromRgb(byte red, byte green, byte blue) => new(red, green, blue); + + /// + /// Creates a color with an alpha channel. + /// + /// The alpha channel (255 is opaque). + /// The red channel. + /// The green channel. + /// The blue channel. + /// The color. + public static Color FromArgb(byte alpha, byte red, byte green, byte blue) => new(red, green, blue, alpha); + + /// + /// Returns the color as #RRGGBB (opaque) or #RRGGBBAA. + /// + /// The hexadecimal representation. + public string ToHex() => + A == 255 + ? string.Create(CultureInfo.InvariantCulture, $"#{R:X2}{G:X2}{B:X2}") + : string.Create(CultureInfo.InvariantCulture, $"#{R:X2}{G:X2}{B:X2}{A:X2}"); +} diff --git a/src/SharpMath.Canvas/FunctionSampler.cs b/src/SharpMath.Canvas/FunctionSampler.cs new file mode 100644 index 0000000..8b45b66 --- /dev/null +++ b/src/SharpMath.Canvas/FunctionSampler.cs @@ -0,0 +1,130 @@ +using System.Runtime.InteropServices; + +namespace SharpMath.Canvas; + +/// +/// Samples a function across the visible area and splits the graph into polylines at gaps and poles. The buffers are +/// reused between calls, so sampling does not allocate once they have grown to size. +/// +internal sealed class FunctionSampler +{ + private const int MaxRefinementDepth = 6; + private const double RefinementPixels = 2; + + private readonly List _points = []; + private readonly List<(int Start, int Length)> _polylines = []; + private int _polylineStart; + + /// + /// Gets the number of polylines produced by the last call to . + /// + public int PolylineCount => _polylines.Count; + + /// + /// Gets a polyline produced by the last call to . The span is valid until the next call. + /// + public ReadOnlySpan GetPolyline(int index) + { + var (start, length) = _polylines[index]; + return CollectionsMarshal.AsSpan(_points).Slice(start, length); + } + + public void Sample(Func function, Viewport viewport, CanvasRect guardBand) + { + _points.Clear(); + _polylines.Clear(); + _polylineStart = 0; + var columns = (int) Math.Ceiling(viewport.Width); + + var previousX = viewport.ScreenToWorld(new CanvasPoint(0, 0)).X; + var previousY = Evaluate(function, previousX); + if (double.IsFinite(previousY)) + _points.Add(ToScreen(viewport, guardBand, previousX, previousY)); + + for (var column = 1; column <= columns; column++) + { + var x = viewport.ScreenToWorld(new CanvasPoint(column, 0)).X; + var y = Evaluate(function, x); + if (!double.IsFinite(y)) + { + Flush(); + previousX = x; + previousY = y; + continue; + } + + if (double.IsFinite(previousY) && + !Refine(function, viewport, guardBand, previousX, previousY, x, y, MaxRefinementDepth)) + Flush(); + + _points.Add(ToScreen(viewport, guardBand, x, y)); + previousX = x; + previousY = y; + } + + Flush(); + } + + /// + /// Adds intermediate samples where the graph is steep. Returns if there is a gap or a pole + /// between the two samples; the points before the break have been added in that case. + /// + private bool Refine(Func function, Viewport viewport, CanvasRect guardBand, double x0, double y0, + double x1, double y1, int depth) + { + var pixelDistance = Math.Abs(y1 - y0) * viewport.ScaleY; + if (pixelDistance <= RefinementPixels) + return true; + + var xm = (x0 + x1) / 2; + var ym = Evaluate(function, xm); + if (!double.IsFinite(ym)) + return false; + + // A pole: the jump is larger than the visible area and the midpoint does not lie between the end points. + var lower = Math.Min(y0, y1); + var upper = Math.Max(y0, y1); + var midpointBetween = ym >= lower && ym <= upper; + if (pixelDistance > viewport.Height && !midpointBetween) + return false; + + if (depth == 0) + return true; + + if (!Refine(function, viewport, guardBand, x0, y0, xm, ym, depth - 1)) + return false; + _points.Add(ToScreen(viewport, guardBand, xm, ym)); + return Refine(function, viewport, guardBand, xm, ym, x1, y1, depth - 1); + } + + private static double Evaluate(Func function, double x) + { + try + { + return function(x); + } + catch (ArithmeticException) + { + return double.NaN; + } + } + + private static CanvasPoint ToScreen(Viewport viewport, CanvasRect guardBand, double x, double y) + { + var point = viewport.WorldToScreen(new Geometry.Point2D(x, y)); + return new CanvasPoint(point.X, Math.Clamp(point.Y, guardBand.Y, guardBand.Bottom)); + } + + /// + /// Ends the current polyline; single points are dropped. + /// + private void Flush() + { + var length = _points.Count - _polylineStart; + if (length >= 2) + _polylines.Add((_polylineStart, length)); + else + _points.RemoveRange(_polylineStart, length); + _polylineStart = _points.Count; + } +} diff --git a/src/SharpMath.Canvas/GridRenderer.cs b/src/SharpMath.Canvas/GridRenderer.cs new file mode 100644 index 0000000..7e0b767 --- /dev/null +++ b/src/SharpMath.Canvas/GridRenderer.cs @@ -0,0 +1,116 @@ +using SharpMath.Geometry; + +namespace SharpMath.Canvas; + +/// +/// Draws the grid, the axis labels and the axes of a . +/// +internal static class GridRenderer +{ + private const double LabelPadding = 3; + + /// + /// Draws grid lines, labels and axes and returns the grid step in world units (X, Y). + /// + public static (double StepX, double StepY) Draw(IDrawingContext context, Viewport viewport, CanvasStyle style) + { + var area = viewport.VisibleArea; + var gridSpacing = Ticks.SafeSpacing(style.MinGridSpacing); + var labelSpacing = Math.Max(gridSpacing, Ticks.SafeSpacing(style.MinLabelSpacing)); + var minorX = Ticks.NiceStep(gridSpacing, viewport.ScaleX); + var minorY = Ticks.NiceStep(gridSpacing, viewport.ScaleY); + + // Labelled ticks are multiples of the grid step, so every label sits on a (major) grid line. + var majorX = Ticks.NiceMultiple(labelSpacing, viewport.ScaleX, minorX); + var majorY = Ticks.NiceMultiple(labelSpacing, viewport.ScaleY, minorY); + + var (firstX, lastX) = Ticks.Range(area.MinX, area.MaxX, minorX); + for (var i = firstX; i <= lastX; i++) + { + var stroke = IsMultiple(i * minorX, majorX) ? style.MajorGridStroke : style.MinorGridStroke; + var x = Snap(viewport.WorldToScreen(new Point2D(i * minorX, 0)).X, stroke); + context.DrawLine(new CanvasPoint(x, 0), new CanvasPoint(x, viewport.Height), stroke); + } + + var (firstY, lastY) = Ticks.Range(area.MinY, area.MaxY, minorY); + for (var i = firstY; i <= lastY; i++) + { + var stroke = IsMultiple(i * minorY, majorY) ? style.MajorGridStroke : style.MinorGridStroke; + var y = Snap(viewport.WorldToScreen(new Point2D(0, i * minorY)).Y, stroke); + context.DrawLine(new CanvasPoint(0, y), new CanvasPoint(viewport.Width, y), stroke); + } + + DrawLabels(context, viewport, style, area, majorX, majorY); + DrawAxes(context, viewport, style.AxisStroke); + return (minorX, minorY); + } + + /// + /// Snaps the position of an axis-aligned line to the pixel grid so anti-aliasing renderers draw it crisply: lines + /// with an odd width are centered on a pixel, lines with an even width on a pixel border. + /// + public static double Snap(double coordinate, Stroke stroke) + { + var width = Math.Max(1, Math.Round(stroke.Width)); + return width % 2 == 1 ? Math.Floor(coordinate) + 0.5 : Math.Round(coordinate); + } + + private static void DrawLabels(IDrawingContext context, Viewport viewport, CanvasStyle style, BoundingBox2D area, + double stepX, double stepY) + { + var font = style.LabelFont; + var lineHeight = context.MeasureText("0", font).Height; + + // Labels stay next to the axes; if an axis is off-screen they stick to the nearest edge. Labels that would be + // cut off at the edges are skipped. + var labelTop = Math.Clamp(viewport.Origin.Y + LabelPadding, LabelPadding, + Math.Max(LabelPadding, viewport.Height - lineHeight - LabelPadding)); + var (firstX, lastX) = Ticks.Range(area.MinX, area.MaxX, stepX); + for (var i = firstX; i <= lastX; i++) + { + if (i == 0) + continue; + var x = viewport.WorldToScreen(new Point2D(i * stepX, 0)).X; + var text = Ticks.Format(i, stepX); + var halfWidth = context.MeasureText(text, font).Width / 2; + if (x - halfWidth < 0 || x + halfWidth > viewport.Width) + continue; + context.DrawText(text, new CanvasPoint(x, labelTop), font, style.LabelColor, TextAlignment.Center); + } + + var (firstY, lastY) = Ticks.Range(area.MinY, area.MaxY, stepY); + for (var i = firstY; i <= lastY; i++) + { + var top = i == 0 ? labelTop : viewport.WorldToScreen(new Point2D(0, i * stepY)).Y - lineHeight / 2; + if (top < 0 || top + lineHeight > viewport.Height) + continue; + var text = Ticks.Format(i, stepY); + var width = context.MeasureText(text, font).Width; + var labelLeft = Math.Clamp(viewport.Origin.X + LabelPadding, LabelPadding, + Math.Max(LabelPadding, viewport.Width - width - LabelPadding)); + context.DrawText(text, new CanvasPoint(labelLeft, top), font, style.LabelColor, TextAlignment.Left); + } + } + + private static void DrawAxes(IDrawingContext context, Viewport viewport, Stroke stroke) + { + var origin = viewport.Origin; + if (origin.Y >= 0 && origin.Y <= viewport.Height) + { + var y = Snap(origin.Y, stroke); + context.DrawLine(new CanvasPoint(0, y), new CanvasPoint(viewport.Width, y), stroke); + } + + if (origin.X >= 0 && origin.X <= viewport.Width) + { + var x = Snap(origin.X, stroke); + context.DrawLine(new CanvasPoint(x, 0), new CanvasPoint(x, viewport.Height), stroke); + } + } + + private static bool IsMultiple(double value, double step) + { + var ratio = value / step; + return Math.Abs(ratio - Math.Round(ratio)) < 1e-6; + } +} diff --git a/src/SharpMath.Canvas/IDrawingContext.cs b/src/SharpMath.Canvas/IDrawingContext.cs new file mode 100644 index 0000000..cf2be71 --- /dev/null +++ b/src/SharpMath.Canvas/IDrawingContext.cs @@ -0,0 +1,88 @@ +namespace SharpMath.Canvas; + +/// +/// The drawing operations a renderer provides to the . All coordinates are in pixels with the +/// origin in the top-left corner and Y pointing down. +/// +/// +/// The canvas clips coordinates to a guard band around the visible area before calling a renderer, so implementations +/// never receive huge or non-finite coordinates. +/// +public interface IDrawingContext +{ + /// + /// Fills the whole drawing area. + /// + /// The color. + void Clear(Color color); + + /// + /// Draws a straight line. + /// + /// The start point. + /// The end point. + /// The stroke. + void DrawLine(CanvasPoint startPoint, CanvasPoint endPoint, Stroke stroke); + + /// + /// Draws connected line segments through the points. + /// + /// The points; at least two. + /// The stroke. + void DrawPolyline(ReadOnlySpan points, Stroke stroke); + + /// + /// Draws a closed polygon. + /// + /// The vertices; at least three. + /// The outline, or for none. + /// The fill color, or for none. + void DrawPolygon(ReadOnlySpan points, Stroke? stroke, Color? fill); + + /// + /// Draws an axis-aligned rectangle. + /// + /// The rectangle. + /// The outline, or for none. + /// The fill color, or for none. + void DrawRectangle(CanvasRect rect, Stroke? stroke, Color? fill); + + /// + /// Draws an axis-aligned ellipse. + /// + /// The center. + /// The horizontal radius. + /// The vertical radius. + /// The outline, or for none. + /// The fill color, or for none. + void DrawEllipse(CanvasPoint center, double radiusX, double radiusY, Stroke? stroke, Color? fill); + + /// + /// Draws a single line of text. + /// + /// The text. + /// The anchor: the top edge of the text and, depending on the alignment, its left edge, center or right edge. + /// The font. + /// The color. + /// The horizontal alignment relative to the anchor. + void DrawText(string text, CanvasPoint position, FontSpec font, Color color, TextAlignment alignment); + + /// + /// Measures a single line of text. + /// + /// The text. + /// The font. + /// The size of the text. + TextSize MeasureText(string text, FontSpec font); + + /// + /// Restricts subsequent drawing to a rectangle until the matching . + /// + /// The clip rectangle. + void PushClip(CanvasRect rect); + + /// + /// Removes the clip rectangle added by the last . + /// + void PopClip(); +} diff --git a/src/SharpMath.Canvas/ItemRenderer.cs b/src/SharpMath.Canvas/ItemRenderer.cs new file mode 100644 index 0000000..220f8be --- /dev/null +++ b/src/SharpMath.Canvas/ItemRenderer.cs @@ -0,0 +1,119 @@ +namespace SharpMath.Canvas; + +/// +/// Draws the items of a , clipping them to a guard band around the visible area. +/// +internal sealed class ItemRenderer +{ + private const double ArrowLength = 10; + private const double ArrowHalfWidth = 4; + + private readonly List _polygon = []; + private readonly FunctionSampler _sampler = new(); + + public void Draw(IDrawingContext context, CanvasItem item, Viewport viewport, CanvasStyle style, + CanvasRect guardBand) + { + switch (item) + { + case FunctionItem function: + var stroke = (function.Style ?? style.FunctionStyle).Stroke; + _sampler.Sample(function.Function, viewport, guardBand); + for (var i = 0; i < _sampler.PolylineCount; i++) + context.DrawPolyline(_sampler.GetPolyline(i), stroke); + break; + case VectorItem vector: + DrawVector(context, vector, viewport, vector.Style ?? style.VectorStyle, guardBand); + break; + case PointItem point: + DrawPoint(context, point, viewport, point.Style ?? style.PointStyle); + break; + case LineItem line: + DrawLine(context, line, viewport, (line.Style ?? style.LineStyle).Stroke); + break; + case SegmentItem segment: + DrawSegment(context, viewport.WorldToScreen(segment.Segment.Start), + viewport.WorldToScreen(segment.Segment.End), (segment.Style ?? style.SegmentStyle).Stroke, + guardBand); + break; + case PolygonItem polygon: + DrawPolygon(context, polygon, viewport, polygon.Style ?? style.PolygonStyle, guardBand); + break; + } + } + + private static void DrawVector(IDrawingContext context, VectorItem item, Viewport viewport, ItemStyle style, + CanvasRect guardBand) + { + var tail = viewport.WorldToScreen(item.Tail); + var tip = viewport.WorldToScreen(item.Tail + item.Vector); + var dx = tip.X - tail.X; + var dy = tip.Y - tail.Y; + var length = Math.Sqrt(dx * dx + dy * dy); + if (!double.IsFinite(length) || length == 0) + return; + + // The shaft ends at the base of the arrowhead. + var ux = dx / length; + var uy = dy / length; + var headLength = Math.Min(ArrowLength, length); + var basePoint = new CanvasPoint(tip.X - ux * headLength, tip.Y - uy * headLength); + DrawSegment(context, tail, basePoint, style.Stroke, guardBand); + + if (!guardBand.Contains(tip)) + return; + var halfWidth = ArrowHalfWidth * headLength / ArrowLength; + ReadOnlySpan head = + [ + tip, + new(basePoint.X - uy * halfWidth, basePoint.Y + ux * halfWidth), + new(basePoint.X + uy * halfWidth, basePoint.Y - ux * halfWidth) + ]; + context.DrawPolygon(head, style.Stroke, style.Fill ?? style.Stroke.Color); + } + + private static void DrawPoint(IDrawingContext context, PointItem item, Viewport viewport, ItemStyle style) + { + var center = viewport.WorldToScreen(item.Point); + var radius = style.PointRadius; + var visible = new CanvasRect(-radius, -radius, viewport.Width + 2 * radius, viewport.Height + 2 * radius); + if (visible.Contains(center)) + context.DrawEllipse(center, radius, radius, style.Stroke, style.Fill); + } + + private static void DrawLine(IDrawingContext context, LineItem item, Viewport viewport, Stroke stroke) + { + var line = item.Line; + if (!double.IsFinite(line.A) || (line.A == 0 && line.B == 0)) + return; + + // Two points beyond the visible area, clipped to the drawing area. + var area = viewport.VisibleArea; + var center = line.ClosestPoint(area.Center); + var reach = Math.Sqrt(area.Width * area.Width + area.Height * area.Height) + 1; + var from = viewport.WorldToScreen(center - line.Direction * reach); + var to = viewport.WorldToScreen(center + line.Direction * reach); + var bounds = new CanvasRect(-1, -1, viewport.Width + 2, viewport.Height + 2); + if (Clipping.ClipSegment(bounds, ref from, ref to)) + context.DrawLine(from, to, stroke); + } + + private static void DrawSegment(IDrawingContext context, CanvasPoint from, CanvasPoint to, Stroke stroke, + CanvasRect guardBand) + { + if (Clipping.ClipSegment(guardBand, ref from, ref to)) + context.DrawLine(from, to, stroke); + } + + private void DrawPolygon(IDrawingContext context, PolygonItem item, Viewport viewport, ItemStyle style, + CanvasRect guardBand) + { + _polygon.Clear(); + foreach (var point in item.Polygon.Points) + _polygon.Add(viewport.WorldToScreen(point)); + var clipped = Clipping.ClipPolygon(guardBand, _polygon); + if (clipped.Count >= 3) + context.DrawPolygon(System.Runtime.InteropServices.CollectionsMarshal.AsSpan(clipped), style.Stroke, + style.Fill); + } +} diff --git a/src/SharpMath.Canvas/Primitives.cs b/src/SharpMath.Canvas/Primitives.cs new file mode 100644 index 0000000..341faaf --- /dev/null +++ b/src/SharpMath.Canvas/Primitives.cs @@ -0,0 +1,100 @@ +namespace SharpMath.Canvas; + +/// +/// A point in screen coordinates (pixels, Y pointing down). +/// +/// The horizontal position. +/// The vertical position. +public readonly record struct CanvasPoint(double X, double Y); + +/// +/// A rectangle in screen coordinates (pixels, Y pointing down). +/// +/// The left edge. +/// The top edge. +/// The width. +/// The height. +public readonly record struct CanvasRect(double X, double Y, double Width, double Height) +{ + /// + /// Gets the right edge. + /// + public double Right => X + Width; + + /// + /// Gets the bottom edge. + /// + public double Bottom => Y + Height; + + /// + /// Determines whether a point lies inside the rectangle or on its border. + /// + /// The point. + /// if the rectangle contains the point; otherwise, . + public bool Contains(CanvasPoint point) => + point.X >= X && point.X <= Right && point.Y >= Y && point.Y <= Bottom; +} + +/// +/// The dash pattern of a stroke. +/// +public enum DashStyle +{ + /// + /// A continuous line. + /// + Solid, + + /// + /// A dashed line. + /// + Dashed, + + /// + /// A dotted line. + /// + Dotted +} + +/// +/// Describes how lines are drawn. +/// +/// The color. +/// The width in pixels. +/// The dash pattern. +public readonly record struct Stroke(Color Color, double Width = 1, DashStyle Dash = DashStyle.Solid); + +/// +/// Describes a font. +/// +/// The font family; renderers fall back to a default font if it is not available. +/// The size in pixels. +public readonly record struct FontSpec(string Family, double Size); + +/// +/// The measured size of a text in pixels. +/// +/// The width. +/// The height (line height). +public readonly record struct TextSize(double Width, double Height); + +/// +/// The horizontal alignment of a text relative to its position. +/// +public enum TextAlignment +{ + /// + /// The position is the left edge of the text. + /// + Left, + + /// + /// The position is the horizontal center of the text. + /// + Center, + + /// + /// The position is the right edge of the text. + /// + Right +} diff --git a/src/SharpMath.Canvas/SharpMath.Canvas.csproj b/src/SharpMath.Canvas/SharpMath.Canvas.csproj new file mode 100644 index 0000000..ed97d6b --- /dev/null +++ b/src/SharpMath.Canvas/SharpMath.Canvas.csproj @@ -0,0 +1,11 @@ + + + + Platform-neutral 2D canvas for SharpMath: plots functions, vectors, points, lines and polygons through a renderer abstraction. Includes an SVG renderer. + + + + + + + diff --git a/src/SharpMath.Canvas/SvgDrawingContext.cs b/src/SharpMath.Canvas/SvgDrawingContext.cs new file mode 100644 index 0000000..e2c8c0a --- /dev/null +++ b/src/SharpMath.Canvas/SvgDrawingContext.cs @@ -0,0 +1,173 @@ +using System.Globalization; +using System.Security; +using System.Text; + +namespace SharpMath.Canvas; + +/// +/// An that produces an SVG document. It needs no native dependencies. +/// +/// +/// SVG has no access to font metrics, so estimates the width as 0.6 times the font size per +/// character and the height as 1.2 times the font size. +/// +public sealed class SvgDrawingContext : IDrawingContext +{ + private readonly StringBuilder _body = new(); + private readonly StringBuilder _definitions = new(); + private readonly double _height; + private readonly double _width; + private int _clipCount; + private int _openGroups; + + /// + /// Initializes a new instance of the class. + /// + /// The width of the document in pixels. + /// The height of the document in pixels. + public SvgDrawingContext(double width, double height) + { + ArgumentOutOfRangeException.ThrowIfNegative(width); + ArgumentOutOfRangeException.ThrowIfNegative(height); + _width = width; + _height = height; + } + + /// + public void Clear(Color color) + { + _body.Clear(); + _definitions.Clear(); + _openGroups = 0; + _body.Append(CultureInfo.InvariantCulture, + $"\n"); + } + + /// + public void DrawLine(CanvasPoint startPoint, CanvasPoint endPoint, Stroke stroke) => + _body.Append(CultureInfo.InvariantCulture, + $"\n"); + + /// + public void DrawPolyline(ReadOnlySpan points, Stroke stroke) + { + if (points.Length < 2) + return; + _body.Append("\n"); + } + + /// + public void DrawPolygon(ReadOnlySpan points, Stroke? stroke, Color? fill) + { + if (points.Length < 3) + return; + _body.Append("\n"); + } + + /// + public void DrawRectangle(CanvasRect rect, Stroke? stroke, Color? fill) => + _body.Append(CultureInfo.InvariantCulture, + $"\n"); + + /// + public void DrawEllipse(CanvasPoint center, double radiusX, double radiusY, Stroke? stroke, Color? fill) => + _body.Append(CultureInfo.InvariantCulture, + $"\n"); + + /// + public void DrawText(string text, CanvasPoint position, FontSpec font, Color color, TextAlignment alignment) + { + ArgumentNullException.ThrowIfNull(text); + var anchor = alignment switch + { + TextAlignment.Center => "middle", + TextAlignment.Right => "end", + _ => "start" + }; + + // SVG positions text at its baseline; 0.8 · size approximates the ascent. + var baseline = position.Y + 0.8 * font.Size; + _body.Append(CultureInfo.InvariantCulture, + $"{Escape(text)}\n"); + } + + /// + public TextSize MeasureText(string text, FontSpec font) + { + ArgumentNullException.ThrowIfNull(text); + return new TextSize(0.6 * font.Size * text.Length, 1.2 * font.Size); + } + + /// + public void PushClip(CanvasRect rect) + { + var id = "clip" + (++_clipCount).ToString(CultureInfo.InvariantCulture); + _definitions.Append(CultureInfo.InvariantCulture, + $"\n"); + _body.Append(CultureInfo.InvariantCulture, $"\n"); + _openGroups++; + } + + /// + public void PopClip() + { + if (_openGroups == 0) + throw new InvalidOperationException("PopClip was called without a matching PushClip."); + _body.Append("\n"); + _openGroups--; + } + + /// + /// Returns the SVG document drawn so far. Unclosed clip regions are closed. + /// + /// The SVG document. + public override string ToString() + { + var document = new StringBuilder(); + document.Append(CultureInfo.InvariantCulture, + $"\n"); + if (_definitions.Length > 0) + document.Append("\n").Append(_definitions).Append("\n"); + document.Append(_body); + for (var i = 0; i < _openGroups; i++) + document.Append("\n"); + return document.Append("\n").ToString(); + } + + private static string F(double value) => Math.Round(value, 3).ToString("0.###", CultureInfo.InvariantCulture); + + private static string Points(ReadOnlySpan points) + { + var builder = new StringBuilder(points.Length * 12); + foreach (var point in points) + builder.Append(F(point.X)).Append(',').Append(F(point.Y)).Append(' '); + return builder.ToString(0, builder.Length - 1); + } + + private static string Fill(Color color) => + color.A == 255 + ? $" fill=\"{color.ToHex()[..7]}\"" + : string.Create(CultureInfo.InvariantCulture, + $" fill=\"{color.ToHex()[..7]}\" fill-opacity=\"{F(color.A / 255.0)}\""); + + private static string StrokeAttributes(Stroke stroke) + { + var builder = new StringBuilder(); + builder.Append(CultureInfo.InvariantCulture, + $" stroke=\"{stroke.Color.ToHex()[..7]}\" stroke-width=\"{F(stroke.Width)}\""); + if (stroke.Color.A != 255) + builder.Append(CultureInfo.InvariantCulture, $" stroke-opacity=\"{F(stroke.Color.A / 255.0)}\""); + if (stroke.Dash == DashStyle.Dashed) + builder.Append(CultureInfo.InvariantCulture, $" stroke-dasharray=\"{F(4 * stroke.Width)},{F(2 * stroke.Width)}\""); + else if (stroke.Dash == DashStyle.Dotted) + builder.Append(CultureInfo.InvariantCulture, $" stroke-dasharray=\"{F(stroke.Width)},{F(2 * stroke.Width)}\""); + return builder.ToString(); + } + + private static string Paint(Stroke? stroke, Color? fill) => + (fill is { } color ? Fill(color) : " fill=\"none\"") + (stroke is { } s ? StrokeAttributes(s) : string.Empty); + + private static string Escape(string text) => SecurityElement.Escape(text); +} diff --git a/src/SharpMath.Canvas/SvgExport.cs b/src/SharpMath.Canvas/SvgExport.cs new file mode 100644 index 0000000..ccfc45d --- /dev/null +++ b/src/SharpMath.Canvas/SvgExport.cs @@ -0,0 +1,35 @@ +namespace SharpMath.Canvas; + +/// +/// Renders a to SVG. +/// +public static class SvgExport +{ + /// + /// Renders the canvas to an SVG document. + /// + /// The canvas. + /// The width in pixels. + /// The height in pixels. + /// The SVG document. + public static string RenderSvg(this Canvas2D canvas, double width, double height) + { + ArgumentNullException.ThrowIfNull(canvas); + var context = new SvgDrawingContext(width, height); + canvas.Render(context, width, height); + return context.ToString(); + } + + /// + /// Renders the canvas to an SVG document and writes it to a . + /// + /// The canvas. + /// The writer. + /// The width in pixels. + /// The height in pixels. + public static void RenderSvg(this Canvas2D canvas, TextWriter writer, double width, double height) + { + ArgumentNullException.ThrowIfNull(writer); + writer.Write(canvas.RenderSvg(width, height)); + } +} diff --git a/src/SharpMath.Canvas/Ticks.cs b/src/SharpMath.Canvas/Ticks.cs new file mode 100644 index 0000000..adc756a --- /dev/null +++ b/src/SharpMath.Canvas/Ticks.cs @@ -0,0 +1,113 @@ +using System.Globalization; + +namespace SharpMath.Canvas; + +/// +/// Calculates "nice" tick steps (1, 2 or 5 times a power of ten) and formats tick labels. +/// +internal static class Ticks +{ + /// + /// The largest number of ticks returned by ; beyond that (or once the tick indices lose + /// precision) no ticks are drawn. + /// + public const long MaxTicks = 10_000; + + /// + /// Returns the smallest nice step in world units whose on-screen length is at least . + /// + public static double NiceStep(double minPixels, double scale) + { + var raw = minPixels / scale; + var magnitude = Math.Pow(10, Math.Floor(Math.Log10(raw))); + foreach (var multiplier in (ReadOnlySpan) [1, 2, 5, 10]) + { + var step = multiplier * magnitude; + if (step >= raw * (1 - 1e-12)) + return step; + } + + return 10 * magnitude; + } + + + /// + /// Returns the smallest nice step that is a whole multiple of and whose on-screen + /// length is at least . + /// + public static double NiceMultiple(double minPixels, double scale, double baseStep) + { + var step = baseStep; + while (step * scale < minPixels * (1 - 1e-12) || !IsWholeMultiple(step, baseStep)) + step = NextNiceStep(step); + return step; + } + + /// + /// Formats a value with one decimal more than needs. + /// + public static string FormatValue(double value, double step) + { + var decimals = Math.Clamp(Decimals(step) + 1, 0, 15); + return Math.Round(value, decimals).ToString("F" + decimals.ToString(CultureInfo.InvariantCulture), + CultureInfo.InvariantCulture); + } + + /// + /// Returns a usable minimum spacing in pixels: at least 1, and 1 for invalid values. + /// + public static double SafeSpacing(double spacing) => double.IsFinite(spacing) && spacing >= 1 ? spacing : 1; + + /// + /// Returns the index range [first, last] of multiples of inside [min, max], or an + /// empty range if it would be too large or imprecise. + /// + public static (long First, long Last) Range(double min, double max, double step) + { + var first = Math.Ceiling(min / step); + var last = Math.Floor(max / step); + if (!double.IsFinite(first) || !double.IsFinite(last) || Math.Abs(first) > 1e15 || Math.Abs(last) > 1e15 || + last - first > MaxTicks) + return (1, 0); + return ((long) first, (long) last); + } + + /// + /// Formats the tick value index · step with as many decimals as the step needs. + /// + public static string Format(long index, double step) + { + var value = index * step; + if (index == 0) + return "0"; + + var decimals = Decimals(step); + var magnitude = Math.Abs(value); + if (decimals > 6 || magnitude >= 1e7) + return value.ToString("0.###E+0", CultureInfo.InvariantCulture); + return value.ToString("F" + decimals.ToString(CultureInfo.InvariantCulture), CultureInfo.InvariantCulture); + } + + private static int Decimals(double step) => Math.Max(0, (int) -Math.Floor(Math.Log10(step) + 1e-9)); + + private static bool IsWholeMultiple(double step, double baseStep) + { + var ratio = step / baseStep; + return Math.Abs(ratio - Math.Round(ratio)) < 1e-9; + } + + /// + /// Returns the next larger nice step (… 1, 2, 5, 10, 20, 50 …). + /// + private static double NextNiceStep(double step) + { + var magnitude = Math.Pow(10, Math.Floor(Math.Log10(step) + 1e-9)); + var leading = Math.Round(step / magnitude); + return leading switch + { + < 2 => 2 * magnitude, + < 5 => 5 * magnitude, + _ => 10 * magnitude + }; + } +} diff --git a/src/SharpMath.Canvas/Viewport.cs b/src/SharpMath.Canvas/Viewport.cs new file mode 100644 index 0000000..833121d --- /dev/null +++ b/src/SharpMath.Canvas/Viewport.cs @@ -0,0 +1,197 @@ +using SharpMath.Geometry; + +namespace SharpMath.Canvas; + +/// +/// Maps between world coordinates (Y up) and screen coordinates (pixels, Y down), and implements panning and zooming. +/// +public sealed class Viewport +{ + /// + /// The smallest allowed scale in pixels per unit. + /// + public const double MinScale = 1e-6; + + /// + /// The largest allowed scale in pixels per unit. + /// + public const double MaxScale = 1e9; + + private bool _originSet; + private CanvasPoint _panBeforeSizing; + private bool _sized; + + /// + /// Occurs when the origin, the scale or the size changes. + /// + public event EventHandler? Changed; + + /// + /// Gets the screen position of the world origin (0, 0). Until it is set or the viewport is panned or zoomed, the + /// origin is centered whenever the size changes. Panning before the first is applied relative + /// to the centered origin. + /// + public CanvasPoint Origin { get; private set; } + + /// + /// Gets the horizontal scale in pixels per unit. + /// + public double ScaleX { get; private set; } = 20; + + /// + /// Gets the vertical scale in pixels per unit. + /// + public double ScaleY { get; private set; } = 20; + + /// + /// Gets the width of the visible area in pixels. + /// + public double Width { get; private set; } + + /// + /// Gets the height of the visible area in pixels. + /// + public double Height { get; private set; } + + /// + /// Gets the visible world area. + /// + public BoundingBox2D VisibleArea + { + get + { + var topLeft = ScreenToWorld(new CanvasPoint(0, 0)); + var bottomRight = ScreenToWorld(new CanvasPoint(Width, Height)); + return new BoundingBox2D(topLeft.X, bottomRight.Y, bottomRight.X, topLeft.Y); + } + } + + /// + /// Sets the screen position of the world origin. + /// + /// The screen position; both coordinates must be finite. + /// A coordinate is not finite. + public void SetOrigin(CanvasPoint origin) + { + if (!double.IsFinite(origin.X) || !double.IsFinite(origin.Y)) + throw new ArgumentOutOfRangeException(nameof(origin), origin, "The origin must be finite."); + _originSet = true; + Update(origin, ScaleX, ScaleY, Width, Height); + } + + /// + /// Sets the scale. + /// + /// The horizontal scale in pixels per unit, between and . + /// The vertical scale in pixels per unit, between and . + /// A scale is out of range. + public void SetScale(double scaleX, double scaleY) + { + ValidateScale(scaleX, nameof(scaleX)); + ValidateScale(scaleY, nameof(scaleY)); + Update(Origin, scaleX, scaleY, Width, Height); + } + + /// + /// Sets the size of the visible area. Renderers call this before drawing. + /// + /// The width in pixels; must be finite and not negative. + /// The height in pixels; must be finite and not negative. + /// A dimension is invalid. + public void Resize(double width, double height) + { + if (!(width >= 0) || !double.IsFinite(width)) + throw new ArgumentOutOfRangeException(nameof(width), width, "The width must be finite and not negative."); + if (!(height >= 0) || !double.IsFinite(height)) + throw new ArgumentOutOfRangeException(nameof(height), height, "The height must be finite and not negative."); + + var origin = _originSet + ? Origin + : new CanvasPoint(width / 2 + _panBeforeSizing.X, height / 2 + _panBeforeSizing.Y); + if (!_sized && _panBeforeSizing != default) + _originSet = true; + _sized = true; + Update(origin, ScaleX, ScaleY, width, height); + } + + /// + /// Moves the visible area by the specified number of pixels. + /// + /// The horizontal distance. + /// The vertical distance. + public void Pan(double deltaX, double deltaY) + { + if (!double.IsFinite(deltaX) || !double.IsFinite(deltaY)) + return; + if (!_sized && !_originSet) + _panBeforeSizing = new CanvasPoint(_panBeforeSizing.X + deltaX, _panBeforeSizing.Y + deltaY); + else + _originSet = true; + Update(new CanvasPoint(Origin.X + deltaX, Origin.Y + deltaY), ScaleX, ScaleY, Width, Height); + } + + /// + /// Zooms by a factor while keeping the world point under fixed. The scale is clamped to + /// [, ]. + /// + /// The screen position that stays fixed, e.g. the mouse position. + /// The zoom factor; greater than 1 zooms in. Must be positive and finite. + /// is not positive and finite. + public void ZoomAt(CanvasPoint anchor, double factor) + { + if (!(factor > 0) || !double.IsFinite(factor)) + throw new ArgumentOutOfRangeException(nameof(factor), factor, "The zoom factor must be positive and finite."); + + var scaleX = Math.Clamp(ScaleX * factor, MinScale, MaxScale); + var scaleY = Math.Clamp(ScaleY * factor, MinScale, MaxScale); + if (!_sized && !_originSet) + { + // Without a size the anchor has no meaning yet; only the scale changes. + Update(Origin, scaleX, scaleY, Width, Height); + return; + } + + var factorX = scaleX / ScaleX; + var factorY = scaleY / ScaleY; + var origin = new CanvasPoint(anchor.X - (anchor.X - Origin.X) * factorX, + anchor.Y - (anchor.Y - Origin.Y) * factorY); + _originSet = true; + Update(origin, scaleX, scaleY, Width, Height); + } + + /// + /// Converts a world position to screen coordinates. + /// + /// The world position. + /// The screen position. + public CanvasPoint WorldToScreen(Point2D point) => + new(Origin.X + point.X * ScaleX, Origin.Y - point.Y * ScaleY); + + /// + /// Converts a screen position to world coordinates. + /// + /// The screen position. + /// The world position. + public Point2D ScreenToWorld(CanvasPoint point) => + new((point.X - Origin.X) / ScaleX, (Origin.Y - point.Y) / ScaleY); + + private static void ValidateScale(double scale, string parameterName) + { + if (!(scale >= MinScale && scale <= MaxScale)) + throw new ArgumentOutOfRangeException(parameterName, scale, + $"The scale must be between {MinScale} and {MaxScale} pixels per unit."); + } + + private void Update(CanvasPoint origin, double scaleX, double scaleY, double width, double height) + { + if (origin == Origin && scaleX == ScaleX && scaleY == ScaleY && width == Width && height == Height) + return; + + Origin = origin; + ScaleX = scaleX; + ScaleY = scaleY; + Width = width; + Height = height; + Changed?.Invoke(this, EventArgs.Empty); + } +} diff --git a/src/SharpMath/Equations/EquationNotSolvableException.cs b/src/SharpMath/Equations/EquationNotSolvableException.cs new file mode 100644 index 0000000..98cc9f8 --- /dev/null +++ b/src/SharpMath/Equations/EquationNotSolvableException.cs @@ -0,0 +1,31 @@ +namespace SharpMath.Equations; + +/// +/// The exception that is thrown when an equation system has no unique solution. +/// +public class EquationNotSolvableException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + public EquationNotSolvableException() : base("The equation system has no unique solution.") + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + public EquationNotSolvableException(string message) : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + /// The exception that caused this exception. + public EquationNotSolvableException(string message, Exception innerException) : base(message, innerException) + { + } +} diff --git a/src/SharpMath/Equations/LinearEquation.cs b/src/SharpMath/Equations/LinearEquation.cs new file mode 100644 index 0000000..f936d5e --- /dev/null +++ b/src/SharpMath/Equations/LinearEquation.cs @@ -0,0 +1,31 @@ +using System.Collections.ObjectModel; + +namespace SharpMath.Equations; + +/// +/// Represents an immutable linear equation a₁·x₁ + a₂·x₂ + … + aₙ·xₙ = result. +/// +public sealed class LinearEquation +{ + /// + /// Initializes a new instance of the class. + /// + /// The coefficients of the variables, in order, e.g. [2.0, -1, 3]. + /// The right-hand side. + public LinearEquation(IEnumerable coefficients, double result) + { + ArgumentNullException.ThrowIfNull(coefficients); + Coefficients = Array.AsReadOnly(coefficients.ToArray()); + Result = result; + } + + /// + /// Gets the coefficients of the variables, in order. + /// + public ReadOnlyCollection Coefficients { get; } + + /// + /// Gets the right-hand side. + /// + public double Result { get; } +} diff --git a/src/SharpMath/Equations/LinearEquationSystem.cs b/src/SharpMath/Equations/LinearEquationSystem.cs new file mode 100644 index 0000000..624fff6 --- /dev/null +++ b/src/SharpMath/Equations/LinearEquationSystem.cs @@ -0,0 +1,75 @@ +using System.Collections.ObjectModel; +using SharpMath.Geometry; + +namespace SharpMath.Equations; + +/// +/// Represents an immutable system of n linear equations in n variables. +/// +public sealed class LinearEquationSystem +{ + /// + /// Initializes a new instance of the class. + /// + /// The equations; at least one, each with exactly one coefficient per equation. + /// The equations do not form a square system. + public LinearEquationSystem(IEnumerable equations) + { + ArgumentNullException.ThrowIfNull(equations); + Equations = Validate(equations.ToArray()); + } + + /// + /// Initializes a new instance of the class. + /// + /// The equations; at least one, each with exactly one coefficient per equation. + /// The equations do not form a square system. + public LinearEquationSystem(params ReadOnlySpan equations) + { + Equations = Validate(equations.ToArray()); + } + + /// + /// Gets the equations. + /// + public ReadOnlyCollection Equations { get; } + + /// + /// Solves the system using an LU decomposition with partial pivoting. + /// + /// The value of each variable, in the order of the coefficients. + /// The system has no unique solution. + public double[] Solve() + { + var count = Equations.Count; + var coefficients = new double[count * count]; + var results = new double[count]; + for (var row = 0; row < count; row++) + { + Equations[row].Coefficients.CopyTo(coefficients, row * count); + results[row] = Equations[row].Result; + } + + var matrix = MatrixMxN.WrapRowMajor(count, count, coefficients); + if (!matrix.TrySolve(results, out var solution)) + throw new EquationNotSolvableException("The linear equation system has no unique solution."); + return solution; + } + + private static ReadOnlyCollection Validate(LinearEquation[] equations) + { + if (equations.Length == 0) + throw new ArgumentException("There must be at least one equation.", nameof(equations)); + for (var row = 0; row < equations.Length; row++) + { + var equation = equations[row] ?? + throw new ArgumentException($"Equation {row} is null.", nameof(equations)); + if (equation.Coefficients.Count != equations.Length) + throw new ArgumentException( + $"Equation {row} has {equation.Coefficients.Count} coefficients, but the system has {equations.Length} equations. " + + "Each equation needs exactly one coefficient per variable.", nameof(equations)); + } + + return Array.AsReadOnly(equations); + } +} diff --git a/src/SharpMath/Expressions/ExpressionContext.cs b/src/SharpMath/Expressions/ExpressionContext.cs new file mode 100644 index 0000000..21cbe1f --- /dev/null +++ b/src/SharpMath/Expressions/ExpressionContext.cs @@ -0,0 +1,210 @@ +using System.Diagnostics.CodeAnalysis; + +namespace SharpMath.Expressions; + +/// +/// Defines the functions, constants and multi-letter variables available when parsing a +/// . +/// +/// +/// +/// A new context contains the built-in functions sin cos tan asin acos atan atan2(y, x) sinh cosh tanh sqrt +/// abs ln lg log(x, base) log2 exp floor ceil round sign pow(x, y) min(…) max(…) and the constants +/// pi, e and tau. Names are case-insensitive and consist of a letter followed by letters or +/// digits. Adding a name that already exists replaces it. new ExpressionContext() is the default context; +/// use to derive variants from a configured context. +/// +/// +/// Every instance is independent. A parsed expression captures the definitions at parse time, so later changes +/// to the context do not affect it. +/// +/// +public sealed class ExpressionContext +{ + private readonly Dictionary _symbols = new(StringComparer.OrdinalIgnoreCase); + + /// + /// Initializes a new instance of the class with the built-in functions and + /// constants. + /// + public ExpressionContext() + { + AddBuiltIn("sin", Math.Sin); + AddBuiltIn("cos", Math.Cos); + AddBuiltIn("tan", Math.Tan); + AddBuiltIn("asin", Math.Asin); + AddBuiltIn("acos", Math.Acos); + AddBuiltIn("atan", Math.Atan); + AddBuiltIn("atan2", Math.Atan2); + AddBuiltIn("sinh", Math.Sinh); + AddBuiltIn("cosh", Math.Cosh); + AddBuiltIn("tanh", Math.Tanh); + AddBuiltIn("sqrt", Math.Sqrt); + AddBuiltIn("abs", Math.Abs); + AddBuiltIn("ln", x => Math.Log(x)); + AddBuiltIn("lg", Math.Log10); + AddBuiltIn("log", (x, b) => Math.Log(x, b)); + AddBuiltIn("log2", Math.Log2); + AddBuiltIn("exp", Math.Exp); + AddBuiltIn("floor", Math.Floor); + AddBuiltIn("ceil", Math.Ceiling); + AddBuiltIn("round", x => Math.Round(x, MidpointRounding.AwayFromZero)); + AddBuiltIn("sign", x => double.IsNaN(x) ? double.NaN : Math.Sign(x)); + AddBuiltIn("pow", Math.Pow); + _symbols["min"] = new FunctionSymbol(1, int.MaxValue, Variadic: Min, IsPure: true); + _symbols["max"] = new FunctionSymbol(1, int.MaxValue, Variadic: Max, IsPure: true); + _symbols["pi"] = new ConstantSymbol(Math.PI); + _symbols["e"] = new ConstantSymbol(Math.E); + _symbols["tau"] = new ConstantSymbol(Math.Tau); + UpdateLongestName(); + } + + internal static ExpressionContext Default { get; } = new(); + + internal int LongestNameLength { get; private set; } + + /// + /// Adds or replaces a function with one argument. + /// + /// The name. + /// The function. + /// This context, for chaining. + /// is not a valid name. + public ExpressionContext AddFunction(string name, Func function) + { + ArgumentNullException.ThrowIfNull(function); + return Add(name, new FunctionSymbol(1, 1, Unary: function)); + } + + /// + /// Adds or replaces a function with two arguments. + /// + /// The name. + /// The function. + /// This context, for chaining. + /// is not a valid name. + public ExpressionContext AddFunction(string name, Func function) + { + ArgumentNullException.ThrowIfNull(function); + return Add(name, new FunctionSymbol(2, 2, Binary: function)); + } + + /// + /// Adds or replaces a function with a variable number of arguments. + /// + /// The name. + /// The minimum number of arguments. Must not be negative. + /// + /// The maximum number of arguments; at least . Use + /// for no limit. + /// + /// The function; it receives the evaluated arguments. + /// This context, for chaining. + /// is not a valid name. + /// The argument counts are invalid. + public ExpressionContext AddFunction(string name, int minArguments, int maxArguments, + Func, double> function) + { + ArgumentNullException.ThrowIfNull(function); + ArgumentOutOfRangeException.ThrowIfNegative(minArguments); + ArgumentOutOfRangeException.ThrowIfLessThan(maxArguments, minArguments); + return Add(name, new FunctionSymbol(minArguments, maxArguments, Variadic: function)); + } + + /// + /// Adds or replaces a constant. + /// + /// The name. + /// The value. + /// This context, for chaining. + /// is not a valid name. + public ExpressionContext AddConstant(string name, double value) => Add(name, new ConstantSymbol(value)); + + /// + /// Declares a variable whose name has more than one character. Single letters that are not otherwise defined are + /// variables automatically. + /// + /// The name. + /// This context, for chaining. + /// is not a valid name. + public ExpressionContext DeclareVariable(string name) => Add(name, VariableSymbol.Declared); + + /// + /// Creates an independent copy of this context, e.g. to extend a shared configuration. + /// + /// The copy. + public ExpressionContext Clone() + { + var clone = new ExpressionContext(); + clone._symbols.Clear(); + foreach (var (name, symbol) in _symbols) + clone._symbols.Add(name, symbol); + clone.UpdateLongestName(); + return clone; + } + + internal bool TryGetSymbol(ReadOnlySpan name, [NotNullWhen(true)] out Symbol? symbol) => + _symbols.GetAlternateLookup>().TryGetValue(name, out symbol); + + private ExpressionContext Add(string name, Symbol symbol) + { + ValidateName(name); + _symbols[name] = symbol; + UpdateLongestName(); + return this; + } + + private void AddBuiltIn(string name, Func function) => + _symbols[name] = new FunctionSymbol(1, 1, Unary: function, IsPure: true); + + private void AddBuiltIn(string name, Func function) => + _symbols[name] = new FunctionSymbol(2, 2, Binary: function, IsPure: true); + + private void UpdateLongestName() => LongestNameLength = _symbols.Keys.Max(key => key.Length); + + private static void ValidateName(string name) + { + ArgumentNullException.ThrowIfNull(name); + if (name.Length == 0 || !char.IsAsciiLetter(name[0]) || !name.All(char.IsAsciiLetterOrDigit)) + throw new ArgumentException( + "A name must start with an ASCII letter and contain only ASCII letters and digits.", nameof(name)); + } + + private static double Min(ReadOnlySpan values) + { + var min = values[0]; + foreach (var value in values[1..]) + min = Math.Min(min, value); + return min; + } + + private static double Max(ReadOnlySpan values) + { + var max = values[0]; + foreach (var value in values[1..]) + max = Math.Max(max, value); + return max; + } +} + +internal abstract record Symbol; + +internal sealed record ConstantSymbol(double Value) : Symbol; + +/// +/// A variable: either declared in the context (multi-letter names) or an implicit single letter. +/// +internal sealed record VariableSymbol(bool IsDeclared) : Symbol +{ + public static VariableSymbol Declared { get; } = new(true); + + public static VariableSymbol Implicit { get; } = new(false); +} + +internal sealed record FunctionSymbol( + int MinArguments, + int MaxArguments, + Func? Unary = null, + Func? Binary = null, + Func, double>? Variadic = null, + bool IsPure = false) : Symbol; diff --git a/src/SharpMath/Expressions/ExpressionParser.cs b/src/SharpMath/Expressions/ExpressionParser.cs new file mode 100644 index 0000000..13fc515 --- /dev/null +++ b/src/SharpMath/Expressions/ExpressionParser.cs @@ -0,0 +1,278 @@ +namespace SharpMath.Expressions; + +/// +/// A recursive-descent parser for mathematical expressions. +/// +/// +/// Grammar, from lowest to highest precedence: +/// +/// additive = multiplicative { ("+" | "-") multiplicative } +/// multiplicative = unary { ("*" | "/" | "%") unary | implicit power } +/// unary = ("+" | "-") unary | power +/// power = postfix [ "^" unary ] (right-associative) +/// postfix = primary { "!" } +/// primary = number | constant | variable | function "(" [ additive { "," additive } ] ")" | "(" additive ")" +/// +/// Implicit multiplication applies when an operand is directly followed by a name or an opening parenthesis. +/// +internal sealed class ExpressionParser +{ + /// + /// The maximum nesting of parentheses, signs and exponents. Parsing recurses several frames per level. + /// + public const int MaxNesting = 256; + + /// + /// The maximum height of the expression tree, e.g. the number of terms in a sum. Evaluation recurses one frame + /// per level. Both limits prevent stack overflows, which cannot be caught. + /// + public const int MaxTreeDepth = 4096; + + private readonly string _text; + private readonly List _tokens; + private int _depth; + private readonly Dictionary _variableSlots = new(StringComparer.OrdinalIgnoreCase); + private readonly List _variableNames = []; + private int _index; + + private ExpressionParser(string text, ExpressionContext context) + { + _text = text; + _tokens = Tokenizer.Tokenize(text, context); + } + + private Token Current => _tokens[_index]; + + public static (Node Root, IReadOnlyList Variables) Parse(string text, ExpressionContext context) + { + if (string.IsNullOrWhiteSpace(text)) + throw new ParserException("The expression is empty.", 0); + + var parser = new ExpressionParser(text, context); + var root = parser.ParseAdditive(); + if (parser.Current.Kind != TokenKind.End) + throw parser.Unexpected(parser.Current); + return (root, parser._variableNames.AsReadOnly()); + } + + private Node ParseAdditive() + { + Enter(); + var left = ParseMultiplicative(); + while (Current.Kind is TokenKind.Plus or TokenKind.Minus) + { + var op = Next().Kind == TokenKind.Plus ? BinaryOperator.Add : BinaryOperator.Subtract; + left = Binary(op, left, ParseMultiplicative()); + } + + _depth--; + return left; + } + + private Node ParseMultiplicative() + { + var left = ParseUnary(); + while (true) + { + switch (Current.Kind) + { + case TokenKind.Star: + Next(); + left = Binary(BinaryOperator.Multiply, left, ParseUnary()); + break; + case TokenKind.Slash: + Next(); + left = Binary(BinaryOperator.Divide, left, ParseUnary()); + break; + case TokenKind.Percent: + Next(); + left = Binary(BinaryOperator.Remainder, left, ParseUnary()); + break; + case TokenKind.Name or TokenKind.OpenParenthesis: + left = Binary(BinaryOperator.Multiply, left, ParsePower()); + break; + case TokenKind.Number: + throw new ParserException("A number cannot directly follow another operand; add an operator.", + Current.Position); + default: + return left; + } + } + } + + private Node ParseUnary() + { + Enter(); + Node result; + switch (Current.Kind) + { + case TokenKind.Plus: + Next(); + result = ParseUnary(); + break; + case TokenKind.Minus: + var position = Next().Position; + var operand = ParseUnary(); + result = operand is ConstantNode constant + ? new ConstantNode(-constant.Value) + : Limit(new NegateNode(operand), position); + break; + default: + result = ParsePower(); + break; + } + + _depth--; + return result; + } + + private Node ParsePower() + { + var basis = ParsePostfix(); + if (Current.Kind != TokenKind.Caret) + return basis; + + Next(); + return Binary(BinaryOperator.Power, basis, ParseUnary()); + } + + private Node ParsePostfix() + { + var operand = ParsePrimary(); + while (Current.Kind == TokenKind.Exclamation) + { + var position = Next().Position; + operand = operand is ConstantNode constant + ? new ConstantNode(FactorialNode.Factorial(constant.Value)) + : Limit(new FactorialNode(operand), position); + } + + return operand; + } + + private Node ParsePrimary() + { + var token = Next(); + switch (token.Kind) + { + case TokenKind.Number: + return new ConstantNode(token.Number); + case TokenKind.OpenParenthesis: + var inner = ParseAdditive(); + Expect(TokenKind.CloseParenthesis, token); + return inner; + case TokenKind.Name: + return token.Symbol switch + { + ConstantSymbol constant => new ConstantNode(constant.Value), + FunctionSymbol function => ParseFunction(token, function), + _ => Variable(token) + }; + default: + throw Unexpected(token); + } + } + + private Node ParseFunction(Token nameToken, FunctionSymbol function) + { + var name = _text.Substring(nameToken.Position, nameToken.Length); + if (Current.Kind != TokenKind.OpenParenthesis) + throw new ParserException($"The function '{name}' must be followed by '('.", Current.Position); + var openParenthesis = Next(); + + var arguments = new List(); + if (Current.Kind != TokenKind.CloseParenthesis) + { + arguments.Add(ParseAdditive()); + while (Current.Kind == TokenKind.Comma) + { + Next(); + arguments.Add(ParseAdditive()); + } + } + + Expect(TokenKind.CloseParenthesis, openParenthesis); + + if (arguments.Count < function.MinArguments || arguments.Count > function.MaxArguments) + throw new ParserException( + $"The function '{name}' expects {DescribeArity(function)}, but got {arguments.Count}.", + nameToken.Position); + + Node node = function switch + { + { Unary: { } unary } => new UnaryFunctionNode(unary, arguments[0]), + { Binary: { } binary } => new BinaryFunctionNode(binary, arguments[0], arguments[1]), + _ => new VariadicFunctionNode(function.Variadic!, [.. arguments]) + }; + + return function.IsPure && arguments.TrueForAll(argument => argument is ConstantNode) + ? new ConstantNode(node.Evaluate([])) + : Limit(node, nameToken.Position); + } + + private VariableNode Variable(Token token) + { + var name = _text.Substring(token.Position, token.Length).ToLowerInvariant(); + if (!_variableSlots.TryGetValue(name, out var slot)) + { + slot = _variableNames.Count; + _variableSlots.Add(name, slot); + _variableNames.Add(name); + } + + return new VariableNode(slot); + } + + private Node Binary(BinaryOperator op, Node left, Node right) => + left is ConstantNode l && right is ConstantNode r + ? new ConstantNode(BinaryNode.Apply(op, l.Value, r.Value)) + : Limit(new BinaryNode(op, left, right), Current.Position); + + private void Enter() + { + if (++_depth > MaxNesting) + throw new ParserException($"The expression is nested more than {MaxNesting} levels deep.", Current.Position); + } + + private static T Limit(T node, int position) where T : Node => + node.Depth <= MaxTreeDepth + ? node + : throw new ParserException($"The expression is too complex (more than {MaxTreeDepth} nested operations).", + position); + + private Token Next() + { + var token = _tokens[_index]; + if (token.Kind != TokenKind.End) + _index++; + return token; + } + + private void Expect(TokenKind kind, Token openingToken) + { + if (Current.Kind == kind) + { + Next(); + return; + } + + if (Current.Kind == TokenKind.End) + throw new ParserException($"The parenthesis opened at position {openingToken.Position} is not closed.", + Current.Position); + throw Unexpected(Current); + } + + private ParserException Unexpected(Token token) => token.Kind switch + { + TokenKind.End => new ParserException("Unexpected end of expression.", token.Position), + TokenKind.CloseParenthesis => new ParserException("Unmatched ')'.", token.Position), + _ => new ParserException($"Unexpected '{_text.Substring(token.Position, token.Length)}'.", token.Position) + }; + + private static string DescribeArity(FunctionSymbol function) => + function.MinArguments == function.MaxArguments + ? $"{function.MinArguments} argument(s)" + : function.MaxArguments == int.MaxValue + ? $"at least {function.MinArguments} argument(s)" + : $"{function.MinArguments} to {function.MaxArguments} arguments"; +} diff --git a/src/SharpMath/Expressions/MathExpression.cs b/src/SharpMath/Expressions/MathExpression.cs new file mode 100644 index 0000000..16163a4 --- /dev/null +++ b/src/SharpMath/Expressions/MathExpression.cs @@ -0,0 +1,156 @@ +namespace SharpMath.Expressions; + +/// +/// Represents a parsed mathematical expression that can be evaluated many times, e.g. 2x^2 - 3x + sin(x). +/// +/// +/// +/// Supported syntax: numbers (12, 1.5, .5, 1e5), the operators + - * / % ^ +/// (^ is right-associative and binds tighter than unary minus, so -2^2 = -4), postfix factorial +/// !, parentheses, function calls with comma-separated arguments, constants and variables (see +/// ). +/// +/// +/// Multiplication may be implicit when an operand is directly followed by a name or an opening parenthesis: +/// 2x, 2pi, 3(x+1), (x+1)(x-1). It has the same precedence as *, so +/// 1/2x is (1/2)·x. Letter sequences are split by longest match against the known names, and any +/// other single letter is a variable: xy is x·y, pix is pi·x. +/// +/// Instances are immutable and thread-safe. Evaluation does not allocate. +/// +public sealed class MathExpression +{ + private readonly Node _root; + + private MathExpression(string text, Node root, IReadOnlyList variables) + { + Text = text; + _root = root; + Variables = variables; + } + + /// + /// Gets the source text. + /// + public string Text { get; } + + /// + /// Gets the names of the variables used in the expression, in lower case and in order of first occurrence. + /// + public IReadOnlyList Variables { get; } + + /// + /// Parses an expression. + /// + /// The expression. + /// The functions, constants and variables to use; the built-ins if . + /// The parsed expression. + /// The expression is invalid; tells where. + public static MathExpression Parse(string text, ExpressionContext? context = null) + { + ArgumentNullException.ThrowIfNull(text); + var (root, variables) = ExpressionParser.Parse(text, context ?? ExpressionContext.Default); + return new MathExpression(text, root, variables); + } + + /// + /// Evaluates an expression without variables. + /// + /// The result. + /// The expression uses variables. + public double Evaluate() + { + if (Variables.Count > 0) + throw MissingVariables(Variables); + return _root.Evaluate([]); + } + + /// + /// Evaluates an expression with at most one variable. + /// + /// The name of the variable (case-insensitive). + /// The value of the variable. + /// The result. + /// The expression uses another variable. + public double Evaluate(string name, double value) + { + ArgumentNullException.ThrowIfNull(name); + switch (Variables.Count) + { + case 0: + return _root.Evaluate([]); + case 1 when string.Equals(Variables[0], name, StringComparison.OrdinalIgnoreCase): + return _root.Evaluate([value]); + default: + throw MissingVariablesExcept(name); + } + } + + /// + /// Evaluates the expression with the specified variable values. Names are matched case-insensitively; unused + /// entries are ignored. + /// + /// The values of the variables. + /// The result. + /// A variable used in the expression has no value. + public double Evaluate(IReadOnlyDictionary variables) + { + ArgumentNullException.ThrowIfNull(variables); + Span values = Variables.Count <= 32 ? stackalloc double[Variables.Count] : new double[Variables.Count]; + List? missing = null; + for (var i = 0; i < Variables.Count; i++) + { + if (TryFind(variables, Variables[i], out var value)) + values[i] = value; + else + (missing ??= []).Add(Variables[i]); + } + + if (missing is not null) + throw MissingVariables(missing); + return _root.Evaluate(values); + } + + /// + /// Creates a function of one variable, e.g. for plotting. + /// + /// The name of the variable (case-insensitive); the expression may also not use it at all. + /// A delegate that evaluates the expression without allocating. + /// The expression uses other variables. + public Func ToFunc(string variable) + { + ArgumentNullException.ThrowIfNull(variable); + if (Variables.Any(name => !string.Equals(name, variable, StringComparison.OrdinalIgnoreCase))) + throw MissingVariablesExcept(variable); + + var root = _root; + if (Variables.Count == 0) + return _ => root.Evaluate([]); + return x => root.Evaluate([x]); + } + + /// + public override string ToString() => Text; + + private static bool TryFind(IReadOnlyDictionary variables, string name, out double value) + { + if (variables.TryGetValue(name, out value)) + return true; + foreach (var (key, candidate) in variables) + { + if (!string.Equals(key, name, StringComparison.OrdinalIgnoreCase)) + continue; + value = candidate; + return true; + } + + return false; + } + + // A separate method keeps the closure over the name off the allocation-free success path. + private ArgumentException MissingVariablesExcept(string name) => + MissingVariables(Variables.Where(variable => !string.Equals(variable, name, StringComparison.OrdinalIgnoreCase))); + + private static ArgumentException MissingVariables(IEnumerable names) => + new($"No value was given for the variable(s): {string.Join(", ", names)}."); +} diff --git a/src/SharpMath/Expressions/Nodes.cs b/src/SharpMath/Expressions/Nodes.cs new file mode 100644 index 0000000..b5625fb --- /dev/null +++ b/src/SharpMath/Expressions/Nodes.cs @@ -0,0 +1,118 @@ +namespace SharpMath.Expressions; + +/// +/// A node of a parsed expression tree. Variables are resolved to slot indices at parse time, so evaluation does not +/// allocate. +/// +internal abstract class Node(int depth) +{ + /// + /// Gets the height of the tree below this node (1 for leaves); evaluation recurses this deep. + /// + public int Depth { get; } = depth; + + public abstract double Evaluate(ReadOnlySpan variables); + + protected static int DepthOf(Node[] children) + { + var depth = 0; + foreach (var child in children) + depth = Math.Max(depth, child.Depth); + return depth + 1; + } +} + +internal sealed class ConstantNode(double value) : Node(1) +{ + public double Value { get; } = value; + + public override double Evaluate(ReadOnlySpan variables) => Value; +} + +internal sealed class VariableNode(int slot) : Node(1) +{ + public override double Evaluate(ReadOnlySpan variables) => variables[slot]; +} + +internal sealed class NegateNode(Node operand) : Node(operand.Depth + 1) +{ + public override double Evaluate(ReadOnlySpan variables) => -operand.Evaluate(variables); +} + +internal sealed class FactorialNode(Node operand) : Node(operand.Depth + 1) +{ + private static readonly double[] Factorials = CreateFactorials(); + + public override double Evaluate(ReadOnlySpan variables) => Factorial(operand.Evaluate(variables)); + + public static double Factorial(double value) + { + if (!(value >= 0) || value != Math.Floor(value)) + return double.NaN; + return value < Factorials.Length ? Factorials[(int) value] : double.PositiveInfinity; + } + + private static double[] CreateFactorials() + { + // 170! is the largest factorial representable as a double. + var factorials = new double[171]; + factorials[0] = 1; + for (var i = 1; i < factorials.Length; i++) + factorials[i] = factorials[i - 1] * i; + return factorials; + } +} + +internal enum BinaryOperator +{ + Add, + Subtract, + Multiply, + Divide, + Remainder, + Power +} + +internal sealed class BinaryNode(BinaryOperator op, Node left, Node right) : Node(Math.Max(left.Depth, right.Depth) + 1) +{ + public override double Evaluate(ReadOnlySpan variables) => + Apply(op, left.Evaluate(variables), right.Evaluate(variables)); + + public static double Apply(BinaryOperator op, double left, double right) => op switch + { + BinaryOperator.Add => left + right, + BinaryOperator.Subtract => left - right, + BinaryOperator.Multiply => left * right, + BinaryOperator.Divide => left / right, + BinaryOperator.Remainder => left % right, + _ => Math.Pow(left, right) + }; +} + +internal sealed class UnaryFunctionNode(Func function, Node argument) : Node(argument.Depth + 1) +{ + public override double Evaluate(ReadOnlySpan variables) => function(argument.Evaluate(variables)); +} + +internal sealed class BinaryFunctionNode(Func function, Node first, Node second) + : Node(Math.Max(first.Depth, second.Depth) + 1) +{ + public override double Evaluate(ReadOnlySpan variables) => + function(first.Evaluate(variables), second.Evaluate(variables)); +} + +internal sealed class VariadicFunctionNode(Func, double> function, Node[] arguments) + : Node(DepthOf(arguments)) +{ + private const int StackLimit = 32; + + public override double Evaluate(ReadOnlySpan variables) + { + Span values = arguments.Length <= StackLimit + ? stackalloc double[arguments.Length] + : new double[arguments.Length]; + for (var i = 0; i < arguments.Length; i++) + values[i] = arguments[i].Evaluate(variables); + return function(values); + } +} diff --git a/src/SharpMath/Expressions/ParserException.cs b/src/SharpMath/Expressions/ParserException.cs new file mode 100644 index 0000000..167e73f --- /dev/null +++ b/src/SharpMath/Expressions/ParserException.cs @@ -0,0 +1,48 @@ +namespace SharpMath.Expressions; + +/// +/// The exception that is thrown when an expression cannot be parsed. +/// +public class ParserException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + public ParserException() : this("The expression is invalid.") + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + public ParserException(string message) : base(message) + { + Position = -1; + } + + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + /// The exception that caused this exception. + public ParserException(string message, Exception innerException) : base(message, innerException) + { + Position = -1; + } + + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + /// The zero-based position in the expression where the error was detected. + public ParserException(string message, int position) : base($"{message} (at position {position})") + { + Position = position; + } + + /// + /// Gets the zero-based position in the expression where the error was detected, or −1 if unknown. + /// + public int Position { get; } +} diff --git a/src/SharpMath/Expressions/Tokenizer.cs b/src/SharpMath/Expressions/Tokenizer.cs new file mode 100644 index 0000000..8897258 --- /dev/null +++ b/src/SharpMath/Expressions/Tokenizer.cs @@ -0,0 +1,150 @@ +using System.Globalization; + +namespace SharpMath.Expressions; + +internal enum TokenKind +{ + Number, + Name, + Plus, + Minus, + Star, + Slash, + Percent, + Caret, + Exclamation, + OpenParenthesis, + CloseParenthesis, + Comma, + End +} + +internal readonly record struct Token(TokenKind Kind, int Position, int Length, double Number = 0, Symbol? Symbol = null); + +/// +/// Splits an expression into tokens. Runs of letters are split into names by longest match against the context +/// (functions, constants, declared variables); any remaining single letter becomes a variable. Digits after a +/// letter that are not part of a known name start a number. +/// +internal static class Tokenizer +{ + public static List Tokenize(string text, ExpressionContext context) + { + var tokens = new List(); + var position = 0; + while (position < text.Length) + { + var current = text[position]; + if (char.IsWhiteSpace(current)) + { + position++; + continue; + } + + if (char.IsAsciiDigit(current) || current == '.') + { + tokens.Add(ReadNumber(text, ref position)); + continue; + } + + if (char.IsAsciiLetter(current)) + { + ReadNames(text, context, ref position, tokens); + continue; + } + + var kind = current switch + { + '+' => TokenKind.Plus, + '-' => TokenKind.Minus, + '*' => TokenKind.Star, + '/' => TokenKind.Slash, + '%' => TokenKind.Percent, + '^' => TokenKind.Caret, + '!' => TokenKind.Exclamation, + '(' => TokenKind.OpenParenthesis, + ')' => TokenKind.CloseParenthesis, + ',' => TokenKind.Comma, + _ => throw new ParserException($"Unexpected character '{current}'.", position) + }; + tokens.Add(new Token(kind, position, 1)); + position++; + } + + tokens.Add(new Token(TokenKind.End, text.Length, 0)); + return tokens; + } + + private static Token ReadNumber(string text, ref int position) + { + var start = position; + while (position < text.Length && char.IsAsciiDigit(text[position])) + position++; + if (position < text.Length && text[position] == '.') + { + position++; + while (position < text.Length && char.IsAsciiDigit(text[position])) + position++; + } + + // Scientific notation only if the exponent has digits; "2e" is 2·e. + if (position < text.Length && text[position] is 'e' or 'E') + { + var exponent = position + 1; + if (exponent < text.Length && text[exponent] is '+' or '-') + exponent++; + if (exponent < text.Length && char.IsAsciiDigit(text[exponent])) + { + position = exponent; + while (position < text.Length && char.IsAsciiDigit(text[position])) + position++; + } + } + + var literal = text.AsSpan(start, position - start); + if (!double.TryParse(literal, NumberStyles.AllowDecimalPoint | NumberStyles.AllowExponent, + CultureInfo.InvariantCulture, out var value)) + throw new ParserException($"'{literal}' is not a valid number.", start); + return new Token(TokenKind.Number, start, position - start, value); + } + + /// + /// Splits a run of letters and digits into names. A run of two or more characters directly in front of "(" must + /// end with a function or a known name; otherwise it is reported as an unknown function. + /// + private static void ReadNames(string text, ExpressionContext context, ref int position, List tokens) + { + var runStart = position; + var runEnd = position; + while (runEnd < text.Length && char.IsAsciiLetterOrDigit(text[runEnd])) + runEnd++; + + while (position < runEnd && char.IsAsciiLetter(text[position])) + tokens.Add(ReadName(text, context, runEnd, ref position)); + + var next = runEnd; + while (next < text.Length && char.IsWhiteSpace(text[next])) + next++; + var lastSymbol = tokens[^1].Symbol; + if (position == runEnd && runEnd - runStart >= 2 && next < text.Length && text[next] == '(' && + lastSymbol is VariableSymbol { IsDeclared: false }) + throw new ParserException($"Unknown function '{text[runStart..runEnd]}'.", runStart); + } + + private static Token ReadName(string text, ExpressionContext context, int runEnd, ref int position) + { + var start = position; + var maxLength = Math.Min(runEnd - start, context.LongestNameLength); + for (var length = maxLength; length > 0; length--) + { + if (!context.TryGetSymbol(text.AsSpan(start, length), out var symbol)) + continue; + position += length; + return new Token(TokenKind.Name, start, length, Symbol: symbol); + } + + // Undefined single letter: an implicit variable. + position++; + return new Token(TokenKind.Name, start, 1, Symbol: VariableSymbol.Implicit); + } +} diff --git a/src/SharpMath/Geometry/BoundingBox2D.cs b/src/SharpMath/Geometry/BoundingBox2D.cs new file mode 100644 index 0000000..d2ce0d0 --- /dev/null +++ b/src/SharpMath/Geometry/BoundingBox2D.cs @@ -0,0 +1,155 @@ +using System.Globalization; + +namespace SharpMath.Geometry; + +/// +/// Represents an immutable axis-aligned bounding box in the plane. +/// +public readonly struct BoundingBox2D : IEquatable +{ + /// + /// Initializes a new instance of the struct. + /// + /// The smallest X coordinate. + /// The smallest Y coordinate. + /// The largest X coordinate. + /// The largest Y coordinate. + /// A minimum is greater than the corresponding maximum, or a value is NaN. + public BoundingBox2D(double minX, double minY, double maxX, double maxY) + { + if (!(minX <= maxX)) + throw new ArgumentException("minX must not be greater than maxX.", nameof(minX)); + if (!(minY <= maxY)) + throw new ArgumentException("minY must not be greater than maxY.", nameof(minY)); + + MinX = minX; + MinY = minY; + MaxX = maxX; + MaxY = maxY; + } + + /// + /// Gets the smallest X coordinate. + /// + public double MinX { get; } + + /// + /// Gets the smallest Y coordinate. + /// + public double MinY { get; } + + /// + /// Gets the largest X coordinate. + /// + public double MaxX { get; } + + /// + /// Gets the largest Y coordinate. + /// + public double MaxY { get; } + + /// + /// Gets the width. + /// + public double Width => MaxX - MinX; + + /// + /// Gets the height. + /// + public double Height => MaxY - MinY; + + /// + /// Gets the center. + /// + public Point2D Center => new((MinX + MaxX) / 2, (MinY + MaxY) / 2); + + /// + /// Creates the smallest bounding box that contains all specified points. + /// + /// The points; at least one. + /// The bounding box. + /// is empty. + public static BoundingBox2D FromPoints(ReadOnlySpan points) + { + if (points.IsEmpty) + throw new ArgumentException("At least one point is required.", nameof(points)); + + double minX = points[0].X, minY = points[0].Y, maxX = minX, maxY = minY; + foreach (var point in points[1..]) + { + minX = Math.Min(minX, point.X); + minY = Math.Min(minY, point.Y); + maxX = Math.Max(maxX, point.X); + maxY = Math.Max(maxY, point.Y); + } + + return new BoundingBox2D(minX, minY, maxX, maxY); + } + + /// + /// Determines whether a point lies inside the box or on its border. + /// + /// The point. + /// if the box contains the point; otherwise, . + public bool Contains(Point2D point) => + point.X >= MinX && point.X <= MaxX && point.Y >= MinY && point.Y <= MaxY; + + /// + /// Returns the smallest bounding box that contains this box and another box. + /// + /// The other box. + /// The union. + public BoundingBox2D Union(BoundingBox2D other) => + new(Math.Min(MinX, other.MinX), Math.Min(MinY, other.MinY), Math.Max(MaxX, other.MaxX), + Math.Max(MaxY, other.MaxY)); + + /// + /// Determines whether this box equals another box within the default tolerances. + /// + /// The other box. + /// if the boxes are approximately equal; otherwise, . + public bool ApproximatelyEquals(BoundingBox2D other) => + ApproximatelyEquals(other, Tolerance.DefaultAbsolute, Tolerance.DefaultRelative); + + /// + /// Determines whether this box equals another box within the specified tolerances. + /// + /// The other box. + /// The absolute tolerance. + /// The relative tolerance. + /// if the boxes are approximately equal; otherwise, . + public bool ApproximatelyEquals(BoundingBox2D other, double absolute, double relative) => + Tolerance.AreEqual(MinX, other.MinX, absolute, relative) && Tolerance.AreEqual(MinY, other.MinY, absolute, relative) && + Tolerance.AreEqual(MaxX, other.MaxX, absolute, relative) && Tolerance.AreEqual(MaxY, other.MaxY, absolute, relative); + + /// + public override string ToString() => + string.Create(CultureInfo.InvariantCulture, $"BoundingBox2D [Min: ({MinX}, {MinY}), Max: ({MaxX}, {MaxY})]"); + + /// + public bool Equals(BoundingBox2D other) => + MinX.Equals(other.MinX) && MinY.Equals(other.MinY) && MaxX.Equals(other.MaxX) && MaxY.Equals(other.MaxY); + + /// + public override bool Equals(object? obj) => obj is BoundingBox2D other && Equals(other); + + /// + public override int GetHashCode() => HashCode.Combine(MinX, MinY, MaxX, MaxY); + + /// + /// Determines whether two boxes are equal. + /// + /// The first box. + /// The second box. + /// if the boxes are equal; otherwise, . + public static bool operator ==(BoundingBox2D left, BoundingBox2D right) => + left.MinX == right.MinX && left.MinY == right.MinY && left.MaxX == right.MaxX && left.MaxY == right.MaxY; + + /// + /// Determines whether two boxes differ. + /// + /// The first box. + /// The second box. + /// if the boxes differ; otherwise, . + public static bool operator !=(BoundingBox2D left, BoundingBox2D right) => !(left == right); +} diff --git a/src/SharpMath/Geometry/IMatrix.cs b/src/SharpMath/Geometry/IMatrix.cs new file mode 100644 index 0000000..0119a45 --- /dev/null +++ b/src/SharpMath/Geometry/IMatrix.cs @@ -0,0 +1,25 @@ +namespace SharpMath.Geometry; + +/// +/// Represents a read-only matrix. +/// +public interface IMatrix +{ + /// + /// Gets the number of rows. + /// + int RowCount { get; } + + /// + /// Gets the number of columns. + /// + int ColumnCount { get; } + + /// + /// Gets the entry at the specified row and column. + /// + /// The zero-based row index. + /// The zero-based column index. + /// An index is out of range. + double this[int row, int column] { get; } +} diff --git a/src/SharpMath/Geometry/ISquareMatrix.cs b/src/SharpMath/Geometry/ISquareMatrix.cs new file mode 100644 index 0000000..1b21b05 --- /dev/null +++ b/src/SharpMath/Geometry/ISquareMatrix.cs @@ -0,0 +1,68 @@ +namespace SharpMath.Geometry; + +/// +/// Represents a read-only square matrix. +/// +/// +/// compares with the tolerances described in . The structural +/// predicates (diagonal, triangular, symmetric) treat an entry as zero if it is negligible relative to the largest +/// entry (), so they do not depend on the scale of the matrix. +/// +public interface ISquareMatrix : IMatrix +{ + /// + /// Gets the number of rows and columns. + /// + int Dimension { get; } + + /// + /// Gets the determinant. + /// + double Determinant { get; } + + /// + /// Gets the trace, i.e. the sum of the diagonal entries. + /// + double Trace { get; } + + /// + /// Gets a value indicating whether the matrix is the identity matrix. + /// + bool IsIdentity { get; } + + /// + /// Gets a value indicating whether all entries outside of the diagonal are zero. + /// + bool IsDiagonal { get; } + + /// + /// Gets a value indicating whether the matrix is upper or lower triangular. + /// + bool IsTriangular { get; } + + /// + /// Gets a value indicating whether the matrix equals its transpose. + /// + bool IsSymmetric { get; } + + /// + /// Gets a value indicating whether the matrix equals its negated transpose. + /// + bool IsSkewSymmetric { get; } + + /// + /// Gets a value indicating whether the matrix multiplied with its transpose is the identity matrix. + /// + bool IsOrthogonal { get; } + + /// + /// Gets a value indicating whether the matrix has no inverse. + /// + /// + /// The check is scale-aware and numerically robust: an LU decomposition with partial pivoting finds a pivot not + /// larger than n · ε · s, where ε is the machine epsilon of , s the + /// largest absolute entry and n the dimension. Uniformly scaled matrices such as 0.001 · I and + /// transformations with large translations are therefore regular. uses the same rule. + /// + bool IsSingular { get; } +} diff --git a/src/SharpMath/Geometry/IVector.cs b/src/SharpMath/Geometry/IVector.cs new file mode 100644 index 0000000..e63ada7 --- /dev/null +++ b/src/SharpMath/Geometry/IVector.cs @@ -0,0 +1,39 @@ +namespace SharpMath.Geometry; + +/// +/// Represents a read-only vector of fixed dimension. +/// +public interface IVector +{ + /// + /// Gets the number of components of the vector. + /// + int Dimension { get; } + + /// + /// Gets the component at the specified index. + /// + /// The zero-based index of the component. + /// is outside of [0, ). + double this[int index] { get; } + + /// + /// Gets the Euclidean length of the vector. + /// + double Length { get; } + + /// + /// Gets the squared Euclidean length of the vector. + /// + double LengthSquared { get; } + + /// + /// Gets a value indicating whether all components are zero within . + /// + bool IsZero { get; } + + /// + /// Gets a value indicating whether the vector has a length of 1 within the default tolerances. + /// + bool IsNormalized { get; } +} diff --git a/src/SharpMath/Geometry/IntersectionKind.cs b/src/SharpMath/Geometry/IntersectionKind.cs new file mode 100644 index 0000000..e447bec --- /dev/null +++ b/src/SharpMath/Geometry/IntersectionKind.cs @@ -0,0 +1,32 @@ +namespace SharpMath.Geometry; + +/// +/// Describes how two lines or segments intersect. +/// +public enum IntersectionKind +{ + /// + /// The objects do not intersect (e.g. distinct parallel lines or disjoint segments). + /// + None, + + /// + /// The objects intersect in exactly one point. + /// + Point, + + /// + /// The lines are identical. + /// + Coincident, + + /// + /// The three-dimensional lines are neither parallel nor intersecting. + /// + Skew, + + /// + /// The collinear segments overlap in a segment of positive length. + /// + Overlap +} diff --git a/src/SharpMath/Geometry/Line2D.cs b/src/SharpMath/Geometry/Line2D.cs new file mode 100644 index 0000000..3b2f64a --- /dev/null +++ b/src/SharpMath/Geometry/Line2D.cs @@ -0,0 +1,268 @@ +using System.Globalization; + +namespace SharpMath.Geometry; + +/// +/// Represents an immutable infinite line in the plane, stored in normalized general form A·x + B·y + C = 0. +/// +/// +/// The coefficients are normalized so that A² + B² = 1 and the first non-zero of A and B is +/// positive. Every line therefore has exactly one representation, so exact equality means "same line" (up to +/// rounding); use to compare lines built in different ways. Vertical +/// lines are supported. Create instances with the static factory methods; is not a valid +/// line. +/// +public readonly struct Line2D : IEquatable +{ + private Line2D(double a, double b, double c) + { + A = a; + B = b; + C = c; + } + + /// + /// Gets the normalized coefficient of x. + /// + public double A { get; } + + /// + /// Gets the normalized coefficient of y. + /// + public double B { get; } + + /// + /// Gets the normalized constant term. |C| is the distance of the line to the origin. + /// + public double C { get; } + + /// + /// Gets the unit normal vector (A, B). + /// + public Vector2 Normal => new(A, B); + + /// + /// Gets the unit direction vector (−B, A). + /// + public Vector2 Direction => new(-B, A); + + /// + /// Gets a value indicating whether the line is parallel to the Y axis. + /// + public bool IsVertical => Tolerance.IsZero(B); + + /// + /// Gets a value indicating whether the line is parallel to the X axis. + /// + public bool IsHorizontal => Tolerance.IsZero(A); + + /// + /// Gets the slope m of y = m·x + b, or if the line is vertical. + /// + public double Slope => IsVertical ? double.NaN : -A / B; + + /// + /// Gets the y-intercept b of y = m·x + b, or if the line is vertical. + /// + public double YIntercept => IsVertical ? double.NaN : -C / B; + + /// + /// Creates the line a·x + b·y + c = 0. + /// + /// The coefficient of x. + /// The coefficient of y. + /// The constant term. + /// The line. + /// and are both zero, or a value is not finite. + public static Line2D FromGeneralForm(double a, double b, double c) + { + if (!double.IsFinite(a) || !double.IsFinite(b) || !double.IsFinite(c)) + throw new ArgumentException("The coefficients must be finite.", double.IsFinite(a) ? double.IsFinite(b) ? nameof(c) : nameof(b) : nameof(a)); + var norm = Math.Sqrt(a * a + b * b); + if (norm == 0) + throw new ArgumentException("The coefficients a and b must not both be zero.", nameof(b)); + + a /= norm; + b /= norm; + c /= norm; + if (a < 0 || (a == 0 && b < 0)) + { + a = -a; + b = -b; + c = -c; + } + + // Avoid negative zeros so that equal lines also compare equal bit by bit. + return new Line2D(a + 0.0, b + 0.0, c + 0.0); + } + + /// + /// Creates the line y = slope·x + yIntercept. + /// + /// The slope. + /// The y-intercept. + /// The line. + /// A value is not finite. + public static Line2D FromSlopeIntercept(double slope, double yIntercept) => FromGeneralForm(slope, -1, yIntercept); + + /// + /// Creates the line through two points. + /// + /// The first point. + /// The second point. + /// The line. + /// The points are (approximately) equal. + public static Line2D FromPoints(Point2D first, Point2D second) + { + if (first.ApproximatelyEquals(second)) + throw new ArgumentException("The points must be distinct to define a line.", nameof(second)); + return FromPointAndDirection(first, second - first); + } + + /// + /// Creates the line through a point with the specified direction. + /// + /// A point on the line. + /// The direction; must not be the zero vector. + /// The line. + /// is the zero vector. + public static Line2D FromPointAndDirection(Point2D point, Vector2 direction) + { + if (direction.LengthSquared == 0) + throw new ArgumentException("The direction must not be the zero vector.", nameof(direction)); + return FromGeneralForm(direction.Y, -direction.X, direction.X * point.Y - direction.Y * point.X); + } + + /// + /// Calculates y for the specified x. + /// + /// The X coordinate. + /// The Y coordinate, or if the line is vertical. + public double SolveForY(double x) => IsVertical ? double.NaN : (-C - A * x) / B; + + /// + /// Calculates x for the specified y. + /// + /// The Y coordinate. + /// The X coordinate, or if the line is horizontal. + public double SolveForX(double y) => IsHorizontal ? double.NaN : (-C - B * y) / A; + + /// + /// Gets the point at the signed distance along , measured from + /// the point of the line closest to the origin. + /// + /// The signed distance along the line. + /// The point. + public Point2D PointAt(double distance) => ClosestPointToOrigin + Direction * distance; + + /// + /// Calculates the signed distance of a point to the line; positive on the side the points to. + /// + /// The point. + /// The signed distance. + public double SignedDistanceTo(Point2D point) => A * point.X + B * point.Y + C; + + /// + /// Calculates the distance of a point to the line. + /// + /// The point. + /// The distance. + public double DistanceTo(Point2D point) => Math.Abs(SignedDistanceTo(point)); + + /// + /// Calculates the point on the line that is closest to the specified point. + /// + /// The point. + /// The orthogonal projection of onto the line. + public Point2D ClosestPoint(Point2D point) => point - Normal * SignedDistanceTo(point); + + /// + /// Determines whether a point lies on the line within the default tolerances. + /// + /// The point. + /// if the point lies on the line; otherwise, . + public bool Contains(Point2D point) => + Tolerance.IsNegligible(SignedDistanceTo(point), + Math.Max(Math.Abs(C), Math.Max(Math.Abs(point.X), Math.Abs(point.Y)))); + + /// + /// Determines whether this line is parallel to (or identical with) another line. + /// + /// The other line. + /// if the lines are parallel; otherwise, . + public bool IsParallelTo(Line2D other) => Tolerance.IsNegligible(ParallelDeterminant(other), 1); + + /// + /// Intersects this line with another line. + /// + /// The other line. + /// The kind of intersection and, for a single intersection point, the point. + public LineIntersection2D Intersect(Line2D other) + { + var determinant = ParallelDeterminant(other); + if (Tolerance.IsNegligible(determinant, 1)) + { + return Contains(other.ClosestPointToOrigin) + ? new LineIntersection2D(IntersectionKind.Coincident, null) + : new LineIntersection2D(IntersectionKind.None, null); + } + + var x = (B * other.C - other.B * C) / determinant; + var y = (other.A * C - A * other.C) / determinant; + return new LineIntersection2D(IntersectionKind.Point, new Point2D(x, y)); + } + + /// + /// Determines whether this line equals another line within the default tolerances (coefficient-wise). + /// + /// The other line. + /// if the lines are approximately equal; otherwise, . + public bool ApproximatelyEquals(Line2D other) => + ApproximatelyEquals(other, Tolerance.DefaultAbsolute, Tolerance.DefaultRelative); + + /// + /// Determines whether this line equals another line within the specified tolerances (coefficient-wise). + /// + /// The other line. + /// The absolute tolerance. + /// The relative tolerance. + /// if the lines are approximately equal; otherwise, . + public bool ApproximatelyEquals(Line2D other, double absolute, double relative) => + Tolerance.AreEqual(A, other.A, absolute, relative) && Tolerance.AreEqual(B, other.B, absolute, relative) && + Tolerance.AreEqual(C, other.C, absolute, relative); + + /// + public override string ToString() => + string.Create(CultureInfo.InvariantCulture, $"Line2D [{A}·x + {B}·y + {C} = 0]"); + + /// + public bool Equals(Line2D other) => A.Equals(other.A) && B.Equals(other.B) && C.Equals(other.C); + + /// + public override bool Equals(object? obj) => obj is Line2D other && Equals(other); + + /// + public override int GetHashCode() => HashCode.Combine(A, B, C); + + /// + /// Determines whether two lines have exactly the same normalized coefficients. + /// + /// The first line. + /// The second line. + /// if the lines are equal; otherwise, . + public static bool operator ==(Line2D left, Line2D right) => + left.A == right.A && left.B == right.B && left.C == right.C; + + /// + /// Determines whether two lines differ. + /// + /// The first line. + /// The second line. + /// if the lines differ; otherwise, . + public static bool operator !=(Line2D left, Line2D right) => !(left == right); + + private Point2D ClosestPointToOrigin => new(-C * A, -C * B); + + // Zero exactly when the (unit) normals are parallel. + private double ParallelDeterminant(Line2D other) => A * other.B - B * other.A; +} diff --git a/src/SharpMath/Geometry/Line3D.cs b/src/SharpMath/Geometry/Line3D.cs new file mode 100644 index 0000000..f19d34f --- /dev/null +++ b/src/SharpMath/Geometry/Line3D.cs @@ -0,0 +1,212 @@ +using System.Globalization; + +namespace SharpMath.Geometry; + +/// +/// Represents an immutable infinite line in three-dimensional space. +/// +/// +/// The line is stored canonically: is the point of the line closest to the origin and +/// is a unit vector whose first non-zero component is positive. Every line therefore has +/// exactly one representation, so exact equality means "same line" (up to rounding); use +/// to compare lines built in different ways. Create instances with the +/// static factory methods; is not a valid line. +/// +public readonly struct Line3D : IEquatable +{ + private Line3D(Point3D point, Vector3 direction) + { + Point = point; + Direction = direction; + } + + /// + /// Gets the point of the line that is closest to the origin. + /// + public Point3D Point { get; } + + /// + /// Gets the unit direction of the line. + /// + public Vector3 Direction { get; } + + /// + /// Creates the line through two points. + /// + /// The first point. + /// The second point. + /// The line. + /// The points are (approximately) equal. + public static Line3D FromPoints(Point3D first, Point3D second) + { + if (first.ApproximatelyEquals(second)) + throw new ArgumentException("The points must be distinct to define a line.", nameof(second)); + return FromPointAndDirection(first, second - first); + } + + /// + /// Creates the line through a point with the specified direction. + /// + /// A point on the line. + /// The direction; must not be the zero vector. + /// The line. + /// is the zero vector or not finite. + public static Line3D FromPointAndDirection(Point3D point, Vector3 direction) + { + if (direction.LengthSquared == 0 || !double.IsFinite(direction.LengthSquared)) + throw new ArgumentException("The direction must be a finite, non-zero vector.", nameof(direction)); + + var unit = direction.Normalize(); + if (unit.X < 0 || (unit.X == 0 && (unit.Y < 0 || (unit.Y == 0 && unit.Z < 0)))) + unit = -unit; + unit = new Vector3(unit.X + 0.0, unit.Y + 0.0, unit.Z + 0.0); + + var position = point.ToVector(); + var closest = position - unit * position.Dot(unit); + return new Line3D(new Point3D(closest.X + 0.0, closest.Y + 0.0, closest.Z + 0.0), unit); + } + + /// + /// Gets the point at the signed distance along , measured from + /// . + /// + /// The signed distance along the line. + /// The point. + public Point3D PointAt(double distance) => Point + Direction * distance; + + /// + /// Calculates the point on the line that is closest to the specified point. + /// + /// The point. + /// The orthogonal projection of onto the line. + public Point3D ClosestPoint(Point3D point) => Point + Direction * (point - Point).Dot(Direction); + + /// + /// Calculates the distance of a point to the line. + /// + /// The point. + /// The distance. + public double DistanceTo(Point3D point) => (point - Point).Cross(Direction).Length; + + /// + /// Determines whether a point lies on the line within the default tolerances. + /// + /// The point. + /// if the point lies on the line; otherwise, . + public bool Contains(Point3D point) => + Tolerance.IsNegligible(DistanceTo(point), Math.Max(point.ToVector().Length, Point.ToVector().Length)); + + /// + /// Determines whether this line is parallel to (or identical with) another line. + /// + /// The other line. + /// if the lines are parallel; otherwise, . + public bool IsParallelTo(Line3D other) => Tolerance.IsNegligible(Direction.Cross(other.Direction).Length, 1); + + /// + /// Calculates the shortest distance between this line and another line. + /// + /// The other line. + /// The distance; 0 if the lines intersect. + public double DistanceTo(Line3D other) + { + var (onThis, onOther) = ClosestPoints(other); + return onThis.Distance(onOther); + } + + /// + /// Calculates the pair of points, one on each line, that are closest to each other. + /// + /// The other line. + /// + /// The closest points. For parallel lines, of this line and its projection onto the other line + /// are returned. + /// + public (Point3D OnThis, Point3D OnOther) ClosestPoints(Line3D other) + { + if (IsParallelTo(other)) + return (Point, other.ClosestPoint(Point)); + + // The connection of the closest points is perpendicular to both directions, i.e. parallel to n = d × e. + // |n|² is computed directly instead of as 1 − (d·e)², which cancels for nearly parallel lines. + var d = Direction; + var e = other.Direction; + var n = d.Cross(e); + var denominator = n.LengthSquared; + var w = other.Point - Point; + var s = w.Cross(e).Dot(n) / denominator; + var t = w.Cross(d).Dot(n) / denominator; + return (Point + d * s, other.Point + e * t); + } + + /// + /// Intersects this line with another line. + /// + /// The other line. + /// The kind of intersection and, for a single intersection point, the point. + public LineIntersection3D Intersect(Line3D other) + { + if (IsParallelTo(other)) + return Contains(other.Point) + ? new LineIntersection3D(IntersectionKind.Coincident, null) + : new LineIntersection3D(IntersectionKind.None, null); + + var (onThis, onOther) = ClosestPoints(other); + var scale = Math.Max(onThis.ToVector().Length, onOther.ToVector().Length); + if (!Tolerance.IsNegligible(onThis.Distance(onOther), scale)) + return new LineIntersection3D(IntersectionKind.Skew, null); + + var midpoint = new Point3D((onThis.X + onOther.X) / 2, (onThis.Y + onOther.Y) / 2, (onThis.Z + onOther.Z) / 2); + return new LineIntersection3D(IntersectionKind.Point, midpoint); + } + + /// + /// Determines whether this line equals another line within the default tolerances. + /// + /// The other line. + /// if the lines are approximately equal; otherwise, . + public bool ApproximatelyEquals(Line3D other) => + ApproximatelyEquals(other, Tolerance.DefaultAbsolute, Tolerance.DefaultRelative); + + /// + /// Determines whether this line equals another line within the specified tolerances. + /// + /// The other line. + /// The absolute tolerance. + /// The relative tolerance. + /// if the lines are approximately equal; otherwise, . + public bool ApproximatelyEquals(Line3D other, double absolute, double relative) => + Point.ApproximatelyEquals(other.Point, absolute, relative) && + Direction.ApproximatelyEquals(other.Direction, absolute, relative); + + /// + public override string ToString() => + string.Create(CultureInfo.InvariantCulture, + $"Line3D [({Point.X}, {Point.Y}, {Point.Z}) + t·({Direction.X}, {Direction.Y}, {Direction.Z})]"); + + /// + public bool Equals(Line3D other) => Point.Equals(other.Point) && Direction.Equals(other.Direction); + + /// + public override bool Equals(object? obj) => obj is Line3D other && Equals(other); + + /// + public override int GetHashCode() => HashCode.Combine(Point, Direction); + + /// + /// Determines whether two lines have exactly the same canonical representation. + /// + /// The first line. + /// The second line. + /// if the lines are equal; otherwise, . + public static bool operator ==(Line3D left, Line3D right) => + left.Point == right.Point && left.Direction == right.Direction; + + /// + /// Determines whether two lines differ. + /// + /// The first line. + /// The second line. + /// if the lines differ; otherwise, . + public static bool operator !=(Line3D left, Line3D right) => !(left == right); +} diff --git a/src/SharpMath/Geometry/LineIntersection2D.cs b/src/SharpMath/Geometry/LineIntersection2D.cs new file mode 100644 index 0000000..5379568 --- /dev/null +++ b/src/SharpMath/Geometry/LineIntersection2D.cs @@ -0,0 +1,11 @@ +namespace SharpMath.Geometry; + +/// +/// The result of intersecting two s. +/// +/// +/// How the lines intersect: , or +/// . +/// +/// The intersection point if is ; otherwise, . +public readonly record struct LineIntersection2D(IntersectionKind Kind, Point2D? Point); diff --git a/src/SharpMath/Geometry/LineIntersection3D.cs b/src/SharpMath/Geometry/LineIntersection3D.cs new file mode 100644 index 0000000..a4770fb --- /dev/null +++ b/src/SharpMath/Geometry/LineIntersection3D.cs @@ -0,0 +1,12 @@ +namespace SharpMath.Geometry; + +/// +/// The result of intersecting two s. +/// +/// +/// How the lines intersect: (distinct parallel lines), +/// , or +/// . +/// +/// The intersection point if is ; otherwise, . +public readonly record struct LineIntersection3D(IntersectionKind Kind, Point3D? Point); diff --git a/src/SharpMath/Geometry/LineSegment2D.cs b/src/SharpMath/Geometry/LineSegment2D.cs new file mode 100644 index 0000000..376f6eb --- /dev/null +++ b/src/SharpMath/Geometry/LineSegment2D.cs @@ -0,0 +1,216 @@ +using System.Globalization; + +namespace SharpMath.Geometry; + +/// +/// Represents an immutable line segment between two points in the plane. +/// +/// +/// A segment whose end points coincide is allowed and behaves like a point. Equality is exact and depends on the +/// order of the end points. +/// +public readonly struct LineSegment2D : IEquatable +{ + /// + /// Initializes a new instance of the struct. + /// + /// The start point. + /// The end point. + public LineSegment2D(Point2D start, Point2D end) + { + Start = start; + End = end; + } + + /// + /// Gets the start point. + /// + public Point2D Start { get; init; } + + /// + /// Gets the end point. + /// + public Point2D End { get; init; } + + /// + /// Gets the vector from to . + /// + public Vector2 Direction => End - Start; + + /// + /// Gets the length. + /// + public double Length => Direction.Length; + + /// + /// Gets the midpoint. + /// + public Point2D Midpoint => new((Start.X + End.X) / 2, (Start.Y + End.Y) / 2); + + /// + /// Returns the infinite line through the segment. + /// + /// The line. + /// The end points coincide. + public Line2D ToLine() => + IsDegenerate + ? throw new InvalidOperationException("A segment of length zero does not define a line.") + : Line2D.FromPointAndDirection(Start, Direction); + + /// + /// Calculates the point on the segment that is closest to the specified point. + /// + /// The point. + /// The closest point. + public Point2D ClosestPoint(Point2D point) => Start + Direction * ClosestParameter(point); + + /// + /// Calculates the distance of a point to the segment. + /// + /// The point. + /// The distance. + public double DistanceTo(Point2D point) => point.Distance(ClosestPoint(point)); + + /// + /// Determines whether a point lies on the segment within the default tolerances. + /// + /// The point. + /// if the point lies on the segment; otherwise, . + public bool Contains(Point2D point) => + SegmentMath.IsOnSegment(point, Start, End, SegmentMath.DistanceTolerance(Scale(point))); + + /// + /// Intersects this segment with another segment. + /// + /// The other segment. + /// The kind of intersection and the intersection point or overlapping segment. + public SegmentIntersection2D Intersect(LineSegment2D other) + { + if (IsDegenerate) + return other.Contains(Start) ? PointResult(Start) : NoIntersection; + if (other.IsDegenerate) + return Contains(other.Start) ? PointResult(other.Start) : NoIntersection; + + var r = Direction; + var s = other.Direction; + var offset = other.Start - Start; + var denominator = r.Cross(s); + + // Parallelism is judged by the angle, distances by the coordinate scale, so tiny segments behave like big ones. + if (r.IsParallelTo(s)) + { + var distanceToLine = Math.Abs(offset.Cross(r)) / r.Length; + if (distanceToLine > SegmentMath.DistanceTolerance(Scale(other.Start))) + return NoIntersection; + + var lengthSquared = r.LengthSquared; + var t0 = offset.Dot(r) / lengthSquared; + var t1 = t0 + s.Dot(r) / lengthSquared; + var from = Math.Max(0, Math.Min(t0, t1)); + var to = Math.Min(1, Math.Max(t0, t1)); + var tolerance = Tolerance.DefaultRelative; + if (from > to + tolerance) + return NoIntersection; + if (to - from <= tolerance) + return PointResult(Start + r * ((from + to) / 2)); + return new SegmentIntersection2D(IntersectionKind.Overlap, null, + new LineSegment2D(Start + r * from, Start + r * to)); + } + + var t = offset.Cross(s) / denominator; + var u = offset.Cross(r) / denominator; + var epsilon = Tolerance.DefaultRelative; + if (t < -epsilon || t > 1 + epsilon || u < -epsilon || u > 1 + epsilon) + return NoIntersection; + return PointResult(Start + r * Math.Clamp(t, 0, 1)); + } + + /// + /// Intersects this segment with an infinite line. + /// + /// The line. + /// + /// The kind of intersection: (with this segment) if the segment lies on the + /// line, a point, or none. + /// + public SegmentIntersection2D Intersect(Line2D line) + { + if (IsDegenerate) + return line.Contains(Start) ? PointResult(Start) : NoIntersection; + + var intersection = ToLine().Intersect(line); + return intersection.Kind switch + { + IntersectionKind.Coincident => new SegmentIntersection2D(IntersectionKind.Overlap, null, this), + IntersectionKind.Point when Contains(intersection.Point.GetValueOrDefault()) => + PointResult(ClosestPoint(intersection.Point.GetValueOrDefault())), + _ => NoIntersection + }; + } + + /// + /// Determines whether this segment equals another segment within the default tolerances (same order of end points). + /// + /// The other segment. + /// if the segments are approximately equal; otherwise, . + public bool ApproximatelyEquals(LineSegment2D other) => + ApproximatelyEquals(other, Tolerance.DefaultAbsolute, Tolerance.DefaultRelative); + + /// + /// Determines whether this segment equals another segment within the specified tolerances (same order of end + /// points). + /// + /// The other segment. + /// The absolute tolerance. + /// The relative tolerance. + /// if the segments are approximately equal; otherwise, . + public bool ApproximatelyEquals(LineSegment2D other, double absolute, double relative) => + Start.ApproximatelyEquals(other.Start, absolute, relative) && End.ApproximatelyEquals(other.End, absolute, relative); + + /// + public override string ToString() => + string.Create(CultureInfo.InvariantCulture, $"LineSegment2D [({Start.X}, {Start.Y}) → ({End.X}, {End.Y})]"); + + /// + public bool Equals(LineSegment2D other) => Start.Equals(other.Start) && End.Equals(other.End); + + /// + public override bool Equals(object? obj) => obj is LineSegment2D other && Equals(other); + + /// + public override int GetHashCode() => HashCode.Combine(Start, End); + + /// + /// Determines whether two segments are equal. + /// + /// The first segment. + /// The second segment. + /// if the segments are equal; otherwise, . + public static bool operator ==(LineSegment2D left, LineSegment2D right) => + left.Start == right.Start && left.End == right.End; + + /// + /// Determines whether two segments differ. + /// + /// The first segment. + /// The second segment. + /// if the segments differ; otherwise, . + public static bool operator !=(LineSegment2D left, LineSegment2D right) => !(left == right); + + private static SegmentIntersection2D NoIntersection => new(IntersectionKind.None, null, null); + + private bool IsDegenerate => Start == End; + + private static SegmentIntersection2D PointResult(Point2D point) => new(IntersectionKind.Point, point, null); + + private double ClosestParameter(Point2D point) + { + var direction = Direction; + var lengthSquared = direction.LengthSquared; + return lengthSquared == 0 ? 0 : Math.Clamp((point - Start).Dot(direction) / lengthSquared, 0, 1); + } + + private double Scale(Point2D point) => + Math.Max(SegmentMath.CoordinateScale(point), + Math.Max(SegmentMath.CoordinateScale(Start), SegmentMath.CoordinateScale(End))); +} diff --git a/src/SharpMath/Geometry/Matrix2x2.cs b/src/SharpMath/Geometry/Matrix2x2.cs new file mode 100644 index 0000000..04875fa --- /dev/null +++ b/src/SharpMath/Geometry/Matrix2x2.cs @@ -0,0 +1,356 @@ +namespace SharpMath.Geometry; + +/// +/// Represents an immutable 2x2 matrix. +/// +/// +/// Vectors are column vectors: a matrix transforms a vector as matrix * vector. Equality is exact; use +/// for tolerance-based comparisons. +/// +public readonly struct Matrix2x2 : ISquareMatrix, IEquatable +{ + /// + /// Initializes a new instance of the struct from its entries in row-major order. + /// + /// The entry in row 1, column 1. + /// The entry in row 1, column 2. + /// The entry in row 2, column 1. + /// The entry in row 2, column 2. + public Matrix2x2(double m11, double m12, double m21, double m22) + { + M11 = m11; + M12 = m12; + M21 = m21; + M22 = m22; + } + + /// + /// Initializes a new instance of the struct from its rows. + /// + /// The first row. + /// The second row. + public Matrix2x2(Vector2 row1, Vector2 row2) : this(row1.X, row1.Y, row2.X, row2.Y) + { + } + + /// + /// Gets the entry in row 1, column 1. + /// + public double M11 { get; init; } + + /// + /// Gets the entry in row 1, column 2. + /// + public double M12 { get; init; } + + /// + /// Gets the entry in row 2, column 1. + /// + public double M21 { get; init; } + + /// + /// Gets the entry in row 2, column 2. + /// + public double M22 { get; init; } + + /// + /// Gets the zero matrix. + /// + public static Matrix2x2 Zero => default; + + /// + /// Gets the identity matrix. + /// + public static Matrix2x2 Identity => new(1, 0, 0, 1); + + /// + /// Gets the entry at the specified row-major index (row · 2 + column). + /// + /// The zero-based index between 0 and 3. + /// is out of range. + public double this[int index] => index switch + { + 0 => M11, + 1 => M12, + 2 => M21, + 3 => M22, + _ => throw MatrixMath.IndexOutOfRange(nameof(index), index, 4) + }; + + /// + public double this[int row, int column] + { + get + { + MatrixMath.CheckIndices(row, column, 2); + return this[row * 2 + column]; + } + } + + /// + public int RowCount => 2; + + /// + public int ColumnCount => 2; + + /// + public int Dimension => 2; + + /// + public double Determinant => M11 * M22 - M12 * M21; + + /// + public double Trace => M11 + M22; + + /// + public bool IsIdentity => Is(MatrixMath.Predicate.Identity); + + /// + public bool IsDiagonal => Is(MatrixMath.Predicate.Diagonal); + + /// + public bool IsTriangular => Is(MatrixMath.Predicate.Triangular); + + /// + public bool IsSymmetric => Is(MatrixMath.Predicate.Symmetric); + + /// + public bool IsSkewSymmetric => Is(MatrixMath.Predicate.SkewSymmetric); + + /// + public bool IsOrthogonal => (this * Transpose()).IsIdentity; + + /// + public bool IsSingular => Is(MatrixMath.Predicate.Singular); + + /// + /// Creates a matrix that rotates vectors counterclockwise by the specified angle. + /// + /// The angle in radians. + /// The rotation matrix. + public static Matrix2x2 Rotation(double angle) + { + var (sin, cos) = Math.SinCos(angle); + return new Matrix2x2(cos, -sin, sin, cos); + } + + /// + /// Creates a matrix that scales vectors. + /// + /// The scale factor along X. + /// The scale factor along Y. + /// The scaling matrix. + public static Matrix2x2 Scaling(double scaleX, double scaleY) => new(scaleX, 0, 0, scaleY); + + /// + /// Creates a matrix that scales vectors uniformly. + /// + /// The scale factor. + /// The scaling matrix. + public static Matrix2x2 Scaling(double scale) => Scaling(scale, scale); + + /// + /// Creates a matrix from any with two rows and two columns. + /// + /// The matrix to copy. + /// The copied matrix. + /// The matrix is not 2x2. + public static Matrix2x2 FromMatrix(IMatrix matrix) + { + ArgumentNullException.ThrowIfNull(matrix); + if (matrix.RowCount != 2 || matrix.ColumnCount != 2) + throw new ArgumentException("The matrix must have 2 rows and 2 columns.", nameof(matrix)); + return new Matrix2x2(matrix[0, 0], matrix[0, 1], matrix[1, 0], matrix[1, 1]); + } + + /// + /// Returns the transpose. + /// + /// The transposed matrix. + public Matrix2x2 Transpose() => new(M11, M21, M12, M22); + + /// + /// Returns the matrix of cofactors. + /// + /// The cofactor matrix. + public Matrix2x2 Cofactors() => new(M22, -M21, -M12, M11); + + /// + /// Returns the adjugate, i.e. the transposed cofactor matrix. + /// + /// The adjugate. + public Matrix2x2 Adjugate() => new(M22, -M12, -M21, M11); + + /// + /// Returns the inverse. + /// + /// The inverse matrix. + /// The matrix is singular (see ). + public Matrix2x2 Inverse() => TryInvert(out var inverse) ? inverse : throw MatrixMath.SingularMatrix(); + + /// + /// Tries to calculate the inverse. + /// + /// The inverse, or if the matrix is singular. + /// if the matrix is invertible; otherwise, . + public bool TryInvert(out Matrix2x2 inverse) + { + if (IsSingular) + { + inverse = default; + return false; + } + + // Expanding along the first row reuses the minors already computed for the adjugate. + var adjugate = Adjugate(); + inverse = adjugate / (M11 * adjugate.M11 + M12 * adjugate.M21); + return true; + } + + /// + /// Determines whether this matrix equals another matrix within the default tolerances (entry-wise). + /// + /// The other matrix. + /// if the matrices are approximately equal; otherwise, . + public bool ApproximatelyEquals(Matrix2x2 other) => + ApproximatelyEquals(other, Tolerance.DefaultAbsolute, Tolerance.DefaultRelative); + + /// + /// Determines whether this matrix equals another matrix within the specified tolerances (entry-wise). + /// + /// The other matrix. + /// The absolute tolerance. + /// The relative tolerance. + /// if the matrices are approximately equal; otherwise, . + public bool ApproximatelyEquals(Matrix2x2 other, double absolute, double relative) + { + Span entries = stackalloc double[4]; + Span otherEntries = stackalloc double[4]; + CopyTo(entries); + other.CopyTo(otherEntries); + return MatrixMath.ApproximatelyEqual(entries, otherEntries, absolute, relative); + } + + /// + public override string ToString() + { + Span entries = stackalloc double[4]; + CopyTo(entries); + return MatrixMath.Format(nameof(Matrix2x2), entries, 2, 2); + } + + /// + /// Copies the entries in row-major order. + /// + internal void CopyTo(Span destination) + { + destination[0] = M11; + destination[1] = M12; + destination[2] = M21; + destination[3] = M22; + } + + private bool Is(MatrixMath.Predicate predicate) + { + Span entries = stackalloc double[4]; + CopyTo(entries); + return MatrixMath.Evaluate(predicate, entries, 2); + } + + /// + public bool Equals(Matrix2x2 other) => + M11.Equals(other.M11) && M12.Equals(other.M12) && M21.Equals(other.M21) && M22.Equals(other.M22); + + /// + public override bool Equals(object? obj) => obj is Matrix2x2 other && Equals(other); + + /// + public override int GetHashCode() => HashCode.Combine(M11, M12, M21, M22); + + /// + /// Adds two matrices. + /// + /// The first matrix. + /// The second matrix. + /// The sum. + public static Matrix2x2 operator +(Matrix2x2 left, Matrix2x2 right) => + new(left.M11 + right.M11, left.M12 + right.M12, left.M21 + right.M21, left.M22 + right.M22); + + /// + /// Subtracts a matrix from another matrix. + /// + /// The minuend. + /// The subtrahend. + /// The difference. + public static Matrix2x2 operator -(Matrix2x2 left, Matrix2x2 right) => + new(left.M11 - right.M11, left.M12 - right.M12, left.M21 - right.M21, left.M22 - right.M22); + + /// + /// Negates a matrix. + /// + /// The matrix. + /// The negated matrix. + public static Matrix2x2 operator -(Matrix2x2 matrix) => new(-matrix.M11, -matrix.M12, -matrix.M21, -matrix.M22); + + /// + /// Multiplies a matrix with a scalar. + /// + /// The matrix. + /// The scalar. + /// The scaled matrix. + public static Matrix2x2 operator *(Matrix2x2 matrix, double scalar) => + new(matrix.M11 * scalar, matrix.M12 * scalar, matrix.M21 * scalar, matrix.M22 * scalar); + + /// + /// Multiplies a scalar with a matrix. + /// + /// The scalar. + /// The matrix. + /// The scaled matrix. + public static Matrix2x2 operator *(double scalar, Matrix2x2 matrix) => matrix * scalar; + + /// + /// Divides a matrix by a scalar. + /// + /// The matrix. + /// The scalar. + /// The scaled matrix. + public static Matrix2x2 operator /(Matrix2x2 matrix, double scalar) => + new(matrix.M11 / scalar, matrix.M12 / scalar, matrix.M21 / scalar, matrix.M22 / scalar); + + /// + /// Multiplies two matrices. + /// + /// The left matrix. + /// The right matrix. + /// The product left · right. + public static Matrix2x2 operator *(Matrix2x2 left, Matrix2x2 right) => + new(left.M11 * right.M11 + left.M12 * right.M21, left.M11 * right.M12 + left.M12 * right.M22, + left.M21 * right.M11 + left.M22 * right.M21, left.M21 * right.M12 + left.M22 * right.M22); + + /// + /// Transforms a column vector. + /// + /// The matrix. + /// The vector. + /// The product matrix · vector. + public static Vector2 operator *(Matrix2x2 matrix, Vector2 vector) => + new(matrix.M11 * vector.X + matrix.M12 * vector.Y, matrix.M21 * vector.X + matrix.M22 * vector.Y); + + /// + /// Determines whether two matrices are exactly equal (entry-wise ==). + /// + /// The first matrix. + /// The second matrix. + /// if all entries are equal; otherwise, . + public static bool operator ==(Matrix2x2 left, Matrix2x2 right) => + left.M11 == right.M11 && left.M12 == right.M12 && left.M21 == right.M21 && left.M22 == right.M22; + + /// + /// Determines whether two matrices differ in at least one entry. + /// + /// The first matrix. + /// The second matrix. + /// if any entry differs; otherwise, . + public static bool operator !=(Matrix2x2 left, Matrix2x2 right) => !(left == right); +} diff --git a/src/SharpMath/Geometry/Matrix3x3.Operations.cs b/src/SharpMath/Geometry/Matrix3x3.Operations.cs new file mode 100644 index 0000000..51cd6ed --- /dev/null +++ b/src/SharpMath/Geometry/Matrix3x3.Operations.cs @@ -0,0 +1,67 @@ +namespace SharpMath.Geometry; + +public readonly partial struct Matrix3x3 +{ + /// + public double Determinant => + M11 * (M22 * M33 - M23 * M32) - M12 * (M21 * M33 - M23 * M31) + M13 * (M21 * M32 - M22 * M31); + + /// + /// Returns the adjugate, i.e. the transposed cofactor matrix. + /// + /// The adjugate. + public Matrix3x3 Adjugate() => new( + M22 * M33 - M23 * M32, M13 * M32 - M12 * M33, M12 * M23 - M13 * M22, + M23 * M31 - M21 * M33, M11 * M33 - M13 * M31, M13 * M21 - M11 * M23, + M21 * M32 - M22 * M31, M12 * M31 - M11 * M32, M11 * M22 - M12 * M21); + + /// + /// Creates a matrix that rotates two-dimensional points counterclockwise around the origin in homogeneous + /// coordinates. + /// + /// The angle in radians. + /// The rotation matrix. + public static Matrix3x3 Rotation(double angle) + { + var (sin, cos) = Math.SinCos(angle); + return new Matrix3x3(cos, -sin, 0, sin, cos, 0, 0, 0, 1); + } + + /// + /// Creates a matrix that scales two-dimensional points in homogeneous coordinates. + /// + /// The scale factor along X. + /// The scale factor along Y. + /// The scaling matrix. + public static Matrix3x3 Scaling(double scaleX, double scaleY) => new(scaleX, 0, 0, 0, scaleY, 0, 0, 0, 1); + + /// + /// Creates a matrix that scales two-dimensional points uniformly in homogeneous coordinates. + /// + /// The scale factor. + /// The scaling matrix. + public static Matrix3x3 Scaling(double scale) => Scaling(scale, scale); + + /// + /// Creates a matrix that translates two-dimensional points in homogeneous coordinates. + /// + /// The translation along X. + /// The translation along Y. + /// The translation matrix. + public static Matrix3x3 Translation(double x, double y) => new(1, 0, x, 0, 1, y, 0, 0, 1); + + /// + /// Transforms a two-dimensional vector in homogeneous coordinates (matrix · (x, y, 1)). The result is divided + /// by the resulting W component if it is not 1. + /// + /// The matrix. + /// The vector. + /// The transformed vector. + public static Vector2 operator *(Matrix3x3 matrix, Vector2 vector) + { + var x = matrix.M11 * vector.X + matrix.M12 * vector.Y + matrix.M13; + var y = matrix.M21 * vector.X + matrix.M22 * vector.Y + matrix.M23; + var w = matrix.M31 * vector.X + matrix.M32 * vector.Y + matrix.M33; + return w == 1 ? new Vector2(x, y) : new Vector2(x / w, y / w); + } +} diff --git a/src/SharpMath/Geometry/Matrix3x3.cs b/src/SharpMath/Geometry/Matrix3x3.cs new file mode 100644 index 0000000..3ad1d81 --- /dev/null +++ b/src/SharpMath/Geometry/Matrix3x3.cs @@ -0,0 +1,408 @@ +namespace SharpMath.Geometry; + +/// +/// Represents an immutable 3x3 matrix. +/// +/// +/// Vectors are column vectors: a matrix transforms a vector as matrix * vector, and transformations compose +/// from right to left (T * R * S * v scales first). Equality is exact; use +/// for tolerance-based comparisons. +/// +public readonly partial struct Matrix3x3 : ISquareMatrix, IEquatable +{ + /// + /// Initializes a new instance of the struct from its entries in row-major order. + /// + /// The entry in row 1, column 1. + /// The entry in row 1, column 2. + /// The entry in row 1, column 3. + /// The entry in row 2, column 1. + /// The entry in row 2, column 2. + /// The entry in row 2, column 3. + /// The entry in row 3, column 1. + /// The entry in row 3, column 2. + /// The entry in row 3, column 3. + public Matrix3x3(double m11, double m12, double m13, double m21, double m22, double m23, double m31, double m32, double m33) + { + M11 = m11; + M12 = m12; + M13 = m13; + M21 = m21; + M22 = m22; + M23 = m23; + M31 = m31; + M32 = m32; + M33 = m33; + } + + /// + /// Initializes a new instance of the struct from its rows. + /// + /// Row 1. + /// Row 2. + /// Row 3. + public Matrix3x3(Vector3 row1, Vector3 row2, Vector3 row3) + : this(row1.X, row1.Y, row1.Z, row2.X, row2.Y, row2.Z, row3.X, row3.Y, row3.Z) + { + } + + /// + /// Gets the entry in row 1, column 1. + /// + public double M11 { get; init; } + + /// + /// Gets the entry in row 1, column 2. + /// + public double M12 { get; init; } + + /// + /// Gets the entry in row 1, column 3. + /// + public double M13 { get; init; } + + /// + /// Gets the entry in row 2, column 1. + /// + public double M21 { get; init; } + + /// + /// Gets the entry in row 2, column 2. + /// + public double M22 { get; init; } + + /// + /// Gets the entry in row 2, column 3. + /// + public double M23 { get; init; } + + /// + /// Gets the entry in row 3, column 1. + /// + public double M31 { get; init; } + + /// + /// Gets the entry in row 3, column 2. + /// + public double M32 { get; init; } + + /// + /// Gets the entry in row 3, column 3. + /// + public double M33 { get; init; } + + /// + /// Gets the zero matrix. + /// + public static Matrix3x3 Zero => default; + + /// + /// Gets the identity matrix. + /// + public static Matrix3x3 Identity => new(1, 0, 0, 0, 1, 0, 0, 0, 1); + + /// + /// Gets the entry at the specified row-major index (row · 3 + column). + /// + /// The zero-based index between 0 and 8. + /// is out of range. + public double this[int index] => index switch + { + 0 => M11, + 1 => M12, + 2 => M13, + 3 => M21, + 4 => M22, + 5 => M23, + 6 => M31, + 7 => M32, + 8 => M33, + _ => throw MatrixMath.IndexOutOfRange(nameof(index), index, 9) + }; + + /// + public double this[int row, int column] + { + get + { + MatrixMath.CheckIndices(row, column, 3); + return this[row * 3 + column]; + } + } + + /// + public int RowCount => 3; + + /// + public int ColumnCount => 3; + + /// + public int Dimension => 3; + + /// + public double Trace => M11 + M22 + M33; + + /// + public bool IsIdentity => Is(MatrixMath.Predicate.Identity); + + /// + public bool IsDiagonal => Is(MatrixMath.Predicate.Diagonal); + + /// + public bool IsTriangular => Is(MatrixMath.Predicate.Triangular); + + /// + public bool IsSymmetric => Is(MatrixMath.Predicate.Symmetric); + + /// + public bool IsSkewSymmetric => Is(MatrixMath.Predicate.SkewSymmetric); + + /// + public bool IsOrthogonal => (this * Transpose()).IsIdentity; + + /// + public bool IsSingular => Is(MatrixMath.Predicate.Singular); + + /// + /// Creates a matrix from any with 3 rows and 3 columns. + /// + /// The matrix to copy. + /// The copied matrix. + /// The matrix is not 3x3. + public static Matrix3x3 FromMatrix(IMatrix matrix) + { + ArgumentNullException.ThrowIfNull(matrix); + if (matrix.RowCount != 3 || matrix.ColumnCount != 3) + throw new ArgumentException("The matrix must have 3 rows and 3 columns.", nameof(matrix)); + return new Matrix3x3(matrix[0, 0], matrix[0, 1], matrix[0, 2], matrix[1, 0], matrix[1, 1], matrix[1, 2], matrix[2, 0], matrix[2, 1], matrix[2, 2]); + } + + /// + /// Returns the transpose. + /// + /// The transposed matrix. + public Matrix3x3 Transpose() => new(M11, M21, M31, M12, M22, M32, M13, M23, M33); + + /// + /// Returns the matrix of cofactors. + /// + /// The cofactor matrix. + public Matrix3x3 Cofactors() => Adjugate().Transpose(); + + /// + /// Returns the inverse. + /// + /// The inverse matrix. + /// The matrix is singular (see ). + public Matrix3x3 Inverse() => TryInvert(out var inverse) ? inverse : throw MatrixMath.SingularMatrix(); + + /// + /// Tries to calculate the inverse. + /// + /// The inverse, or if the matrix is singular. + /// if the matrix is invertible; otherwise, . + public bool TryInvert(out Matrix3x3 inverse) + { + if (IsSingular) + { + inverse = default; + return false; + } + + // Expanding along the first row reuses the minors already computed for the adjugate. + var adjugate = Adjugate(); + inverse = adjugate / (M11 * adjugate.M11 + M12 * adjugate.M21 + M13 * adjugate.M31); + return true; + } + + /// + /// Determines whether this matrix equals another matrix within the default tolerances (entry-wise). + /// + /// The other matrix. + /// if the matrices are approximately equal; otherwise, . + public bool ApproximatelyEquals(Matrix3x3 other) => + ApproximatelyEquals(other, Tolerance.DefaultAbsolute, Tolerance.DefaultRelative); + + /// + /// Determines whether this matrix equals another matrix within the specified tolerances (entry-wise). + /// + /// The other matrix. + /// The absolute tolerance. + /// The relative tolerance. + /// if the matrices are approximately equal; otherwise, . + public bool ApproximatelyEquals(Matrix3x3 other, double absolute, double relative) + { + Span entries = stackalloc double[9]; + Span otherEntries = stackalloc double[9]; + CopyTo(entries); + other.CopyTo(otherEntries); + return MatrixMath.ApproximatelyEqual(entries, otherEntries, absolute, relative); + } + + /// + public override string ToString() + { + Span entries = stackalloc double[9]; + CopyTo(entries); + return MatrixMath.Format(nameof(Matrix3x3), entries, 3, 3); + } + + /// + /// Copies the entries in row-major order. + /// + internal void CopyTo(Span destination) + { + destination[0] = M11; + destination[1] = M12; + destination[2] = M13; + destination[3] = M21; + destination[4] = M22; + destination[5] = M23; + destination[6] = M31; + destination[7] = M32; + destination[8] = M33; + } + + private bool Is(MatrixMath.Predicate predicate) + { + Span entries = stackalloc double[9]; + CopyTo(entries); + return MatrixMath.Evaluate(predicate, entries, 3); + } + + /// + public bool Equals(Matrix3x3 other) => + M11.Equals(other.M11) && + M12.Equals(other.M12) && + M13.Equals(other.M13) && + M21.Equals(other.M21) && + M22.Equals(other.M22) && + M23.Equals(other.M23) && + M31.Equals(other.M31) && + M32.Equals(other.M32) && + M33.Equals(other.M33); + + /// + public override bool Equals(object? obj) => obj is Matrix3x3 other && Equals(other); + + /// + public override int GetHashCode() + { + var hash = new HashCode(); + hash.Add(M11); + hash.Add(M12); + hash.Add(M13); + hash.Add(M21); + hash.Add(M22); + hash.Add(M23); + hash.Add(M31); + hash.Add(M32); + hash.Add(M33); + return hash.ToHashCode(); + } + + /// + /// Adds two matrices. + /// + /// The first matrix. + /// The second matrix. + /// The sum. + public static Matrix3x3 operator +(Matrix3x3 left, Matrix3x3 right) => + new(left.M11 + right.M11, left.M12 + right.M12, left.M13 + right.M13, left.M21 + right.M21, left.M22 + right.M22, left.M23 + right.M23, left.M31 + right.M31, left.M32 + right.M32, left.M33 + right.M33); + + /// + /// Subtracts a matrix from another matrix. + /// + /// The minuend. + /// The subtrahend. + /// The difference. + public static Matrix3x3 operator -(Matrix3x3 left, Matrix3x3 right) => + new(left.M11 - right.M11, left.M12 - right.M12, left.M13 - right.M13, left.M21 - right.M21, left.M22 - right.M22, left.M23 - right.M23, left.M31 - right.M31, left.M32 - right.M32, left.M33 - right.M33); + + /// + /// Negates a matrix. + /// + /// The matrix. + /// The negated matrix. + public static Matrix3x3 operator -(Matrix3x3 matrix) => + new(-matrix.M11, -matrix.M12, -matrix.M13, -matrix.M21, -matrix.M22, -matrix.M23, -matrix.M31, -matrix.M32, -matrix.M33); + + /// + /// Multiplies a matrix with a scalar. + /// + /// The matrix. + /// The scalar. + /// The scaled matrix. + public static Matrix3x3 operator *(Matrix3x3 matrix, double scalar) => + new(matrix.M11 * scalar, matrix.M12 * scalar, matrix.M13 * scalar, matrix.M21 * scalar, matrix.M22 * scalar, matrix.M23 * scalar, matrix.M31 * scalar, matrix.M32 * scalar, matrix.M33 * scalar); + + /// + /// Multiplies a scalar with a matrix. + /// + /// The scalar. + /// The matrix. + /// The scaled matrix. + public static Matrix3x3 operator *(double scalar, Matrix3x3 matrix) => matrix * scalar; + + /// + /// Divides a matrix by a scalar. + /// + /// The matrix. + /// The scalar. + /// The scaled matrix. + public static Matrix3x3 operator /(Matrix3x3 matrix, double scalar) => + new(matrix.M11 / scalar, matrix.M12 / scalar, matrix.M13 / scalar, matrix.M21 / scalar, matrix.M22 / scalar, matrix.M23 / scalar, matrix.M31 / scalar, matrix.M32 / scalar, matrix.M33 / scalar); + + /// + /// Multiplies two matrices. + /// + /// The left matrix. + /// The right matrix. + /// The product left · right. + public static Matrix3x3 operator *(Matrix3x3 left, Matrix3x3 right) => + new(left.M11 * right.M11 + left.M12 * right.M21 + left.M13 * right.M31, + left.M11 * right.M12 + left.M12 * right.M22 + left.M13 * right.M32, + left.M11 * right.M13 + left.M12 * right.M23 + left.M13 * right.M33, + left.M21 * right.M11 + left.M22 * right.M21 + left.M23 * right.M31, + left.M21 * right.M12 + left.M22 * right.M22 + left.M23 * right.M32, + left.M21 * right.M13 + left.M22 * right.M23 + left.M23 * right.M33, + left.M31 * right.M11 + left.M32 * right.M21 + left.M33 * right.M31, + left.M31 * right.M12 + left.M32 * right.M22 + left.M33 * right.M32, + left.M31 * right.M13 + left.M32 * right.M23 + left.M33 * right.M33); + + /// + /// Transforms a column vector. + /// + /// The matrix. + /// The vector. + /// The product matrix · vector. + public static Vector3 operator *(Matrix3x3 matrix, Vector3 vector) => + new(matrix.M11 * vector.X + matrix.M12 * vector.Y + matrix.M13 * vector.Z, + matrix.M21 * vector.X + matrix.M22 * vector.Y + matrix.M23 * vector.Z, + matrix.M31 * vector.X + matrix.M32 * vector.Y + matrix.M33 * vector.Z); + + /// + /// Determines whether two matrices are exactly equal (entry-wise ==). + /// + /// The first matrix. + /// The second matrix. + /// if all entries are equal; otherwise, . + public static bool operator ==(Matrix3x3 left, Matrix3x3 right) => + left.M11 == right.M11 && + left.M12 == right.M12 && + left.M13 == right.M13 && + left.M21 == right.M21 && + left.M22 == right.M22 && + left.M23 == right.M23 && + left.M31 == right.M31 && + left.M32 == right.M32 && + left.M33 == right.M33; + + /// + /// Determines whether two matrices differ in at least one entry. + /// + /// The first matrix. + /// The second matrix. + /// if any entry differs; otherwise, . + public static bool operator !=(Matrix3x3 left, Matrix3x3 right) => !(left == right); +} diff --git a/src/SharpMath/Geometry/Matrix4x4.Operations.cs b/src/SharpMath/Geometry/Matrix4x4.Operations.cs new file mode 100644 index 0000000..6d120d6 --- /dev/null +++ b/src/SharpMath/Geometry/Matrix4x4.Operations.cs @@ -0,0 +1,245 @@ +namespace SharpMath.Geometry; + +public readonly partial struct Matrix4x4 +{ + /// + public double Determinant + { + get + { + var s0 = M11 * M22 - M21 * M12; + var s1 = M11 * M23 - M21 * M13; + var s2 = M11 * M24 - M21 * M14; + var s3 = M12 * M23 - M22 * M13; + var s4 = M12 * M24 - M22 * M14; + var s5 = M13 * M24 - M23 * M14; + var c5 = M33 * M44 - M43 * M34; + var c4 = M32 * M44 - M42 * M34; + var c3 = M32 * M43 - M42 * M33; + var c2 = M31 * M44 - M41 * M34; + var c1 = M31 * M43 - M41 * M33; + var c0 = M31 * M42 - M41 * M32; + return s0 * c5 - s1 * c4 + s2 * c3 + s3 * c2 - s4 * c1 + s5 * c0; + } + } + + /// + /// Returns the adjugate, i.e. the transposed cofactor matrix. + /// + /// The adjugate. + public Matrix4x4 Adjugate() + { + // Laplace expansion by 2x2 minors of the upper (s) and lower (c) two rows. + var s0 = M11 * M22 - M21 * M12; + var s1 = M11 * M23 - M21 * M13; + var s2 = M11 * M24 - M21 * M14; + var s3 = M12 * M23 - M22 * M13; + var s4 = M12 * M24 - M22 * M14; + var s5 = M13 * M24 - M23 * M14; + var c5 = M33 * M44 - M43 * M34; + var c4 = M32 * M44 - M42 * M34; + var c3 = M32 * M43 - M42 * M33; + var c2 = M31 * M44 - M41 * M34; + var c1 = M31 * M43 - M41 * M33; + var c0 = M31 * M42 - M41 * M32; + + return new Matrix4x4( + M22 * c5 - M23 * c4 + M24 * c3, -M12 * c5 + M13 * c4 - M14 * c3, + M42 * s5 - M43 * s4 + M44 * s3, -M32 * s5 + M33 * s4 - M34 * s3, + -M21 * c5 + M23 * c2 - M24 * c1, M11 * c5 - M13 * c2 + M14 * c1, + -M41 * s5 + M43 * s2 - M44 * s1, M31 * s5 - M33 * s2 + M34 * s1, + M21 * c4 - M22 * c2 + M24 * c0, -M11 * c4 + M12 * c2 - M14 * c0, + M41 * s4 - M42 * s2 + M44 * s0, -M31 * s4 + M32 * s2 - M34 * s0, + -M21 * c3 + M22 * c1 - M23 * c0, M11 * c3 - M12 * c1 + M13 * c0, + -M41 * s3 + M42 * s1 - M43 * s0, M31 * s3 - M32 * s1 + M33 * s0); + } + + /// + /// Creates a matrix that translates points. + /// + /// The translation along X. + /// The translation along Y. + /// The translation along Z. + /// The translation matrix. + public static Matrix4x4 Translation(double x, double y, double z) => + new(1, 0, 0, x, 0, 1, 0, y, 0, 0, 1, z, 0, 0, 0, 1); + + /// + /// Creates a matrix that translates points. + /// + /// The translation. + /// The translation matrix. + public static Matrix4x4 Translation(Vector3 translation) => + Translation(translation.X, translation.Y, translation.Z); + + /// + /// Creates a matrix that scales points. + /// + /// The scale factor along X. + /// The scale factor along Y. + /// The scale factor along Z. + /// The scaling matrix. + public static Matrix4x4 Scaling(double scaleX, double scaleY, double scaleZ) => + new(scaleX, 0, 0, 0, 0, scaleY, 0, 0, 0, 0, scaleZ, 0, 0, 0, 0, 1); + + /// + /// Creates a matrix that scales points uniformly. + /// + /// The scale factor. + /// The scaling matrix. + public static Matrix4x4 Scaling(double scale) => Scaling(scale, scale, scale); + + /// + /// Creates a matrix that rotates points around the X axis (counterclockwise when looking from +X towards the + /// origin). + /// + /// The angle in radians. + /// The rotation matrix. + public static Matrix4x4 RotationX(double angle) + { + var (sin, cos) = Math.SinCos(angle); + return new Matrix4x4(1, 0, 0, 0, 0, cos, -sin, 0, 0, sin, cos, 0, 0, 0, 0, 1); + } + + /// + /// Creates a matrix that rotates points around the Y axis (counterclockwise when looking from +Y towards the + /// origin). + /// + /// The angle in radians. + /// The rotation matrix. + public static Matrix4x4 RotationY(double angle) + { + var (sin, cos) = Math.SinCos(angle); + return new Matrix4x4(cos, 0, sin, 0, 0, 1, 0, 0, -sin, 0, cos, 0, 0, 0, 0, 1); + } + + /// + /// Creates a matrix that rotates points around the Z axis (counterclockwise when looking from +Z towards the + /// origin). + /// + /// The angle in radians. + /// The rotation matrix. + public static Matrix4x4 RotationZ(double angle) + { + var (sin, cos) = Math.SinCos(angle); + return new Matrix4x4(cos, -sin, 0, 0, sin, cos, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); + } + + /// + /// Creates a matrix that rotates around Z, then Y, then X (RotationX · RotationY · RotationZ). + /// + /// The angle around the X axis in radians. + /// The angle around the Y axis in radians. + /// The angle around the Z axis in radians. + /// The rotation matrix. + public static Matrix4x4 Rotation(double angleX, double angleY, double angleZ) => + RotationX(angleX) * RotationY(angleY) * RotationZ(angleZ); + + /// + /// Creates a right-handed perspective projection matrix that maps the view frustum to clip space with a depth range + /// of [0, 1]. + /// + /// The vertical field of view in radians, in (0, π). + /// The aspect ratio (width / height). Must be positive. + /// The distance to the near plane. Must be positive. + /// The distance to the far plane. Must be greater than . + /// The projection matrix. + /// An argument is out of range. + public static Matrix4x4 PerspectiveFieldOfView(double fieldOfView, double aspectRatio, double nearPlane, + double farPlane) + { + ProjectionData.Validate(fieldOfView, aspectRatio, nearPlane, farPlane); + + var yScale = 1 / Math.Tan(fieldOfView / 2); + var xScale = yScale / aspectRatio; + var range = farPlane / (nearPlane - farPlane); + return new Matrix4x4( + xScale, 0, 0, 0, + 0, yScale, 0, 0, + 0, 0, range, range * nearPlane, + 0, 0, -1, 0); + } + + /// + /// Creates a right-handed perspective projection matrix from projection data (see + /// ). + /// + /// The projection data. + /// The projection matrix. + public static Matrix4x4 PerspectiveFieldOfView(ProjectionData projectionData) => + PerspectiveFieldOfView(projectionData.FieldOfView, projectionData.AspectRatio, projectionData.NearPlane, + projectionData.FarPlane); + + /// + /// Creates a right-handed view matrix for a camera at looking at + /// . The camera looks along its local −Z axis. + /// + /// The camera position. + /// The point the camera looks at. + /// The approximate up direction; must not be parallel to the viewing direction. + /// The view matrix. + /// + /// equals , or is zero or parallel to the + /// viewing direction. + /// + public static Matrix4x4 LookAt(Vector3 eye, Vector3 target, Vector3 up) + { + var (xAxis, yAxis, zAxis) = OrthonormalBasis(eye - target, up, nameof(target)); + return new Matrix4x4( + xAxis.X, xAxis.Y, xAxis.Z, -xAxis.Dot(eye), + yAxis.X, yAxis.Y, yAxis.Z, -yAxis.Dot(eye), + zAxis.X, zAxis.Y, zAxis.Z, -zAxis.Dot(eye), + 0, 0, 0, 1); + } + + /// + /// Creates a world matrix that places an object at , oriented so that its local −Z axis + /// points along and its local Y axis approximately along . + /// + /// The position of the object. + /// The forward direction of the object. + /// The approximate up direction; must not be parallel to . + /// The world matrix. + /// + /// is zero, or is zero or parallel to it. + /// + public static Matrix4x4 World(Vector3 position, Vector3 forward, Vector3 up) + { + var (xAxis, yAxis, zAxis) = OrthonormalBasis(-forward, up, nameof(forward)); + return new Matrix4x4( + xAxis.X, yAxis.X, zAxis.X, position.X, + xAxis.Y, yAxis.Y, zAxis.Y, position.Y, + xAxis.Z, yAxis.Z, zAxis.Z, position.Z, + 0, 0, 0, 1); + } + + /// + /// Transforms a three-dimensional vector in homogeneous coordinates (matrix · (x, y, z, 1)). The result is + /// divided by the resulting W component if it is not 1 (perspective divide). + /// + /// The matrix. + /// The vector. + /// The transformed vector. + public static Vector3 operator *(Matrix4x4 matrix, Vector3 vector) + { + var x = matrix.M11 * vector.X + matrix.M12 * vector.Y + matrix.M13 * vector.Z + matrix.M14; + var y = matrix.M21 * vector.X + matrix.M22 * vector.Y + matrix.M23 * vector.Z + matrix.M24; + var z = matrix.M31 * vector.X + matrix.M32 * vector.Y + matrix.M33 * vector.Z + matrix.M34; + var w = matrix.M41 * vector.X + matrix.M42 * vector.Y + matrix.M43 * vector.Z + matrix.M44; + return w == 1 ? new Vector3(x, y, z) : new Vector3(x / w, y / w, z / w); + } + + private static (Vector3 XAxis, Vector3 YAxis, Vector3 ZAxis) OrthonormalBasis(Vector3 backward, Vector3 up, + string directionParameter) + { + if (backward.LengthSquared == 0) + throw new ArgumentException("The viewing direction must not be the zero vector.", directionParameter); + var zAxis = backward.Normalize(); + var x = up.Cross(zAxis); + if (up.LengthSquared == 0 || x.IsZero || up.IsParallelTo(zAxis)) + throw new ArgumentException("The up vector must not be zero or parallel to the viewing direction.", + nameof(up)); + var xAxis = x.Normalize(); + return (xAxis, zAxis.Cross(xAxis), zAxis); + } +} diff --git a/src/SharpMath/Geometry/Matrix4x4.cs b/src/SharpMath/Geometry/Matrix4x4.cs new file mode 100644 index 0000000..68f7f82 --- /dev/null +++ b/src/SharpMath/Geometry/Matrix4x4.cs @@ -0,0 +1,501 @@ +namespace SharpMath.Geometry; + +/// +/// Represents an immutable 4x4 matrix. +/// +/// +/// Vectors are column vectors: a matrix transforms a vector as matrix * vector, and transformations compose +/// from right to left (T * R * S * v scales first). Equality is exact; use +/// for tolerance-based comparisons. +/// +public readonly partial struct Matrix4x4 : ISquareMatrix, IEquatable +{ + /// + /// Initializes a new instance of the struct from its entries in row-major order. + /// + /// The entry in row 1, column 1. + /// The entry in row 1, column 2. + /// The entry in row 1, column 3. + /// The entry in row 1, column 4. + /// The entry in row 2, column 1. + /// The entry in row 2, column 2. + /// The entry in row 2, column 3. + /// The entry in row 2, column 4. + /// The entry in row 3, column 1. + /// The entry in row 3, column 2. + /// The entry in row 3, column 3. + /// The entry in row 3, column 4. + /// The entry in row 4, column 1. + /// The entry in row 4, column 2. + /// The entry in row 4, column 3. + /// The entry in row 4, column 4. + public Matrix4x4(double m11, double m12, double m13, double m14, double m21, double m22, double m23, double m24, double m31, double m32, double m33, double m34, double m41, double m42, double m43, double m44) + { + M11 = m11; + M12 = m12; + M13 = m13; + M14 = m14; + M21 = m21; + M22 = m22; + M23 = m23; + M24 = m24; + M31 = m31; + M32 = m32; + M33 = m33; + M34 = m34; + M41 = m41; + M42 = m42; + M43 = m43; + M44 = m44; + } + + /// + /// Initializes a new instance of the struct from its rows. + /// + /// Row 1. + /// Row 2. + /// Row 3. + /// Row 4. + public Matrix4x4(Vector4 row1, Vector4 row2, Vector4 row3, Vector4 row4) + : this(row1.X, row1.Y, row1.Z, row1.W, row2.X, row2.Y, row2.Z, row2.W, row3.X, row3.Y, row3.Z, row3.W, row4.X, row4.Y, row4.Z, row4.W) + { + } + + /// + /// Gets the entry in row 1, column 1. + /// + public double M11 { get; init; } + + /// + /// Gets the entry in row 1, column 2. + /// + public double M12 { get; init; } + + /// + /// Gets the entry in row 1, column 3. + /// + public double M13 { get; init; } + + /// + /// Gets the entry in row 1, column 4. + /// + public double M14 { get; init; } + + /// + /// Gets the entry in row 2, column 1. + /// + public double M21 { get; init; } + + /// + /// Gets the entry in row 2, column 2. + /// + public double M22 { get; init; } + + /// + /// Gets the entry in row 2, column 3. + /// + public double M23 { get; init; } + + /// + /// Gets the entry in row 2, column 4. + /// + public double M24 { get; init; } + + /// + /// Gets the entry in row 3, column 1. + /// + public double M31 { get; init; } + + /// + /// Gets the entry in row 3, column 2. + /// + public double M32 { get; init; } + + /// + /// Gets the entry in row 3, column 3. + /// + public double M33 { get; init; } + + /// + /// Gets the entry in row 3, column 4. + /// + public double M34 { get; init; } + + /// + /// Gets the entry in row 4, column 1. + /// + public double M41 { get; init; } + + /// + /// Gets the entry in row 4, column 2. + /// + public double M42 { get; init; } + + /// + /// Gets the entry in row 4, column 3. + /// + public double M43 { get; init; } + + /// + /// Gets the entry in row 4, column 4. + /// + public double M44 { get; init; } + + /// + /// Gets the zero matrix. + /// + public static Matrix4x4 Zero => default; + + /// + /// Gets the identity matrix. + /// + public static Matrix4x4 Identity => new(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); + + /// + /// Gets the entry at the specified row-major index (row · 4 + column). + /// + /// The zero-based index between 0 and 15. + /// is out of range. + public double this[int index] => index switch + { + 0 => M11, + 1 => M12, + 2 => M13, + 3 => M14, + 4 => M21, + 5 => M22, + 6 => M23, + 7 => M24, + 8 => M31, + 9 => M32, + 10 => M33, + 11 => M34, + 12 => M41, + 13 => M42, + 14 => M43, + 15 => M44, + _ => throw MatrixMath.IndexOutOfRange(nameof(index), index, 16) + }; + + /// + public double this[int row, int column] + { + get + { + MatrixMath.CheckIndices(row, column, 4); + return this[row * 4 + column]; + } + } + + /// + public int RowCount => 4; + + /// + public int ColumnCount => 4; + + /// + public int Dimension => 4; + + /// + public double Trace => M11 + M22 + M33 + M44; + + /// + public bool IsIdentity => Is(MatrixMath.Predicate.Identity); + + /// + public bool IsDiagonal => Is(MatrixMath.Predicate.Diagonal); + + /// + public bool IsTriangular => Is(MatrixMath.Predicate.Triangular); + + /// + public bool IsSymmetric => Is(MatrixMath.Predicate.Symmetric); + + /// + public bool IsSkewSymmetric => Is(MatrixMath.Predicate.SkewSymmetric); + + /// + public bool IsOrthogonal => (this * Transpose()).IsIdentity; + + /// + public bool IsSingular => Is(MatrixMath.Predicate.Singular); + + /// + /// Creates a matrix from any with 4 rows and 4 columns. + /// + /// The matrix to copy. + /// The copied matrix. + /// The matrix is not 4x4. + public static Matrix4x4 FromMatrix(IMatrix matrix) + { + ArgumentNullException.ThrowIfNull(matrix); + if (matrix.RowCount != 4 || matrix.ColumnCount != 4) + throw new ArgumentException("The matrix must have 4 rows and 4 columns.", nameof(matrix)); + return new Matrix4x4(matrix[0, 0], matrix[0, 1], matrix[0, 2], matrix[0, 3], matrix[1, 0], matrix[1, 1], matrix[1, 2], matrix[1, 3], matrix[2, 0], matrix[2, 1], matrix[2, 2], matrix[2, 3], matrix[3, 0], matrix[3, 1], matrix[3, 2], matrix[3, 3]); + } + + /// + /// Returns the transpose. + /// + /// The transposed matrix. + public Matrix4x4 Transpose() => new(M11, M21, M31, M41, M12, M22, M32, M42, M13, M23, M33, M43, M14, M24, M34, M44); + + /// + /// Returns the matrix of cofactors. + /// + /// The cofactor matrix. + public Matrix4x4 Cofactors() => Adjugate().Transpose(); + + /// + /// Returns the inverse. + /// + /// The inverse matrix. + /// The matrix is singular (see ). + public Matrix4x4 Inverse() => TryInvert(out var inverse) ? inverse : throw MatrixMath.SingularMatrix(); + + /// + /// Tries to calculate the inverse. + /// + /// The inverse, or if the matrix is singular. + /// if the matrix is invertible; otherwise, . + public bool TryInvert(out Matrix4x4 inverse) + { + if (IsSingular) + { + inverse = default; + return false; + } + + // Expanding along the first row reuses the minors already computed for the adjugate. + var adjugate = Adjugate(); + inverse = adjugate / (M11 * adjugate.M11 + M12 * adjugate.M21 + M13 * adjugate.M31 + M14 * adjugate.M41); + return true; + } + + /// + /// Determines whether this matrix equals another matrix within the default tolerances (entry-wise). + /// + /// The other matrix. + /// if the matrices are approximately equal; otherwise, . + public bool ApproximatelyEquals(Matrix4x4 other) => + ApproximatelyEquals(other, Tolerance.DefaultAbsolute, Tolerance.DefaultRelative); + + /// + /// Determines whether this matrix equals another matrix within the specified tolerances (entry-wise). + /// + /// The other matrix. + /// The absolute tolerance. + /// The relative tolerance. + /// if the matrices are approximately equal; otherwise, . + public bool ApproximatelyEquals(Matrix4x4 other, double absolute, double relative) + { + Span entries = stackalloc double[16]; + Span otherEntries = stackalloc double[16]; + CopyTo(entries); + other.CopyTo(otherEntries); + return MatrixMath.ApproximatelyEqual(entries, otherEntries, absolute, relative); + } + + /// + public override string ToString() + { + Span entries = stackalloc double[16]; + CopyTo(entries); + return MatrixMath.Format(nameof(Matrix4x4), entries, 4, 4); + } + + /// + /// Copies the entries in row-major order. + /// + internal void CopyTo(Span destination) + { + destination[0] = M11; + destination[1] = M12; + destination[2] = M13; + destination[3] = M14; + destination[4] = M21; + destination[5] = M22; + destination[6] = M23; + destination[7] = M24; + destination[8] = M31; + destination[9] = M32; + destination[10] = M33; + destination[11] = M34; + destination[12] = M41; + destination[13] = M42; + destination[14] = M43; + destination[15] = M44; + } + + private bool Is(MatrixMath.Predicate predicate) + { + Span entries = stackalloc double[16]; + CopyTo(entries); + return MatrixMath.Evaluate(predicate, entries, 4); + } + + /// + public bool Equals(Matrix4x4 other) => + M11.Equals(other.M11) && + M12.Equals(other.M12) && + M13.Equals(other.M13) && + M14.Equals(other.M14) && + M21.Equals(other.M21) && + M22.Equals(other.M22) && + M23.Equals(other.M23) && + M24.Equals(other.M24) && + M31.Equals(other.M31) && + M32.Equals(other.M32) && + M33.Equals(other.M33) && + M34.Equals(other.M34) && + M41.Equals(other.M41) && + M42.Equals(other.M42) && + M43.Equals(other.M43) && + M44.Equals(other.M44); + + /// + public override bool Equals(object? obj) => obj is Matrix4x4 other && Equals(other); + + /// + public override int GetHashCode() + { + var hash = new HashCode(); + hash.Add(M11); + hash.Add(M12); + hash.Add(M13); + hash.Add(M14); + hash.Add(M21); + hash.Add(M22); + hash.Add(M23); + hash.Add(M24); + hash.Add(M31); + hash.Add(M32); + hash.Add(M33); + hash.Add(M34); + hash.Add(M41); + hash.Add(M42); + hash.Add(M43); + hash.Add(M44); + return hash.ToHashCode(); + } + + /// + /// Adds two matrices. + /// + /// The first matrix. + /// The second matrix. + /// The sum. + public static Matrix4x4 operator +(Matrix4x4 left, Matrix4x4 right) => + new(left.M11 + right.M11, left.M12 + right.M12, left.M13 + right.M13, left.M14 + right.M14, left.M21 + right.M21, left.M22 + right.M22, left.M23 + right.M23, left.M24 + right.M24, left.M31 + right.M31, left.M32 + right.M32, left.M33 + right.M33, left.M34 + right.M34, left.M41 + right.M41, left.M42 + right.M42, left.M43 + right.M43, left.M44 + right.M44); + + /// + /// Subtracts a matrix from another matrix. + /// + /// The minuend. + /// The subtrahend. + /// The difference. + public static Matrix4x4 operator -(Matrix4x4 left, Matrix4x4 right) => + new(left.M11 - right.M11, left.M12 - right.M12, left.M13 - right.M13, left.M14 - right.M14, left.M21 - right.M21, left.M22 - right.M22, left.M23 - right.M23, left.M24 - right.M24, left.M31 - right.M31, left.M32 - right.M32, left.M33 - right.M33, left.M34 - right.M34, left.M41 - right.M41, left.M42 - right.M42, left.M43 - right.M43, left.M44 - right.M44); + + /// + /// Negates a matrix. + /// + /// The matrix. + /// The negated matrix. + public static Matrix4x4 operator -(Matrix4x4 matrix) => + new(-matrix.M11, -matrix.M12, -matrix.M13, -matrix.M14, -matrix.M21, -matrix.M22, -matrix.M23, -matrix.M24, -matrix.M31, -matrix.M32, -matrix.M33, -matrix.M34, -matrix.M41, -matrix.M42, -matrix.M43, -matrix.M44); + + /// + /// Multiplies a matrix with a scalar. + /// + /// The matrix. + /// The scalar. + /// The scaled matrix. + public static Matrix4x4 operator *(Matrix4x4 matrix, double scalar) => + new(matrix.M11 * scalar, matrix.M12 * scalar, matrix.M13 * scalar, matrix.M14 * scalar, matrix.M21 * scalar, matrix.M22 * scalar, matrix.M23 * scalar, matrix.M24 * scalar, matrix.M31 * scalar, matrix.M32 * scalar, matrix.M33 * scalar, matrix.M34 * scalar, matrix.M41 * scalar, matrix.M42 * scalar, matrix.M43 * scalar, matrix.M44 * scalar); + + /// + /// Multiplies a scalar with a matrix. + /// + /// The scalar. + /// The matrix. + /// The scaled matrix. + public static Matrix4x4 operator *(double scalar, Matrix4x4 matrix) => matrix * scalar; + + /// + /// Divides a matrix by a scalar. + /// + /// The matrix. + /// The scalar. + /// The scaled matrix. + public static Matrix4x4 operator /(Matrix4x4 matrix, double scalar) => + new(matrix.M11 / scalar, matrix.M12 / scalar, matrix.M13 / scalar, matrix.M14 / scalar, matrix.M21 / scalar, matrix.M22 / scalar, matrix.M23 / scalar, matrix.M24 / scalar, matrix.M31 / scalar, matrix.M32 / scalar, matrix.M33 / scalar, matrix.M34 / scalar, matrix.M41 / scalar, matrix.M42 / scalar, matrix.M43 / scalar, matrix.M44 / scalar); + + /// + /// Multiplies two matrices. + /// + /// The left matrix. + /// The right matrix. + /// The product left · right. + public static Matrix4x4 operator *(Matrix4x4 left, Matrix4x4 right) => + new(left.M11 * right.M11 + left.M12 * right.M21 + left.M13 * right.M31 + left.M14 * right.M41, + left.M11 * right.M12 + left.M12 * right.M22 + left.M13 * right.M32 + left.M14 * right.M42, + left.M11 * right.M13 + left.M12 * right.M23 + left.M13 * right.M33 + left.M14 * right.M43, + left.M11 * right.M14 + left.M12 * right.M24 + left.M13 * right.M34 + left.M14 * right.M44, + left.M21 * right.M11 + left.M22 * right.M21 + left.M23 * right.M31 + left.M24 * right.M41, + left.M21 * right.M12 + left.M22 * right.M22 + left.M23 * right.M32 + left.M24 * right.M42, + left.M21 * right.M13 + left.M22 * right.M23 + left.M23 * right.M33 + left.M24 * right.M43, + left.M21 * right.M14 + left.M22 * right.M24 + left.M23 * right.M34 + left.M24 * right.M44, + left.M31 * right.M11 + left.M32 * right.M21 + left.M33 * right.M31 + left.M34 * right.M41, + left.M31 * right.M12 + left.M32 * right.M22 + left.M33 * right.M32 + left.M34 * right.M42, + left.M31 * right.M13 + left.M32 * right.M23 + left.M33 * right.M33 + left.M34 * right.M43, + left.M31 * right.M14 + left.M32 * right.M24 + left.M33 * right.M34 + left.M34 * right.M44, + left.M41 * right.M11 + left.M42 * right.M21 + left.M43 * right.M31 + left.M44 * right.M41, + left.M41 * right.M12 + left.M42 * right.M22 + left.M43 * right.M32 + left.M44 * right.M42, + left.M41 * right.M13 + left.M42 * right.M23 + left.M43 * right.M33 + left.M44 * right.M43, + left.M41 * right.M14 + left.M42 * right.M24 + left.M43 * right.M34 + left.M44 * right.M44); + + /// + /// Transforms a column vector. + /// + /// The matrix. + /// The vector. + /// The product matrix · vector. + public static Vector4 operator *(Matrix4x4 matrix, Vector4 vector) => + new(matrix.M11 * vector.X + matrix.M12 * vector.Y + matrix.M13 * vector.Z + matrix.M14 * vector.W, + matrix.M21 * vector.X + matrix.M22 * vector.Y + matrix.M23 * vector.Z + matrix.M24 * vector.W, + matrix.M31 * vector.X + matrix.M32 * vector.Y + matrix.M33 * vector.Z + matrix.M34 * vector.W, + matrix.M41 * vector.X + matrix.M42 * vector.Y + matrix.M43 * vector.Z + matrix.M44 * vector.W); + + /// + /// Determines whether two matrices are exactly equal (entry-wise ==). + /// + /// The first matrix. + /// The second matrix. + /// if all entries are equal; otherwise, . + public static bool operator ==(Matrix4x4 left, Matrix4x4 right) => + left.M11 == right.M11 && + left.M12 == right.M12 && + left.M13 == right.M13 && + left.M14 == right.M14 && + left.M21 == right.M21 && + left.M22 == right.M22 && + left.M23 == right.M23 && + left.M24 == right.M24 && + left.M31 == right.M31 && + left.M32 == right.M32 && + left.M33 == right.M33 && + left.M34 == right.M34 && + left.M41 == right.M41 && + left.M42 == right.M42 && + left.M43 == right.M43 && + left.M44 == right.M44; + + /// + /// Determines whether two matrices differ in at least one entry. + /// + /// The first matrix. + /// The second matrix. + /// if any entry differs; otherwise, . + public static bool operator !=(Matrix4x4 left, Matrix4x4 right) => !(left == right); +} diff --git a/src/SharpMath/Geometry/MatrixMath.cs b/src/SharpMath/Geometry/MatrixMath.cs new file mode 100644 index 0000000..2472008 --- /dev/null +++ b/src/SharpMath/Geometry/MatrixMath.cs @@ -0,0 +1,183 @@ +using System.Globalization; +using System.Text; + +namespace SharpMath.Geometry; + +/// +/// Shared helpers for the matrix types. They work on row-major spans, so the fixed-size matrices can copy their +/// entries to the stack and the checks run without boxing or allocations. +/// +internal static class MatrixMath +{ + /// + /// The machine epsilon of (2⁻⁵²). + /// + public const double MachineEpsilon = 2.220446049250313e-16; + + public enum Predicate + { + Identity, + Diagonal, + Triangular, + Symmetric, + SkewSymmetric, + Singular + } + + public static bool Evaluate(Predicate predicate, ReadOnlySpan entries, int dimension) => predicate switch + { + Predicate.Identity => IsIdentity(entries, dimension), + Predicate.Diagonal => IsDiagonal(entries, dimension), + Predicate.Triangular => IsTriangular(entries, dimension), + Predicate.Symmetric => IsSymmetric(entries, dimension, -1), + Predicate.SkewSymmetric => IsSymmetric(entries, dimension, 1), + _ => IsSingular(entries, dimension) + }; + + public static double MaxAbs(ReadOnlySpan values) + { + var max = 0.0; + foreach (var value in values) + max = Math.Max(max, Math.Abs(value)); + return max; + } + + /// + /// The pivot size below which a matrix counts as singular: n · ε · s, where s is the largest + /// absolute entry. This is the usual backward-stable rank test of an LU decomposition with partial pivoting. + /// + public static double SingularPivotThreshold(int dimension, double scale) => dimension * MachineEpsilon * scale; + + /// + /// Determines singularity with an LU decomposition with partial pivoting (on a stack copy). + /// + public static bool IsSingular(ReadOnlySpan entries, int dimension) + { + var threshold = SingularPivotThreshold(dimension, MaxAbs(entries)); + if (!(threshold > 0)) + return true; + + Span a = stackalloc double[entries.Length]; + entries.CopyTo(a); + for (var k = 0; k < dimension; k++) + { + var pivotRow = k; + for (var row = k + 1; row < dimension; row++) + if (Math.Abs(a[row * dimension + k]) > Math.Abs(a[pivotRow * dimension + k])) + pivotRow = row; + + var pivot = a[pivotRow * dimension + k]; + if (!(Math.Abs(pivot) > threshold)) + return true; + + if (pivotRow != k) + for (var column = k; column < dimension; column++) + (a[k * dimension + column], a[pivotRow * dimension + column]) = + (a[pivotRow * dimension + column], a[k * dimension + column]); + + for (var row = k + 1; row < dimension; row++) + { + var factor = a[row * dimension + k] / pivot; + for (var column = k + 1; column < dimension; column++) + a[row * dimension + column] -= factor * a[k * dimension + column]; + } + } + + return false; + } + + public static bool IsIdentity(ReadOnlySpan entries, int dimension) + { + for (var row = 0; row < dimension; row++) + for (var column = 0; column < dimension; column++) + if (!Tolerance.AreEqual(entries[row * dimension + column], row == column ? 1 : 0)) + return false; + return true; + } + + // Structural checks compare entries relative to the largest entry only, so they do not depend on the scale. + private static bool IsNegligible(double value, double scale) => Math.Abs(value) <= Tolerance.DefaultRelative * scale; + + private static bool IsDiagonal(ReadOnlySpan entries, int dimension) + { + var scale = MaxAbs(entries); + for (var row = 0; row < dimension; row++) + for (var column = 0; column < dimension; column++) + if (row != column && !IsNegligible(entries[row * dimension + column], scale)) + return false; + return true; + } + + private static bool IsTriangular(ReadOnlySpan entries, int dimension) + { + var scale = MaxAbs(entries); + bool upper = true, lower = true; + for (var row = 0; row < dimension; row++) + for (var column = 0; column < dimension; column++) + { + if (row == column || IsNegligible(entries[row * dimension + column], scale)) + continue; + if (row > column) + upper = false; + else + lower = false; + } + + return upper || lower; + } + + /// + /// Checks a[i,j] = −sign · a[j,i]: −1 tests symmetry, +1 skew symmetry. + /// + private static bool IsSymmetric(ReadOnlySpan entries, int dimension, int sign) + { + var scale = MaxAbs(entries); + for (var row = 0; row < dimension; row++) + for (var column = row; column < dimension; column++) + if (!IsNegligible(entries[row * dimension + column] + sign * entries[column * dimension + row], scale)) + return false; + return true; + } + + public static bool ApproximatelyEqual(ReadOnlySpan left, ReadOnlySpan right, double absolute, + double relative) + { + for (var i = 0; i < left.Length; i++) + if (!Tolerance.AreEqual(left[i], right[i], absolute, relative)) + return false; + return true; + } + + public static string Format(string name, ReadOnlySpan entries, int rowCount, int columnCount) + { + var builder = new StringBuilder(name).Append(" ["); + for (var row = 0; row < rowCount; row++) + { + builder.Append(row == 0 ? "[" : ", ["); + for (var column = 0; column < columnCount; column++) + { + if (column > 0) + builder.Append(", "); + builder.Append(entries[row * columnCount + column].ToString(CultureInfo.InvariantCulture)); + } + + builder.Append(']'); + } + + return builder.Append(']').ToString(); + } + + public static ArgumentOutOfRangeException IndexOutOfRange(string parameterName, int value, int count) => + new(parameterName, value, $"The index must be between 0 and {count - 1}."); + + public static void CheckIndices(int row, int column, int dimension) + { + if ((uint) row >= (uint) dimension) + throw IndexOutOfRange(nameof(row), row, dimension); + if ((uint) column >= (uint) dimension) + throw IndexOutOfRange(nameof(column), column, dimension); + } + + public static InvalidOperationException SingularMatrix() => + new("The matrix is singular and has no inverse."); +} diff --git a/src/SharpMath/Geometry/MatrixMxN.cs b/src/SharpMath/Geometry/MatrixMxN.cs new file mode 100644 index 0000000..aeac111 --- /dev/null +++ b/src/SharpMath/Geometry/MatrixMxN.cs @@ -0,0 +1,614 @@ +using System.Diagnostics.CodeAnalysis; + +namespace SharpMath.Geometry; + +/// +/// Represents an immutable matrix of arbitrary size. +/// +/// +/// Determinant, inverse and solving use an LU decomposition with partial pivoting. A matrix is treated as singular if +/// a pivot is not larger than n · ε · s, where ε is the machine epsilon of and +/// s the largest absolute entry; the fixed-size matrices use the same rule for IsSingular. Equality is exact; use for +/// tolerance-based comparisons. +/// +public sealed class MatrixMxN : IMatrix, IEquatable +{ + private readonly double[] _values; + + /// + /// Initializes a new instance of the class by copying a two-dimensional array. + /// + /// The entries, indexed as [row, column]. + /// has no rows or no columns. + public MatrixMxN(double[,] values) + { + ArgumentNullException.ThrowIfNull(values); + RowCount = values.GetLength(0); + ColumnCount = values.GetLength(1); + if (RowCount == 0 || ColumnCount == 0) + throw new ArgumentException("A matrix needs at least one row and one column.", nameof(values)); + + _values = new double[RowCount * ColumnCount]; + for (var row = 0; row < RowCount; row++) + for (var column = 0; column < ColumnCount; column++) + _values[row * ColumnCount + column] = values[row, column]; + } + + /// + /// Initializes a new instance of the class from entries in row-major order. + /// + /// The number of rows. Must be positive. + /// The number of columns. Must be positive. + /// The entries in row-major order; exactly rowCount · columnCount values. + /// A count is not positive. + /// The number of values does not match the size. + public MatrixMxN(int rowCount, int columnCount, ReadOnlySpan rowMajorValues) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(rowCount); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(columnCount); + if (rowMajorValues.Length != rowCount * columnCount) + throw new ArgumentException($"Expected {rowCount * columnCount} values.", nameof(rowMajorValues)); + + RowCount = rowCount; + ColumnCount = columnCount; + _values = rowMajorValues.ToArray(); + } + + private MatrixMxN(int rowCount, int columnCount, double[] values) + { + RowCount = rowCount; + ColumnCount = columnCount; + _values = values; + } + + /// + public int RowCount { get; } + + /// + public int ColumnCount { get; } + + /// + /// Gets a value indicating whether the matrix has as many rows as columns. + /// + public bool IsSquare => RowCount == ColumnCount; + + /// + public double this[int row, int column] + { + get + { + if ((uint) row >= (uint) RowCount) + throw MatrixMath.IndexOutOfRange(nameof(row), row, RowCount); + if ((uint) column >= (uint) ColumnCount) + throw MatrixMath.IndexOutOfRange(nameof(column), column, ColumnCount); + return _values[row * ColumnCount + column]; + } + } + + /// + /// Creates a zero matrix. + /// + /// The number of rows. Must be positive. + /// The number of columns. Must be positive. + /// The zero matrix. + public static MatrixMxN Zero(int rowCount, int columnCount) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(rowCount); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(columnCount); + return new MatrixMxN(rowCount, columnCount, new double[rowCount * columnCount]); + } + + /// + /// Creates an identity matrix. + /// + /// The number of rows and columns. Must be positive. + /// The identity matrix. + public static MatrixMxN Identity(int dimension) + { + var identity = Zero(dimension, dimension); + for (var i = 0; i < dimension; i++) + identity._values[i * dimension + i] = 1; + return identity; + } + + /// + /// Creates a matrix by copying any , e.g. a . + /// + /// The matrix to copy. + /// The copied matrix. + public static MatrixMxN FromMatrix(IMatrix matrix) + { + ArgumentNullException.ThrowIfNull(matrix); + var values = new double[matrix.RowCount * matrix.ColumnCount]; + for (var row = 0; row < matrix.RowCount; row++) + for (var column = 0; column < matrix.ColumnCount; column++) + values[row * matrix.ColumnCount + column] = matrix[row, column]; + return new MatrixMxN(matrix.RowCount, matrix.ColumnCount, values); + } + + /// + /// Copies the entries into a new two-dimensional array. + /// + /// The entries, indexed as [row, column]. + public double[,] ToArray() + { + var array = new double[RowCount, ColumnCount]; + for (var row = 0; row < RowCount; row++) + for (var column = 0; column < ColumnCount; column++) + array[row, column] = _values[row * ColumnCount + column]; + return array; + } + + /// + /// Returns the transpose. + /// + /// The transposed matrix. + public MatrixMxN Transpose() + { + var values = new double[_values.Length]; + for (var row = 0; row < RowCount; row++) + for (var column = 0; column < ColumnCount; column++) + values[column * RowCount + row] = _values[row * ColumnCount + column]; + return new MatrixMxN(ColumnCount, RowCount, values); + } + + /// + /// Calculates the determinant. + /// + /// The determinant, computed as the signed product of the LU pivots. + /// The matrix is not square. + public double Determinant() + { + EnsureSquare(); + return LuDecomposition.Decompose(RowCount, _values).Determinant; + } + + /// + /// Returns the inverse. + /// + /// The inverse matrix. + /// The matrix is not square or singular. + public MatrixMxN Inverse() => TryInvert(out var inverse) ? inverse : throw MatrixMath.SingularMatrix(); + + /// + /// Tries to calculate the inverse. + /// + /// The inverse, or if the matrix is singular. + /// if the matrix is invertible; otherwise, . + /// The matrix is not square. + public bool TryInvert([NotNullWhen(true)] out MatrixMxN? inverse) + { + EnsureSquare(); + var lu = LuDecomposition.Decompose(RowCount, _values); + if (lu.IsSingular) + { + inverse = null; + return false; + } + + var n = RowCount; + var values = new double[n * n]; + var column = new double[n]; + for (var j = 0; j < n; j++) + { + Array.Clear(column); + column[j] = 1; + lu.Solve(column); + for (var i = 0; i < n; i++) + values[i * n + j] = column[i]; + } + + inverse = new MatrixMxN(n, n, values); + return true; + } + + /// + /// Solves the linear system this · x = rightHandSide. + /// + /// The right-hand side; one value per row. + /// The solution x. + /// The matrix is not square or singular. + /// The length of does not match. + public double[] Solve(ReadOnlySpan rightHandSide) => + TrySolve(rightHandSide, out var solution) ? solution : throw MatrixMath.SingularMatrix(); + + /// + /// Solves the linear system this · X = rightHandSide for every column of the right-hand side. + /// + /// The right-hand side with as many rows as this matrix. + /// The solution X. + /// The matrix is not square or singular. + /// The row count of does not match. + public MatrixMxN Solve(MatrixMxN rightHandSide) + { + ArgumentNullException.ThrowIfNull(rightHandSide); + EnsureSquare(); + if (rightHandSide.RowCount != RowCount) + throw new ArgumentException("The right-hand side must have as many rows as the matrix.", + nameof(rightHandSide)); + + var lu = LuDecomposition.Decompose(RowCount, _values); + if (lu.IsSingular) + throw MatrixMath.SingularMatrix(); + + int n = RowCount, m = rightHandSide.ColumnCount; + var values = new double[n * m]; + var column = new double[n]; + for (var j = 0; j < m; j++) + { + for (var i = 0; i < n; i++) + column[i] = rightHandSide._values[i * m + j]; + lu.Solve(column); + for (var i = 0; i < n; i++) + values[i * m + j] = column[i]; + } + + return new MatrixMxN(n, m, values); + } + + /// + /// Calculates the rank, i.e. the number of linearly independent rows. + /// + /// The rank. + public int Rank() + { + int rows = RowCount, columns = ColumnCount; + var a = (double[]) _values.Clone(); + var threshold = MatrixMath.SingularPivotThreshold(Math.Max(rows, columns), MatrixMath.MaxAbs(_values)); + var rank = 0; + for (var column = 0; column < columns && rank < rows; column++) + { + var pivotRow = rank; + for (var row = rank + 1; row < rows; row++) + if (Math.Abs(a[row * columns + column]) > Math.Abs(a[pivotRow * columns + column])) + pivotRow = row; + if (Math.Abs(a[pivotRow * columns + column]) <= threshold) + continue; + + LuDecomposition.SwapRows(a, columns, pivotRow, rank); + for (var row = rank + 1; row < rows; row++) + { + var factor = a[row * columns + column] / a[rank * columns + column]; + for (var k = column; k < columns; k++) + a[row * columns + k] -= factor * a[rank * columns + k]; + } + + rank++; + } + + return rank; + } + + /// + /// Determines whether this matrix equals another matrix within the default tolerances (entry-wise). + /// + /// The other matrix. + /// + /// if both matrices have the same size and approximately equal entries; otherwise, + /// . + /// + public bool ApproximatelyEquals(MatrixMxN other) => + ApproximatelyEquals(other, Tolerance.DefaultAbsolute, Tolerance.DefaultRelative); + + /// + /// Determines whether this matrix equals another matrix within the specified tolerances (entry-wise). + /// + /// The other matrix. + /// The absolute tolerance. + /// The relative tolerance. + /// + /// if both matrices have the same size and approximately equal entries; otherwise, + /// . + /// + public bool ApproximatelyEquals(MatrixMxN other, double absolute, double relative) + { + ArgumentNullException.ThrowIfNull(other); + if (RowCount != other.RowCount || ColumnCount != other.ColumnCount) + return false; + for (var i = 0; i < _values.Length; i++) + if (!Tolerance.AreEqual(_values[i], other._values[i], absolute, relative)) + return false; + return true; + } + + /// + public override string ToString() => MatrixMath.Format(nameof(MatrixMxN), _values, RowCount, ColumnCount); + + /// + public bool Equals(MatrixMxN? other) + { + if (other is null) + return false; + if (ReferenceEquals(this, other)) + return true; + return RowCount == other.RowCount && ColumnCount == other.ColumnCount && + _values.AsSpan().SequenceEqual(other._values); + } + + /// + public override bool Equals(object? obj) => Equals(obj as MatrixMxN); + + /// + public override int GetHashCode() + { + var hash = new HashCode(); + hash.Add(RowCount); + hash.Add(ColumnCount); + foreach (var value in _values) + hash.Add(value); + return hash.ToHashCode(); + } + + /// + /// Adds two matrices of the same size. + /// + /// The first matrix. + /// The second matrix. + /// The sum. + /// The sizes differ. + public static MatrixMxN operator +(MatrixMxN left, MatrixMxN right) => Combine(left, right, static (a, b) => a + b); + + /// + /// Subtracts a matrix from another matrix of the same size. + /// + /// The minuend. + /// The subtrahend. + /// The difference. + /// The sizes differ. + public static MatrixMxN operator -(MatrixMxN left, MatrixMxN right) => Combine(left, right, static (a, b) => a - b); + + /// + /// Negates a matrix. + /// + /// The matrix. + /// The negated matrix. + public static MatrixMxN operator -(MatrixMxN matrix) => matrix * -1; + + /// + /// Multiplies a matrix with a scalar. + /// + /// The matrix. + /// The scalar. + /// The scaled matrix. + public static MatrixMxN operator *(MatrixMxN matrix, double scalar) + { + ArgumentNullException.ThrowIfNull(matrix); + var values = new double[matrix._values.Length]; + for (var i = 0; i < values.Length; i++) + values[i] = matrix._values[i] * scalar; + return new MatrixMxN(matrix.RowCount, matrix.ColumnCount, values); + } + + /// + /// Multiplies a scalar with a matrix. + /// + /// The scalar. + /// The matrix. + /// The scaled matrix. + public static MatrixMxN operator *(double scalar, MatrixMxN matrix) => matrix * scalar; + + /// + /// Divides a matrix by a scalar. + /// + /// The matrix. + /// The scalar. + /// The scaled matrix. + public static MatrixMxN operator /(MatrixMxN matrix, double scalar) + { + ArgumentNullException.ThrowIfNull(matrix); + var values = new double[matrix._values.Length]; + for (var i = 0; i < values.Length; i++) + values[i] = matrix._values[i] / scalar; + return new MatrixMxN(matrix.RowCount, matrix.ColumnCount, values); + } + + /// + /// Multiplies two matrices. + /// + /// The left matrix. + /// The right matrix; must have as many rows as has columns. + /// The product left · right. + /// The sizes are incompatible. + public static MatrixMxN operator *(MatrixMxN left, MatrixMxN right) + { + ArgumentNullException.ThrowIfNull(left); + ArgumentNullException.ThrowIfNull(right); + if (left.ColumnCount != right.RowCount) + throw new ArgumentException( + "The column count of the left matrix must match the row count of the right matrix.", nameof(right)); + + int rows = left.RowCount, inner = left.ColumnCount, columns = right.ColumnCount; + var values = new double[rows * columns]; + for (var row = 0; row < rows; row++) + for (var k = 0; k < inner; k++) + { + var factor = left._values[row * inner + k]; + if (factor == 0) + continue; + for (var column = 0; column < columns; column++) + values[row * columns + column] += factor * right._values[k * columns + column]; + } + + return new MatrixMxN(rows, columns, values); + } + + /// + /// Determines whether two matrices are exactly equal. + /// + /// The first matrix. + /// The second matrix. + /// if both are or equal; otherwise, . + public static bool operator ==(MatrixMxN? left, MatrixMxN? right) + { + if (left is null || right is null) + return left is null && right is null; + if (left.RowCount != right.RowCount || left.ColumnCount != right.ColumnCount) + return false; + for (var i = 0; i < left._values.Length; i++) + if (left._values[i] != right._values[i]) + return false; + return true; + } + + /// + /// Determines whether two matrices differ. + /// + /// The first matrix. + /// The second matrix. + /// if the matrices differ; otherwise, . + public static bool operator !=(MatrixMxN? left, MatrixMxN? right) => !(left == right); + + /// + /// Creates a matrix that takes ownership of a row-major array without copying it. + /// + internal static MatrixMxN WrapRowMajor(int rowCount, int columnCount, double[] values) => + new(rowCount, columnCount, values); + + internal bool TrySolve(ReadOnlySpan rightHandSide, + [NotNullWhen(true)] out double[]? solution) + { + EnsureSquare(); + if (rightHandSide.Length != RowCount) + throw new ArgumentException("The right-hand side must have one value per row.", nameof(rightHandSide)); + + var lu = LuDecomposition.Decompose(RowCount, _values); + if (lu.IsSingular) + { + solution = null; + return false; + } + + solution = rightHandSide.ToArray(); + lu.Solve(solution); + return true; + } + + private static MatrixMxN Combine(MatrixMxN left, MatrixMxN right, Func operation) + { + ArgumentNullException.ThrowIfNull(left); + ArgumentNullException.ThrowIfNull(right); + if (left.RowCount != right.RowCount || left.ColumnCount != right.ColumnCount) + throw new ArgumentException("The matrices must have the same size.", nameof(right)); + + var values = new double[left._values.Length]; + for (var i = 0; i < values.Length; i++) + values[i] = operation(left._values[i], right._values[i]); + return new MatrixMxN(left.RowCount, left.ColumnCount, values); + } + + private void EnsureSquare() + { + if (!IsSquare) + throw new InvalidOperationException("The operation requires a square matrix."); + } + + /// + /// An LU decomposition with partial pivoting (P · A = L · U), stored compactly. + /// + private sealed class LuDecomposition + { + private readonly double[] _lu; + private readonly int _n; + private readonly int[] _permutation; + private readonly double[] _scratch; + + private LuDecomposition(int n, double[] lu, int[] permutation, double determinant, bool isSingular) + { + _n = n; + _lu = lu; + _permutation = permutation; + _scratch = new double[n]; + Determinant = determinant; + IsSingular = isSingular; + } + + /// + /// Gets a value indicating whether a pivot was not larger than + /// . + /// + public bool IsSingular { get; } + + public double Determinant { get; } + + public static LuDecomposition Decompose(int n, double[] values) + { + var lu = (double[]) values.Clone(); + var permutation = new int[n]; + for (var i = 0; i < n; i++) + permutation[i] = i; + + var threshold = MatrixMath.SingularPivotThreshold(n, MatrixMath.MaxAbs(values)); + var isSingular = !(threshold > 0); + var determinant = 1.0; + for (var k = 0; k < n; k++) + { + var pivotRow = k; + for (var row = k + 1; row < n; row++) + if (Math.Abs(lu[row * n + k]) > Math.Abs(lu[pivotRow * n + k])) + pivotRow = row; + + if (pivotRow != k) + { + SwapRows(lu, n, pivotRow, k); + (permutation[pivotRow], permutation[k]) = (permutation[k], permutation[pivotRow]); + determinant = -determinant; + } + + // Tiny pivots make the matrix numerically singular, but the determinant is still their product. + var pivot = lu[k * n + k]; + determinant *= pivot; + isSingular |= !(Math.Abs(pivot) > threshold); + if (pivot == 0) + return new LuDecomposition(n, lu, permutation, 0, true); + + for (var row = k + 1; row < n; row++) + { + var factor = lu[row * n + k] /= pivot; + for (var column = k + 1; column < n; column++) + lu[row * n + column] -= factor * lu[k * n + column]; + } + } + + return new LuDecomposition(n, lu, permutation, determinant, isSingular); + } + + public static void SwapRows(double[] values, int columns, int first, int second) + { + if (first == second) + return; + for (var column = 0; column < columns; column++) + (values[first * columns + column], values[second * columns + column]) = + (values[second * columns + column], values[first * columns + column]); + } + + /// + /// Overwrites with the solution of A · x = b. + /// + public void Solve(Span b) + { + var permuted = _scratch.AsSpan(); + for (var i = 0; i < _n; i++) + permuted[i] = b[_permutation[i]]; + + // Forward substitution with the unit lower triangle. + for (var i = 0; i < _n; i++) + { + var sum = permuted[i]; + for (var j = 0; j < i; j++) + sum -= _lu[i * _n + j] * permuted[j]; + permuted[i] = sum; + } + + // Back substitution with the upper triangle. + for (var i = _n - 1; i >= 0; i--) + { + var sum = permuted[i]; + for (var j = i + 1; j < _n; j++) + sum -= _lu[i * _n + j] * permuted[j]; + permuted[i] = sum / _lu[i * _n + i]; + } + + permuted.CopyTo(b); + } + } +} diff --git a/src/SharpMath/Geometry/Point2D.cs b/src/SharpMath/Geometry/Point2D.cs new file mode 100644 index 0000000..ae248ab --- /dev/null +++ b/src/SharpMath/Geometry/Point2D.cs @@ -0,0 +1,157 @@ +using System.Globalization; + +namespace SharpMath.Geometry; + +/// +/// Represents an immutable point in the plane. +/// +/// +/// Points follow affine-space rules: point − point is a , point ± vector is a +/// point. Equality is exact; use for tolerance-based comparisons. +/// +public readonly struct Point2D : IEquatable +{ + /// + /// Initializes a new instance of the struct. + /// + /// The X coordinate. + /// The Y coordinate. + public Point2D(double x, double y) + { + X = x; + Y = y; + } + + /// + /// Gets the X coordinate. + /// + public double X { get; init; } + + /// + /// Gets the Y coordinate. + /// + public double Y { get; init; } + + /// + /// Gets the origin (0, 0). + /// + public static Point2D Origin => default; + + /// + /// Gets the coordinate at the specified index (0 = X, 1 = Y). + /// + /// The zero-based index. + /// is not 0 or 1. + public double this[int index] => index switch + { + 0 => X, + 1 => Y, + _ => throw new ArgumentOutOfRangeException(nameof(index), index, "The index must be 0 or 1.") + }; + + /// + /// Returns the position vector of the point. + /// + /// The vector from the origin to the point. + public Vector2 ToVector() => new(X, Y); + + /// + /// Calculates the distance to another point. + /// + /// The other point. + /// The distance. + public double Distance(Point2D other) => (other - this).Length; + + /// + /// Calculates the distance between two points. + /// + /// The first point. + /// The second point. + /// The distance. + public static double Distance(Point2D left, Point2D right) => left.Distance(right); + + /// + /// Calculates the squared distance to another point. + /// + /// The other point. + /// The squared distance. + public double DistanceSquared(Point2D other) => (other - this).LengthSquared; + + /// + /// Calculates the squared distance between two points. + /// + /// The first point. + /// The second point. + /// The squared distance. + public static double DistanceSquared(Point2D left, Point2D right) => left.DistanceSquared(right); + + /// + /// Determines whether this point equals another point within the default tolerances (coordinate-wise). + /// + /// The other point. + /// if the points are approximately equal; otherwise, . + public bool ApproximatelyEquals(Point2D other) => + ApproximatelyEquals(other, Tolerance.DefaultAbsolute, Tolerance.DefaultRelative); + + /// + /// Determines whether this point equals another point within the specified tolerances (coordinate-wise). + /// + /// The other point. + /// The absolute tolerance. + /// The relative tolerance. + /// if the points are approximately equal; otherwise, . + public bool ApproximatelyEquals(Point2D other, double absolute, double relative) => + Tolerance.AreEqual(X, other.X, absolute, relative) && Tolerance.AreEqual(Y, other.Y, absolute, relative); + + /// + public override string ToString() => string.Create(CultureInfo.InvariantCulture, $"Point2D [X: {X}, Y: {Y}]"); + + /// + public bool Equals(Point2D other) => X.Equals(other.X) && Y.Equals(other.Y); + + /// + public override bool Equals(object? obj) => obj is Point2D other && Equals(other); + + /// + public override int GetHashCode() => HashCode.Combine(X, Y); + + /// + /// Translates a point by a vector. + /// + /// The point. + /// The translation. + /// The translated point. + public static Point2D operator +(Point2D point, Vector2 vector) => new(point.X + vector.X, point.Y + vector.Y); + + /// + /// Translates a point by the negated vector. + /// + /// The point. + /// The translation to subtract. + /// The translated point. + public static Point2D operator -(Point2D point, Vector2 vector) => new(point.X - vector.X, point.Y - vector.Y); + + /// + /// Calculates the vector from one point to another. + /// + /// The point the vector points to. + /// The point the vector starts at. + /// The vector end − start. + public static Vector2 operator -(Point2D end, Point2D start) => new(end.X - start.X, end.Y - start.Y); + + /// + /// Determines whether two points are exactly equal (coordinate-wise ==). + /// + /// The first point. + /// The second point. + /// if all coordinates are equal; otherwise, . + public static bool operator ==(Point2D left, Point2D right) => left.X == right.X && left.Y == right.Y; + + /// + /// Determines whether two points differ in at least one coordinate. + /// + /// The first point. + /// The second point. + /// if any coordinate differs; otherwise, . + public static bool operator !=(Point2D left, Point2D right) => !(left == right); +} diff --git a/src/SharpMath/Geometry/Point3D.cs b/src/SharpMath/Geometry/Point3D.cs new file mode 100644 index 0000000..4e95907 --- /dev/null +++ b/src/SharpMath/Geometry/Point3D.cs @@ -0,0 +1,166 @@ +using System.Globalization; + +namespace SharpMath.Geometry; + +/// +/// Represents an immutable point in three-dimensional space. +/// +/// +/// Points follow affine-space rules: point − point is a , point ± vector is a +/// point. Equality is exact; use for tolerance-based comparisons. +/// +public readonly struct Point3D : IEquatable +{ + /// + /// Initializes a new instance of the struct. + /// + /// The X coordinate. + /// The Y coordinate. + /// The Z coordinate. + public Point3D(double x, double y, double z) + { + X = x; + Y = y; + Z = z; + } + + /// + /// Gets the X coordinate. + /// + public double X { get; init; } + + /// + /// Gets the Y coordinate. + /// + public double Y { get; init; } + + /// + /// Gets the Z coordinate. + /// + public double Z { get; init; } + + /// + /// Gets the origin (0, 0, 0). + /// + public static Point3D Origin => default; + + /// + /// Gets the coordinate at the specified index (0 = X, 1 = Y, 2 = Z). + /// + /// The zero-based index. + /// is not between 0 and 2. + public double this[int index] => index switch + { + 0 => X, + 1 => Y, + 2 => Z, + _ => throw new ArgumentOutOfRangeException(nameof(index), index, "The index must be between 0 and 2.") + }; + + /// + /// Returns the position vector of the point. + /// + /// The vector from the origin to the point. + public Vector3 ToVector() => new(X, Y, Z); + + /// + /// Calculates the distance to another point. + /// + /// The other point. + /// The distance. + public double Distance(Point3D other) => (other - this).Length; + + /// + /// Calculates the distance between two points. + /// + /// The first point. + /// The second point. + /// The distance. + public static double Distance(Point3D left, Point3D right) => left.Distance(right); + + /// + /// Calculates the squared distance to another point. + /// + /// The other point. + /// The squared distance. + public double DistanceSquared(Point3D other) => (other - this).LengthSquared; + + /// + /// Calculates the squared distance between two points. + /// + /// The first point. + /// The second point. + /// The squared distance. + public static double DistanceSquared(Point3D left, Point3D right) => left.DistanceSquared(right); + + /// + /// Determines whether this point equals another point within the default tolerances (coordinate-wise). + /// + /// The other point. + /// if the points are approximately equal; otherwise, . + public bool ApproximatelyEquals(Point3D other) => + ApproximatelyEquals(other, Tolerance.DefaultAbsolute, Tolerance.DefaultRelative); + + /// + /// Determines whether this point equals another point within the specified tolerances (coordinate-wise). + /// + /// The other point. + /// The absolute tolerance. + /// The relative tolerance. + /// if the points are approximately equal; otherwise, . + public bool ApproximatelyEquals(Point3D other, double absolute, double relative) => + Tolerance.AreEqual(X, other.X, absolute, relative) && Tolerance.AreEqual(Y, other.Y, absolute, relative) && + Tolerance.AreEqual(Z, other.Z, absolute, relative); + + /// + public override string ToString() => string.Create(CultureInfo.InvariantCulture, $"Point3D [X: {X}, Y: {Y}, Z: {Z}]"); + + /// + public bool Equals(Point3D other) => X.Equals(other.X) && Y.Equals(other.Y) && Z.Equals(other.Z); + + /// + public override bool Equals(object? obj) => obj is Point3D other && Equals(other); + + /// + public override int GetHashCode() => HashCode.Combine(X, Y, Z); + + /// + /// Translates a point by a vector. + /// + /// The point. + /// The translation. + /// The translated point. + public static Point3D operator +(Point3D point, Vector3 vector) => new(point.X + vector.X, point.Y + vector.Y, point.Z + vector.Z); + + /// + /// Translates a point by the negated vector. + /// + /// The point. + /// The translation to subtract. + /// The translated point. + public static Point3D operator -(Point3D point, Vector3 vector) => new(point.X - vector.X, point.Y - vector.Y, point.Z - vector.Z); + + /// + /// Calculates the vector from one point to another. + /// + /// The point the vector points to. + /// The point the vector starts at. + /// The vector end − start. + public static Vector3 operator -(Point3D end, Point3D start) => new(end.X - start.X, end.Y - start.Y, end.Z - start.Z); + + /// + /// Determines whether two points are exactly equal (coordinate-wise ==). + /// + /// The first point. + /// The second point. + /// if all coordinates are equal; otherwise, . + public static bool operator ==(Point3D left, Point3D right) => left.X == right.X && left.Y == right.Y && left.Z == right.Z; + + /// + /// Determines whether two points differ in at least one coordinate. + /// + /// The first point. + /// The second point. + /// if any coordinate differs; otherwise, . + public static bool operator !=(Point3D left, Point3D right) => !(left == right); +} diff --git a/src/SharpMath/Geometry/Polygon.cs b/src/SharpMath/Geometry/Polygon.cs new file mode 100644 index 0000000..739632a --- /dev/null +++ b/src/SharpMath/Geometry/Polygon.cs @@ -0,0 +1,292 @@ +using System.Collections.ObjectModel; +using System.Globalization; +using System.Text; + +namespace SharpMath.Geometry; + +/// +/// Represents an immutable simple polygon in the plane, defined by its vertices in order. +/// +/// +/// The polygon is closed implicitly (the last vertex connects to the first). Equality is exact and requires the same +/// vertices in the same order; compares with tolerances. +/// +public sealed class Polygon : IEquatable +{ + private readonly Point2D[] _points; + + // The largest absolute coordinate, used to scale the tolerance of boundary checks. + private readonly double _scale; + + /// + /// Initializes a new instance of the class. + /// + /// The vertices in order; at least three. + /// Fewer than three vertices are specified. + public Polygon(params ReadOnlySpan points) + { + if (points.Length < 3) + throw new ArgumentException("A polygon needs at least three points.", nameof(points)); + _points = points.ToArray(); + Points = Array.AsReadOnly(_points); + foreach (var point in _points) + _scale = Math.Max(_scale, Math.Max(Math.Abs(point.X), Math.Abs(point.Y))); + } + + /// + /// Initializes a new instance of the class. + /// + /// The vertices in order; at least three. + /// Fewer than three vertices are specified. + public Polygon(IEnumerable points) : this( + (points ?? throw new ArgumentNullException(nameof(points))).ToArray().AsSpan()) + { + } + + /// + /// Gets the vertices in order. + /// + public ReadOnlyCollection Points { get; } + + /// + /// Gets the signed area: positive if the vertices are ordered counterclockwise, negative if clockwise. + /// + public double SignedArea + { + get + { + var sum = 0.0; + for (int i = 0, j = _points.Length - 1; i < _points.Length; j = i++) + sum += _points[j].X * _points[i].Y - _points[i].X * _points[j].Y; + return sum / 2; + } + } + + /// + /// Gets the area (shoelace formula). + /// + public double Area => Math.Abs(SignedArea); + + /// + /// Gets the perimeter. + /// + public double Perimeter + { + get + { + var sum = 0.0; + for (int i = 0, j = _points.Length - 1; i < _points.Length; j = i++) + sum += _points[j].Distance(_points[i]); + return sum; + } + } + + /// + /// Gets the centroid (center of mass of the enclosed area). For a polygon without area (all vertices collinear), + /// the average of the vertices is returned. + /// + public Point2D Centroid + { + get + { + double x = 0, y = 0, twiceArea = 0; + for (int i = 0, j = _points.Length - 1; i < _points.Length; j = i++) + { + var cross = _points[j].X * _points[i].Y - _points[i].X * _points[j].Y; + twiceArea += cross; + x += (_points[j].X + _points[i].X) * cross; + y += (_points[j].Y + _points[i].Y) * cross; + } + + if (twiceArea == 0) + { + foreach (var point in _points) + { + x += point.X; + y += point.Y; + } + + return new Point2D(x / _points.Length, y / _points.Length); + } + + return new Point2D(x / (3 * twiceArea), y / (3 * twiceArea)); + } + } + + /// + /// Gets the axis-aligned bounding box. + /// + public BoundingBox2D BoundingBox => BoundingBox2D.FromPoints(_points); + + /// + /// Determines whether a point lies inside the polygon. Points on the border count as inside. + /// + /// The point. + /// if the polygon contains the point; otherwise, . + public bool Contains(Point2D point) + { + var tolerance = SegmentMath.DistanceTolerance(Math.Max(_scale, SegmentMath.CoordinateScale(point))); + var inside = false; + for (int i = 0, j = _points.Length - 1; i < _points.Length; j = i++) + { + Point2D a = _points[j], b = _points[i]; + if (SegmentMath.IsOnSegment(point, a, b, tolerance)) + return true; + + // Crossing number test with a horizontal ray to the right. + if ((b.Y > point.Y) != (a.Y > point.Y) && point.X < (a.X - b.X) * (point.Y - b.Y) / (a.Y - b.Y) + b.X) + inside = !inside; + } + + return inside; + } + + /// + /// Determines whether another polygon lies completely inside this polygon (touching the border is allowed). + /// + /// The other polygon. + /// if this polygon contains the other polygon; otherwise, . + public bool Contains(Polygon other) + { + ArgumentNullException.ThrowIfNull(other); + + foreach (var point in other._points) + if (!Contains(point)) + return false; + + // An edge may still leave and re-enter (e.g. through a concave notch or exactly through vertices). Split every + // edge at its intersections with the border; each piece must then lie inside, so testing its midpoint suffices. + var parameters = new List(); + for (int i = 0, j = other._points.Length - 1; i < other._points.Length; j = i++) + { + var edge = new LineSegment2D(other._points[j], other._points[i]); + CollectBorderIntersections(edge, parameters); + for (var k = 1; k < parameters.Count; k++) + { + var middle = (parameters[k - 1] + parameters[k]) / 2; + if (parameters[k] - parameters[k - 1] > 1e-12 && !Contains(edge.Start + edge.Direction * middle)) + return false; + } + } + + return true; + } + + /// + /// Determines whether this polygon equals another polygon within the default tolerances (same vertices in the same + /// order). + /// + /// The other polygon. + /// if the polygons are approximately equal; otherwise, . + public bool ApproximatelyEquals(Polygon other) => + ApproximatelyEquals(other, Tolerance.DefaultAbsolute, Tolerance.DefaultRelative); + + /// + /// Determines whether this polygon equals another polygon within the specified tolerances (same vertices in the + /// same order). + /// + /// The other polygon. + /// The absolute tolerance. + /// The relative tolerance. + /// if the polygons are approximately equal; otherwise, . + public bool ApproximatelyEquals(Polygon other, double absolute, double relative) + { + ArgumentNullException.ThrowIfNull(other); + if (_points.Length != other._points.Length) + return false; + for (var i = 0; i < _points.Length; i++) + if (!_points[i].ApproximatelyEquals(other._points[i], absolute, relative)) + return false; + return true; + } + + /// + public override string ToString() + { + var builder = new StringBuilder("Polygon ["); + for (var i = 0; i < _points.Length; i++) + { + if (i > 0) + builder.Append(", "); + builder.Append(CultureInfo.InvariantCulture, $"({_points[i].X}, {_points[i].Y})"); + } + + return builder.Append(']').ToString(); + } + + /// + public bool Equals(Polygon? other) => + other is not null && (ReferenceEquals(this, other) || _points.AsSpan().SequenceEqual(other._points)); + + /// + public override bool Equals(object? obj) => Equals(obj as Polygon); + + /// + public override int GetHashCode() + { + var hash = new HashCode(); + foreach (var point in _points) + hash.Add(point); + return hash.ToHashCode(); + } + + /// + /// Determines whether two polygons have the same vertices in the same order. + /// + /// The first polygon. + /// The second polygon. + /// if both are or equal; otherwise, . + public static bool operator ==(Polygon? left, Polygon? right) + { + if (left is null || right is null) + return left is null && right is null; + if (left._points.Length != right._points.Length) + return false; + for (var i = 0; i < left._points.Length; i++) + if (left._points[i] != right._points[i]) + return false; + return true; + } + + /// + /// Determines whether two polygons differ. + /// + /// The first polygon. + /// The second polygon. + /// if the polygons differ; otherwise, . + public static bool operator !=(Polygon? left, Polygon? right) => !(left == right); + + /// + /// Fills with 0, 1 and the sorted positions (as fractions of the edge) where the + /// edge meets the border of this polygon. + /// + private void CollectBorderIntersections(LineSegment2D edge, List parameters) + { + parameters.Clear(); + parameters.Add(0); + parameters.Add(1); + var lengthSquared = edge.Direction.LengthSquared; + if (lengthSquared == 0) + return; + + for (int i = 0, j = _points.Length - 1; i < _points.Length; j = i++) + { + var intersection = edge.Intersect(new LineSegment2D(_points[j], _points[i])); + switch (intersection.Kind) + { + case IntersectionKind.Point: + parameters.Add(Parameter(intersection.Point.GetValueOrDefault())); + break; + case IntersectionKind.Overlap: + parameters.Add(Parameter(intersection.Segment.GetValueOrDefault().Start)); + parameters.Add(Parameter(intersection.Segment.GetValueOrDefault().End)); + break; + } + } + + parameters.Sort(); + return; + + double Parameter(Point2D point) => Math.Clamp((point - edge.Start).Dot(edge.Direction) / lengthSquared, 0, 1); + } +} diff --git a/src/SharpMath/Geometry/ProjectionData.cs b/src/SharpMath/Geometry/ProjectionData.cs new file mode 100644 index 0000000..eee7161 --- /dev/null +++ b/src/SharpMath/Geometry/ProjectionData.cs @@ -0,0 +1,121 @@ +namespace SharpMath.Geometry; + +/// +/// Describes a perspective projection onto a viewport. +/// +public readonly struct ProjectionData : IEquatable +{ + /// + /// Initializes a new instance of the struct. + /// + /// The width of the viewport, e.g. in pixels. Must not be negative. + /// The height of the viewport, e.g. in pixels. Must not be negative. + /// The aspect ratio (width / height) used for the projection. Must be positive. + /// The vertical field of view in radians, in (0, π). + /// The distance to the near plane. Must be positive. + /// The distance to the far plane. Must be greater than . + /// An argument is out of range. + public ProjectionData(double width, double height, double aspectRatio, double fieldOfView, double nearPlane, + double farPlane) + { + if (!(width >= 0) || double.IsPositiveInfinity(width)) + throw new ArgumentOutOfRangeException(nameof(width), width, "The width must be a finite, non-negative number."); + if (!(height >= 0) || double.IsPositiveInfinity(height)) + throw new ArgumentOutOfRangeException(nameof(height), height, + "The height must be a finite, non-negative number."); + Validate(fieldOfView, aspectRatio, nearPlane, farPlane); + + Width = width; + Height = height; + AspectRatio = aspectRatio; + FieldOfView = fieldOfView; + NearPlane = nearPlane; + FarPlane = farPlane; + } + + /// + /// Gets the default projection data: a 100x100 viewport, an aspect ratio of 16:9, a field of view of 60° and the + /// planes 1 and 100. + /// + public static ProjectionData Default => new(100, 100, 16.0 / 9.0, Math.PI / 3, 1, 100); + + /// + /// Gets the width of the viewport. + /// + public double Width { get; } + + /// + /// Gets the height of the viewport. + /// + public double Height { get; } + + /// + /// Gets the aspect ratio (width / height) used for the projection. + /// + public double AspectRatio { get; } + + /// + /// Gets the vertical field of view in radians. + /// + public double FieldOfView { get; } + + /// + /// Gets the distance to the near plane. + /// + public double NearPlane { get; } + + /// + /// Gets the distance to the far plane. + /// + public double FarPlane { get; } + + /// + /// Gets the distance between the near and the far plane. + /// + public double Depth => FarPlane - NearPlane; + + /// + public bool Equals(ProjectionData other) => + Width.Equals(other.Width) && Height.Equals(other.Height) && AspectRatio.Equals(other.AspectRatio) && + FieldOfView.Equals(other.FieldOfView) && NearPlane.Equals(other.NearPlane) && FarPlane.Equals(other.FarPlane); + + /// + public override bool Equals(object? obj) => obj is ProjectionData other && Equals(other); + + /// + public override int GetHashCode() => HashCode.Combine(Width, Height, AspectRatio, FieldOfView, NearPlane, FarPlane); + + /// + /// Determines whether two instances are equal. + /// + /// The first instance. + /// The second instance. + /// if all values are equal; otherwise, . + public static bool operator ==(ProjectionData left, ProjectionData right) => + left.Width == right.Width && left.Height == right.Height && left.AspectRatio == right.AspectRatio && + left.FieldOfView == right.FieldOfView && left.NearPlane == right.NearPlane && left.FarPlane == right.FarPlane; + + /// + /// Determines whether two instances differ. + /// + /// The first instance. + /// The second instance. + /// if any value differs; otherwise, . + public static bool operator !=(ProjectionData left, ProjectionData right) => !(left == right); + + internal static void Validate(double fieldOfView, double aspectRatio, double nearPlane, double farPlane) + { + if (!(fieldOfView > 0 && fieldOfView < Math.PI)) + throw new ArgumentOutOfRangeException(nameof(fieldOfView), fieldOfView, + "The field of view must be greater than 0 and less than π (180°)."); + if (!(aspectRatio > 0) || double.IsPositiveInfinity(aspectRatio)) + throw new ArgumentOutOfRangeException(nameof(aspectRatio), aspectRatio, + "The aspect ratio must be a finite, positive number."); + if (!(nearPlane > 0) || double.IsPositiveInfinity(nearPlane)) + throw new ArgumentOutOfRangeException(nameof(nearPlane), nearPlane, + "The near plane must be a finite, positive distance."); + if (!(farPlane > nearPlane) || double.IsPositiveInfinity(farPlane)) + throw new ArgumentOutOfRangeException(nameof(farPlane), farPlane, + "The far plane must be finite and farther away than the near plane."); + } +} diff --git a/src/SharpMath/Geometry/SegmentIntersection2D.cs b/src/SharpMath/Geometry/SegmentIntersection2D.cs new file mode 100644 index 0000000..d0975e1 --- /dev/null +++ b/src/SharpMath/Geometry/SegmentIntersection2D.cs @@ -0,0 +1,12 @@ +namespace SharpMath.Geometry; + +/// +/// The result of intersecting a with another segment or a line. +/// +/// +/// How the objects intersect: , or +/// . +/// +/// The intersection point if is ; otherwise, . +/// The overlapping segment if is ; otherwise, . +public readonly record struct SegmentIntersection2D(IntersectionKind Kind, Point2D? Point, LineSegment2D? Segment); diff --git a/src/SharpMath/Geometry/SegmentMath.cs b/src/SharpMath/Geometry/SegmentMath.cs new file mode 100644 index 0000000..5e6bc14 --- /dev/null +++ b/src/SharpMath/Geometry/SegmentMath.cs @@ -0,0 +1,37 @@ +namespace SharpMath.Geometry; + +/// +/// Shared helpers for segments and polygon edges. +/// +internal static class SegmentMath +{ + /// + /// The tolerance for distances between points with coordinates up to . + /// + public static double DistanceTolerance(double scale) => + Math.Max(Tolerance.DefaultAbsolute, Tolerance.DefaultRelative * scale); + + public static double CoordinateScale(Point2D point) => Math.Max(Math.Abs(point.X), Math.Abs(point.Y)); + + /// + /// Determines whether a point lies within of the segment from + /// to , without square roots on the common path. + /// + public static bool IsOnSegment(Point2D point, Point2D a, Point2D b, double tolerance) + { + var edge = b - a; + var offset = point - a; + var lengthSquared = edge.LengthSquared; + var toleranceSquared = tolerance * tolerance; + if (lengthSquared == 0) + return offset.LengthSquared <= toleranceSquared; + + // The distance to the line is |cross| / |edge|; the projection must fall within the segment (± tolerance). + var cross = edge.Cross(offset); + if (cross * cross > toleranceSquared * lengthSquared) + return false; + var dot = edge.Dot(offset); + var slack = tolerance * Math.Sqrt(lengthSquared); + return dot >= -slack && dot <= lengthSquared + slack; + } +} diff --git a/src/SharpMath/Geometry/Vector2.cs b/src/SharpMath/Geometry/Vector2.cs new file mode 100644 index 0000000..26a2533 --- /dev/null +++ b/src/SharpMath/Geometry/Vector2.cs @@ -0,0 +1,408 @@ +using System.Globalization; + +namespace SharpMath.Geometry; + +/// +/// Represents an immutable two-dimensional vector. +/// +/// +/// Equality (, ==) is exact. Use +/// for tolerance-based comparisons. +/// +public readonly struct Vector2 : IVector, IEquatable +{ + /// + /// Initializes a new instance of the struct. + /// + /// The X component. + /// The Y component. + public Vector2(double x, double y) + { + X = x; + Y = y; + } + + /// + /// Initializes a new instance of the struct with the position vector of a point. + /// + /// The point. + public Vector2(Point2D point) : this(point.X, point.Y) + { + } + + /// + /// Gets the X component. + /// + public double X { get; init; } + + /// + /// Gets the Y component. + /// + public double Y { get; init; } + + /// + /// Gets the vector (0, 0). + /// + public static Vector2 Zero => default; + + /// + /// Gets the vector (1, 1). + /// + public static Vector2 One => new(1, 1); + + /// + /// Gets the unit vector (1, 0). + /// + public static Vector2 UnitX => new(1, 0); + + /// + /// Gets the unit vector (0, 1). + /// + public static Vector2 UnitY => new(0, 1); + + /// + /// Gets the vector (0, 1). + /// + public static Vector2 Up => new(0, 1); + + /// + /// Gets the vector (0, −1). + /// + public static Vector2 Down => new(0, -1); + + /// + /// Gets the vector (−1, 0). + /// + public static Vector2 Left => new(-1, 0); + + /// + /// Gets the vector (1, 0). + /// + public static Vector2 Right => new(1, 0); + + /// + public int Dimension => 2; + + /// + public double this[int index] => index switch + { + 0 => X, + 1 => Y, + _ => throw new ArgumentOutOfRangeException(nameof(index), index, "The index must be 0 or 1.") + }; + + /// + public double Length => Math.Sqrt(LengthSquared); + + /// + public double LengthSquared => X * X + Y * Y; + + /// + public bool IsZero => Tolerance.IsZero(X) && Tolerance.IsZero(Y); + + /// + public bool IsNormalized => Tolerance.AreEqual(LengthSquared, 1); + + /// + /// Gets the vector rotated by 90° counterclockwise, i.e. (−Y, X). + /// + public Vector2 Perpendicular => new(-Y, X); + + /// + /// Returns the vector with the same direction and a length of 1. + /// + /// The normalized vector. + /// The vector is the zero vector. + public Vector2 Normalize() + { + // Scaling by the largest component first avoids underflow and overflow of the squared length. + var scale = Math.Max(Math.Abs(X), Math.Abs(Y)); + if (scale == 0) + throw new InvalidOperationException("The zero vector cannot be normalized."); + var scaled = new Vector2(X / scale, Y / scale); + var length = scaled.Length; + return new Vector2(scaled.X / length, scaled.Y / length); + } + + /// + /// Calculates the dot product with another vector. + /// + /// The other vector. + /// The dot product. + public double Dot(Vector2 other) => X * other.X + Y * other.Y; + + /// + /// Calculates the dot product of two vectors. + /// + /// The first vector. + /// The second vector. + /// The dot product. + public static double Dot(Vector2 left, Vector2 right) => left.Dot(right); + + /// + /// Calculates the two-dimensional cross product (the Z component of the three-dimensional cross product) with another + /// vector. + /// + /// The other vector. + /// X · other.Y − Y · other.X. + public double Cross(Vector2 other) => X * other.Y - Y * other.X; + + /// + /// Calculates the two-dimensional cross product of two vectors. + /// + /// The first vector. + /// The second vector. + /// left.X · right.Y − left.Y · right.X. + public static double Cross(Vector2 left, Vector2 right) => left.Cross(right); + + /// + /// Calculates the angle between this vector and another vector. + /// + /// The other vector. + /// The angle in radians, in the range [0, π]. + /// One of the vectors is the zero vector. + public double Angle(Vector2 other) + { + if (IsExactlyZero || other.IsExactlyZero) + throw VectorMath.ZeroVectorAngle(); + var (u, v) = (Normalize(), other.Normalize()); + return VectorMath.Angle(Math.Abs(u.Cross(v)), u.Dot(v)); + } + + /// + /// Calculates the angle between two vectors. + /// + /// The first vector. + /// The second vector. + /// The angle in radians, in the range [0, π]. + /// One of the vectors is the zero vector. + public static double Angle(Vector2 left, Vector2 right) => left.Angle(right); + + /// + /// Calculates the distance between the tips of this vector and another vector. + /// + /// The other vector. + /// The distance. + public double Distance(Vector2 other) => (this - other).Length; + + /// + /// Calculates the distance between the tips of two vectors. + /// + /// The first vector. + /// The second vector. + /// The distance. + public static double Distance(Vector2 left, Vector2 right) => left.Distance(right); + + /// + /// Calculates the squared distance between the tips of this vector and another vector. + /// + /// The other vector. + /// The squared distance. + public double DistanceSquared(Vector2 other) => (this - other).LengthSquared; + + /// + /// Calculates the squared distance between the tips of two vectors. + /// + /// The first vector. + /// The second vector. + /// The squared distance. + public static double DistanceSquared(Vector2 left, Vector2 right) => left.DistanceSquared(right); + + /// + /// Calculates the area of the parallelogram spanned by this vector and another vector. + /// + /// The other vector. + /// The area. + public double ParallelogramArea(Vector2 other) => Math.Abs(Cross(other)); + + /// + /// Interpolates linearly between two vectors. The interpolant is clamped to [0, 1]. + /// + /// The vector returned for an interpolant of 0. + /// The vector returned for an interpolant of 1. + /// The interpolant. + /// The interpolated vector. + public static Vector2 Lerp(Vector2 source, Vector2 target, double amount) => + LerpUnclamped(source, target, Math.Clamp(amount, 0, 1)); + + /// + /// Interpolates or extrapolates linearly between two vectors. + /// + /// The vector returned for an interpolant of 0. + /// The vector returned for an interpolant of 1. + /// The interpolant; values outside of [0, 1] extrapolate. + /// The interpolated vector. + public static Vector2 LerpUnclamped(Vector2 source, Vector2 target, double amount) => + source + (target - source) * amount; + + /// + /// Moves a vector towards a target by at most the specified distance without overshooting it. + /// + /// The vector to move. + /// The target vector. + /// The maximum distance to move. + /// The moved vector, or if it is within reach. + public static Vector2 MoveTowards(Vector2 source, Vector2 target, double maxDistanceDelta) + { + var delta = target - source; + var distance = delta.Length; + if (distance <= maxDistanceDelta || distance == 0) + return target; + return source + delta / distance * maxDistanceDelta; + } + + /// + /// Determines whether this vector is parallel (or antiparallel) to another vector, i.e. whether the sine of the + /// angle between them is at most 1e-9, independent of their lengths. The zero vector is parallel to every vector. + /// + /// The other vector. + /// if the vectors are parallel; otherwise, . + public bool IsParallelTo(Vector2 other) + { + if (IsExactlyZero || other.IsExactlyZero) + return true; + var (u, v) = (Normalize(), other.Normalize()); + return VectorMath.IsNegligible(Math.Abs(u.Cross(v))); + } + + /// + /// Determines whether this vector is orthogonal to another vector, i.e. whether the cosine of the angle between + /// them is at most 1e-9, independent of their lengths. The zero vector is orthogonal to every vector. + /// + /// The other vector. + /// if the vectors are orthogonal; otherwise, . + public bool IsOrthogonalTo(Vector2 other) => + IsExactlyZero || other.IsExactlyZero || VectorMath.IsNegligible(Normalize().Dot(other.Normalize())); + + /// + /// Determines whether this vector and another vector are both normalized and orthogonal to each other. + /// + /// The other vector. + /// if the vectors are orthonormal; otherwise, . + public bool IsOrthonormalTo(Vector2 other) => IsNormalized && other.IsNormalized && IsOrthogonalTo(other); + + /// + /// Transforms a vector with a 2x2 matrix (matrix · vector). + /// + /// The vector. + /// The matrix. + /// The transformed vector. + public static Vector2 Transform(Vector2 vector, Matrix2x2 matrix) => matrix * vector; + + /// + /// Transforms a vector with a 3x3 matrix in homogeneous coordinates (matrix · (x, y, 1)), so translations + /// are applied. The result is divided by the resulting W component if it is not 1. + /// + /// The vector. + /// The matrix. + /// The transformed vector. + public static Vector2 Transform(Vector2 vector, Matrix3x3 matrix) => matrix * vector; + + /// + /// Converts the vector into the point it points to. + /// + /// The point (X, Y). + public Point2D ToPoint() => new(X, Y); + + /// + /// Determines whether this vector equals another vector within the default tolerances (component-wise). + /// + /// The other vector. + /// if the vectors are approximately equal; otherwise, . + public bool ApproximatelyEquals(Vector2 other) => + ApproximatelyEquals(other, Tolerance.DefaultAbsolute, Tolerance.DefaultRelative); + + /// + /// Determines whether this vector equals another vector within the specified tolerances (component-wise). + /// + /// The other vector. + /// The absolute tolerance. + /// The relative tolerance. + /// if the vectors are approximately equal; otherwise, . + public bool ApproximatelyEquals(Vector2 other, double absolute, double relative) => + Tolerance.AreEqual(X, other.X, absolute, relative) && Tolerance.AreEqual(Y, other.Y, absolute, relative); + + private bool IsExactlyZero => X == 0 && Y == 0; + + /// + /// Returns a LaTeX representation of the vector as a column vector. + /// + /// The LaTeX string. + public string ToLaTeXString() => VectorMath.ToLaTeX([X, Y]); + + /// + public override string ToString() => + string.Create(CultureInfo.InvariantCulture, $"Vector2 {{X: {X}, Y: {Y}}}"); + + /// + public bool Equals(Vector2 other) => X.Equals(other.X) && Y.Equals(other.Y); + + /// + public override bool Equals(object? obj) => obj is Vector2 other && Equals(other); + + /// + public override int GetHashCode() => HashCode.Combine(X, Y); + + /// + /// Adds two vectors. + /// + /// The first vector. + /// The second vector. + /// The sum. + public static Vector2 operator +(Vector2 left, Vector2 right) => new(left.X + right.X, left.Y + right.Y); + + /// + /// Subtracts a vector from another vector. + /// + /// The minuend. + /// The subtrahend. + /// The difference. + public static Vector2 operator -(Vector2 left, Vector2 right) => new(left.X - right.X, left.Y - right.Y); + + /// + /// Negates a vector. + /// + /// The vector. + /// The negated vector. + public static Vector2 operator -(Vector2 vector) => new(-vector.X, -vector.Y); + + /// + /// Multiplies a vector with a scalar. + /// + /// The vector. + /// The scalar. + /// The scaled vector. + public static Vector2 operator *(Vector2 vector, double scalar) => new(vector.X * scalar, vector.Y * scalar); + + /// + /// Multiplies a scalar with a vector. + /// + /// The scalar. + /// The vector. + /// The scaled vector. + public static Vector2 operator *(double scalar, Vector2 vector) => vector * scalar; + + /// + /// Divides a vector by a scalar. + /// + /// The vector. + /// The scalar. + /// The scaled vector. + public static Vector2 operator /(Vector2 vector, double scalar) => new(vector.X / scalar, vector.Y / scalar); + + /// + /// Determines whether two vectors are exactly equal (component-wise ==). + /// + /// The first vector. + /// The second vector. + /// if all components are equal; otherwise, . + public static bool operator ==(Vector2 left, Vector2 right) => left.X == right.X && left.Y == right.Y; + + /// + /// Determines whether two vectors differ in at least one component. + /// + /// The first vector. + /// The second vector. + /// if any component differs; otherwise, . + public static bool operator !=(Vector2 left, Vector2 right) => !(left == right); +} diff --git a/src/SharpMath/Geometry/Vector3.cs b/src/SharpMath/Geometry/Vector3.cs new file mode 100644 index 0000000..6bafe7a --- /dev/null +++ b/src/SharpMath/Geometry/Vector3.cs @@ -0,0 +1,466 @@ +using System.Globalization; + +namespace SharpMath.Geometry; + +/// +/// Represents an immutable three-dimensional vector. +/// +/// +/// The library uses a right-handed coordinate system with column vectors: points along −Z. +/// Equality (, ==) is exact. Use +/// for tolerance-based comparisons. +/// +public readonly struct Vector3 : IVector, IEquatable +{ + /// + /// Initializes a new instance of the struct. + /// + /// The X component. + /// The Y component. + /// The Z component. + public Vector3(double x, double y, double z) + { + X = x; + Y = y; + Z = z; + } + + /// + /// Initializes a new instance of the struct from a two-dimensional vector and a Z component. + /// + /// The X and Y components. + /// The Z component. + public Vector3(Vector2 xy, double z) : this(xy.X, xy.Y, z) + { + } + + /// + /// Initializes a new instance of the struct with the position vector of a point. + /// + /// The point. + public Vector3(Point3D point) : this(point.X, point.Y, point.Z) + { + } + + /// + /// Gets the X component. + /// + public double X { get; init; } + + /// + /// Gets the Y component. + /// + public double Y { get; init; } + + /// + /// Gets the Z component. + /// + public double Z { get; init; } + + /// + /// Gets the vector (0, 0, 0). + /// + public static Vector3 Zero => default; + + /// + /// Gets the vector (1, 1, 1). + /// + public static Vector3 One => new(1, 1, 1); + + /// + /// Gets the unit vector (1, 0, 0). + /// + public static Vector3 UnitX => new(1, 0, 0); + + /// + /// Gets the unit vector (0, 1, 0). + /// + public static Vector3 UnitY => new(0, 1, 0); + + /// + /// Gets the unit vector (0, 0, 1). + /// + public static Vector3 UnitZ => new(0, 0, 1); + + /// + /// Gets the vector (0, 1, 0). + /// + public static Vector3 Up => new(0, 1, 0); + + /// + /// Gets the vector (0, −1, 0). + /// + public static Vector3 Down => new(0, -1, 0); + + /// + /// Gets the vector (−1, 0, 0). + /// + public static Vector3 Left => new(-1, 0, 0); + + /// + /// Gets the vector (1, 0, 0). + /// + public static Vector3 Right => new(1, 0, 0); + + /// + /// Gets the vector (0, 0, −1), the viewing direction in the right-handed coordinate system. + /// + public static Vector3 Forward => new(0, 0, -1); + + /// + /// Gets the vector (0, 0, 1). + /// + public static Vector3 Back => new(0, 0, 1); + + /// + public int Dimension => 3; + + /// + public double this[int index] => index switch + { + 0 => X, + 1 => Y, + 2 => Z, + _ => throw new ArgumentOutOfRangeException(nameof(index), index, "The index must be between 0 and 2.") + }; + + /// + public double Length => Math.Sqrt(LengthSquared); + + /// + public double LengthSquared => X * X + Y * Y + Z * Z; + + /// + public bool IsZero => Tolerance.IsZero(X) && Tolerance.IsZero(Y) && Tolerance.IsZero(Z); + + /// + public bool IsNormalized => Tolerance.AreEqual(LengthSquared, 1); + + /// + /// Creates a vector from homogeneous coordinates by dividing X, Y and Z by W. + /// + /// The homogeneous vector. + /// The Cartesian vector. + /// The W component is zero (a point at infinity). + public static Vector3 FromHomogeneous(Vector4 vector) + { + if (vector.W == 0) + throw new ArgumentException("A homogeneous vector with W = 0 has no Cartesian representation.", nameof(vector)); + return new Vector3(vector.X / vector.W, vector.Y / vector.W, vector.Z / vector.W); + } + + /// + /// Returns the vector with the same direction and a length of 1. + /// + /// The normalized vector. + /// The vector is the zero vector. + public Vector3 Normalize() + { + // Scaling by the largest component first avoids underflow and overflow of the squared length. + var scale = Math.Max(Math.Max(Math.Abs(X), Math.Abs(Y)), Math.Abs(Z)); + if (scale == 0) + throw new InvalidOperationException("The zero vector cannot be normalized."); + var scaled = new Vector3(X / scale, Y / scale, Z / scale); + var length = scaled.Length; + return new Vector3(scaled.X / length, scaled.Y / length, scaled.Z / length); + } + + /// + /// Calculates the dot product with another vector. + /// + /// The other vector. + /// The dot product. + public double Dot(Vector3 other) => X * other.X + Y * other.Y + Z * other.Z; + + /// + /// Calculates the dot product of two vectors. + /// + /// The first vector. + /// The second vector. + /// The dot product. + public static double Dot(Vector3 left, Vector3 right) => left.Dot(right); + + /// + /// Calculates the cross product with another vector. + /// + /// The other vector. + /// The cross product this × other. + public Vector3 Cross(Vector3 other) => + new(Y * other.Z - Z * other.Y, Z * other.X - X * other.Z, X * other.Y - Y * other.X); + + /// + /// Calculates the cross product of two vectors. + /// + /// The first vector. + /// The second vector. + /// The cross product left × right. + public static Vector3 Cross(Vector3 left, Vector3 right) => left.Cross(right); + + /// + /// Calculates the scalar triple product first · (second × third), the signed volume of the parallelepiped + /// spanned by the three vectors. + /// + /// The first vector. + /// The second vector. + /// The third vector. + /// The scalar triple product. + public static double ScalarTripleProduct(Vector3 first, Vector3 second, Vector3 third) => + first.Dot(second.Cross(third)); + + /// + /// Calculates the angle between this vector and another vector. + /// + /// The other vector. + /// The angle in radians, in the range [0, π]. + /// One of the vectors is the zero vector. + public double Angle(Vector3 other) + { + if (IsExactlyZero || other.IsExactlyZero) + throw VectorMath.ZeroVectorAngle(); + var (u, v) = (Normalize(), other.Normalize()); + return VectorMath.Angle(u.Cross(v).Length, u.Dot(v)); + } + + /// + /// Calculates the angle between two vectors. + /// + /// The first vector. + /// The second vector. + /// The angle in radians, in the range [0, π]. + /// One of the vectors is the zero vector. + public static double Angle(Vector3 left, Vector3 right) => left.Angle(right); + + /// + /// Calculates the distance between the tips of this vector and another vector. + /// + /// The other vector. + /// The distance. + public double Distance(Vector3 other) => (this - other).Length; + + /// + /// Calculates the distance between the tips of two vectors. + /// + /// The first vector. + /// The second vector. + /// The distance. + public static double Distance(Vector3 left, Vector3 right) => left.Distance(right); + + /// + /// Calculates the squared distance between the tips of this vector and another vector. + /// + /// The other vector. + /// The squared distance. + public double DistanceSquared(Vector3 other) => (this - other).LengthSquared; + + /// + /// Calculates the squared distance between the tips of two vectors. + /// + /// The first vector. + /// The second vector. + /// The squared distance. + public static double DistanceSquared(Vector3 left, Vector3 right) => left.DistanceSquared(right); + + /// + /// Calculates the area of the parallelogram spanned by this vector and another vector. + /// + /// The other vector. + /// The area |this × other|. + public double ParallelogramArea(Vector3 other) => Cross(other).Length; + + /// + /// Interpolates linearly between two vectors. The interpolant is clamped to [0, 1]. + /// + /// The vector returned for an interpolant of 0. + /// The vector returned for an interpolant of 1. + /// The interpolant. + /// The interpolated vector. + public static Vector3 Lerp(Vector3 source, Vector3 target, double amount) => + LerpUnclamped(source, target, Math.Clamp(amount, 0, 1)); + + /// + /// Interpolates or extrapolates linearly between two vectors. + /// + /// The vector returned for an interpolant of 0. + /// The vector returned for an interpolant of 1. + /// The interpolant; values outside of [0, 1] extrapolate. + /// The interpolated vector. + public static Vector3 LerpUnclamped(Vector3 source, Vector3 target, double amount) => + source + (target - source) * amount; + + /// + /// Moves a vector towards a target by at most the specified distance without overshooting it. + /// + /// The vector to move. + /// The target vector. + /// The maximum distance to move. + /// The moved vector, or if it is within reach. + public static Vector3 MoveTowards(Vector3 source, Vector3 target, double maxDistanceDelta) + { + var delta = target - source; + var distance = delta.Length; + if (distance <= maxDistanceDelta || distance == 0) + return target; + return source + delta / distance * maxDistanceDelta; + } + + /// + /// Determines whether this vector is parallel (or antiparallel) to another vector, i.e. whether the sine of the + /// angle between them is at most 1e-9, independent of their lengths. The zero vector is parallel to every vector. + /// + /// The other vector. + /// if the vectors are parallel; otherwise, . + public bool IsParallelTo(Vector3 other) + { + if (IsExactlyZero || other.IsExactlyZero) + return true; + var (u, v) = (Normalize(), other.Normalize()); + return VectorMath.IsNegligible(u.Cross(v).Length); + } + + /// + /// Determines whether this vector is orthogonal to another vector, i.e. whether the cosine of the angle between + /// them is at most 1e-9, independent of their lengths. The zero vector is orthogonal to every vector. + /// + /// The other vector. + /// if the vectors are orthogonal; otherwise, . + public bool IsOrthogonalTo(Vector3 other) => + IsExactlyZero || other.IsExactlyZero || VectorMath.IsNegligible(Normalize().Dot(other.Normalize())); + + /// + /// Determines whether this vector and another vector are both normalized and orthogonal to each other. + /// + /// The other vector. + /// if the vectors are orthonormal; otherwise, . + public bool IsOrthonormalTo(Vector3 other) => IsNormalized && other.IsNormalized && IsOrthogonalTo(other); + + /// + /// Transforms a vector with a 3x3 matrix (matrix · vector). + /// + /// The vector. + /// The matrix. + /// The transformed vector. + public static Vector3 Transform(Vector3 vector, Matrix3x3 matrix) => matrix * vector; + + /// + /// Transforms a vector with a 4x4 matrix in homogeneous coordinates (matrix · (x, y, z, 1)), so + /// translations are applied. The result is divided by the resulting W component if it is not 1 (perspective divide). + /// + /// The vector. + /// The matrix. + /// The transformed vector. + public static Vector3 Transform(Vector3 vector, Matrix4x4 matrix) => matrix * vector; + + /// + /// Converts the vector into the point it points to. + /// + /// The point (X, Y, Z). + public Point3D ToPoint() => new(X, Y, Z); + + /// + /// Determines whether this vector equals another vector within the default tolerances (component-wise). + /// + /// The other vector. + /// if the vectors are approximately equal; otherwise, . + public bool ApproximatelyEquals(Vector3 other) => + ApproximatelyEquals(other, Tolerance.DefaultAbsolute, Tolerance.DefaultRelative); + + /// + /// Determines whether this vector equals another vector within the specified tolerances (component-wise). + /// + /// The other vector. + /// The absolute tolerance. + /// The relative tolerance. + /// if the vectors are approximately equal; otherwise, . + public bool ApproximatelyEquals(Vector3 other, double absolute, double relative) => + Tolerance.AreEqual(X, other.X, absolute, relative) && Tolerance.AreEqual(Y, other.Y, absolute, relative) && + Tolerance.AreEqual(Z, other.Z, absolute, relative); + + private bool IsExactlyZero => X == 0 && Y == 0 && Z == 0; + + /// + /// Returns a LaTeX representation of the vector as a column vector. + /// + /// The LaTeX string. + public string ToLaTeXString() => VectorMath.ToLaTeX([X, Y, Z]); + + /// + public override string ToString() => + string.Create(CultureInfo.InvariantCulture, $"Vector3 {{X: {X}, Y: {Y}, Z: {Z}}}"); + + /// + public bool Equals(Vector3 other) => X.Equals(other.X) && Y.Equals(other.Y) && Z.Equals(other.Z); + + /// + public override bool Equals(object? obj) => obj is Vector3 other && Equals(other); + + /// + public override int GetHashCode() => HashCode.Combine(X, Y, Z); + + /// + /// Adds two vectors. + /// + /// The first vector. + /// The second vector. + /// The sum. + public static Vector3 operator +(Vector3 left, Vector3 right) => + new(left.X + right.X, left.Y + right.Y, left.Z + right.Z); + + /// + /// Subtracts a vector from another vector. + /// + /// The minuend. + /// The subtrahend. + /// The difference. + public static Vector3 operator -(Vector3 left, Vector3 right) => + new(left.X - right.X, left.Y - right.Y, left.Z - right.Z); + + /// + /// Negates a vector. + /// + /// The vector. + /// The negated vector. + public static Vector3 operator -(Vector3 vector) => new(-vector.X, -vector.Y, -vector.Z); + + /// + /// Multiplies a vector with a scalar. + /// + /// The vector. + /// The scalar. + /// The scaled vector. + public static Vector3 operator *(Vector3 vector, double scalar) => + new(vector.X * scalar, vector.Y * scalar, vector.Z * scalar); + + /// + /// Multiplies a scalar with a vector. + /// + /// The scalar. + /// The vector. + /// The scaled vector. + public static Vector3 operator *(double scalar, Vector3 vector) => vector * scalar; + + /// + /// Divides a vector by a scalar. + /// + /// The vector. + /// The scalar. + /// The scaled vector. + public static Vector3 operator /(Vector3 vector, double scalar) => + new(vector.X / scalar, vector.Y / scalar, vector.Z / scalar); + + /// + /// Determines whether two vectors are exactly equal (component-wise ==). + /// + /// The first vector. + /// The second vector. + /// if all components are equal; otherwise, . + public static bool operator ==(Vector3 left, Vector3 right) => + left.X == right.X && left.Y == right.Y && left.Z == right.Z; + + /// + /// Determines whether two vectors differ in at least one component. + /// + /// The first vector. + /// The second vector. + /// if any component differs; otherwise, . + public static bool operator !=(Vector3 left, Vector3 right) => !(left == right); +} diff --git a/src/SharpMath/Geometry/Vector4.cs b/src/SharpMath/Geometry/Vector4.cs new file mode 100644 index 0000000..55cab43 --- /dev/null +++ b/src/SharpMath/Geometry/Vector4.cs @@ -0,0 +1,391 @@ +using System.Globalization; + +namespace SharpMath.Geometry; + +/// +/// Represents an immutable four-dimensional vector, e.g. a point or direction in homogeneous coordinates. +/// +/// +/// Equality (, ==) is exact. Use +/// for tolerance-based comparisons. +/// +public readonly struct Vector4 : IVector, IEquatable +{ + /// + /// Initializes a new instance of the struct. + /// + /// The X component. + /// The Y component. + /// The Z component. + /// The W component. + public Vector4(double x, double y, double z, double w) + { + X = x; + Y = y; + Z = z; + W = w; + } + + /// + /// Initializes a new instance of the struct from a three-dimensional vector and a W component. + /// + /// The X, Y and Z components. + /// The W component (1 for points, 0 for directions). + public Vector4(Vector3 xyz, double w) : this(xyz.X, xyz.Y, xyz.Z, w) + { + } + + /// + /// Gets the X component. + /// + public double X { get; init; } + + /// + /// Gets the Y component. + /// + public double Y { get; init; } + + /// + /// Gets the Z component. + /// + public double Z { get; init; } + + /// + /// Gets the W component. + /// + public double W { get; init; } + + /// + /// Gets the vector (0, 0, 0, 0). + /// + public static Vector4 Zero => default; + + /// + /// Gets the vector (1, 1, 1, 1). + /// + public static Vector4 One => new(1, 1, 1, 1); + + /// + /// Gets the unit vector (1, 0, 0, 0). + /// + public static Vector4 UnitX => new(1, 0, 0, 0); + + /// + /// Gets the unit vector (0, 1, 0, 0). + /// + public static Vector4 UnitY => new(0, 1, 0, 0); + + /// + /// Gets the unit vector (0, 0, 1, 0). + /// + public static Vector4 UnitZ => new(0, 0, 1, 0); + + /// + /// Gets the unit vector (0, 0, 0, 1). + /// + public static Vector4 UnitW => new(0, 0, 0, 1); + + /// + public int Dimension => 4; + + /// + public double this[int index] => index switch + { + 0 => X, + 1 => Y, + 2 => Z, + 3 => W, + _ => throw new ArgumentOutOfRangeException(nameof(index), index, "The index must be between 0 and 3.") + }; + + /// + public double Length => Math.Sqrt(LengthSquared); + + /// + public double LengthSquared => X * X + Y * Y + Z * Z + W * W; + + /// + public bool IsZero => Tolerance.IsZero(X) && Tolerance.IsZero(Y) && Tolerance.IsZero(Z) && Tolerance.IsZero(W); + + /// + public bool IsNormalized => Tolerance.AreEqual(LengthSquared, 1); + + /// + /// Returns the vector with the same direction and a length of 1. + /// + /// The normalized vector. + /// The vector is the zero vector. + public Vector4 Normalize() + { + // Scaling by the largest component first avoids underflow and overflow of the squared length. + var scale = Math.Max(Math.Max(Math.Max(Math.Abs(X), Math.Abs(Y)), Math.Abs(Z)), Math.Abs(W)); + if (scale == 0) + throw new InvalidOperationException("The zero vector cannot be normalized."); + var scaled = new Vector4(X / scale, Y / scale, Z / scale, W / scale); + var length = scaled.Length; + return new Vector4(scaled.X / length, scaled.Y / length, scaled.Z / length, scaled.W / length); + } + + /// + /// Calculates the dot product with another vector. + /// + /// The other vector. + /// The dot product. + public double Dot(Vector4 other) => X * other.X + Y * other.Y + Z * other.Z + W * other.W; + + /// + /// Calculates the dot product of two vectors. + /// + /// The first vector. + /// The second vector. + /// The dot product. + public static double Dot(Vector4 left, Vector4 right) => left.Dot(right); + + /// + /// Calculates the angle between this vector and another vector. + /// + /// The other vector. + /// The angle in radians, in the range [0, π]. + /// One of the vectors is the zero vector. + public double Angle(Vector4 other) + { + if (IsExactlyZero || other.IsExactlyZero) + throw VectorMath.ZeroVectorAngle(); + var (u, v) = (Normalize(), other.Normalize()); + return VectorMath.Angle(u.SpannedArea(v), u.Dot(v)); + } + + /// + /// Calculates the angle between two vectors. + /// + /// The first vector. + /// The second vector. + /// The angle in radians, in the range [0, π]. + /// One of the vectors is the zero vector. + public static double Angle(Vector4 left, Vector4 right) => left.Angle(right); + + /// + /// Calculates the distance between the tips of this vector and another vector. + /// + /// The other vector. + /// The distance. + public double Distance(Vector4 other) => (this - other).Length; + + /// + /// Calculates the distance between the tips of two vectors. + /// + /// The first vector. + /// The second vector. + /// The distance. + public static double Distance(Vector4 left, Vector4 right) => left.Distance(right); + + /// + /// Calculates the squared distance between the tips of this vector and another vector. + /// + /// The other vector. + /// The squared distance. + public double DistanceSquared(Vector4 other) => (this - other).LengthSquared; + + /// + /// Calculates the squared distance between the tips of two vectors. + /// + /// The first vector. + /// The second vector. + /// The squared distance. + public static double DistanceSquared(Vector4 left, Vector4 right) => left.DistanceSquared(right); + + /// + /// Interpolates linearly between two vectors. The interpolant is clamped to [0, 1]. + /// + /// The vector returned for an interpolant of 0. + /// The vector returned for an interpolant of 1. + /// The interpolant. + /// The interpolated vector. + public static Vector4 Lerp(Vector4 source, Vector4 target, double amount) => + LerpUnclamped(source, target, Math.Clamp(amount, 0, 1)); + + /// + /// Interpolates or extrapolates linearly between two vectors. + /// + /// The vector returned for an interpolant of 0. + /// The vector returned for an interpolant of 1. + /// The interpolant; values outside of [0, 1] extrapolate. + /// The interpolated vector. + public static Vector4 LerpUnclamped(Vector4 source, Vector4 target, double amount) => + source + (target - source) * amount; + + /// + /// Moves a vector towards a target by at most the specified distance without overshooting it. + /// + /// The vector to move. + /// The target vector. + /// The maximum distance to move. + /// The moved vector, or if it is within reach. + public static Vector4 MoveTowards(Vector4 source, Vector4 target, double maxDistanceDelta) + { + var delta = target - source; + var distance = delta.Length; + if (distance <= maxDistanceDelta || distance == 0) + return target; + return source + delta / distance * maxDistanceDelta; + } + + /// + /// Determines whether this vector is parallel (or antiparallel) to another vector, i.e. whether the sine of the + /// angle between them is at most 1e-9, independent of their lengths. The zero vector is parallel to every vector. + /// + /// The other vector. + /// if the vectors are parallel; otherwise, . + public bool IsParallelTo(Vector4 other) + { + if (IsExactlyZero || other.IsExactlyZero) + return true; + var (u, v) = (Normalize(), other.Normalize()); + return VectorMath.IsNegligible(u.SpannedArea(v)); + } + + + /// + /// Determines whether this vector is orthogonal to another vector, i.e. whether the cosine of the angle between + /// them is at most 1e-9, independent of their lengths. The zero vector is orthogonal to every vector. + /// + /// The other vector. + /// if the vectors are orthogonal; otherwise, . + public bool IsOrthogonalTo(Vector4 other) => + IsExactlyZero || other.IsExactlyZero || VectorMath.IsNegligible(Normalize().Dot(other.Normalize())); + + /// + /// Determines whether this vector and another vector are both normalized and orthogonal to each other. + /// + /// The other vector. + /// if the vectors are orthonormal; otherwise, . + public bool IsOrthonormalTo(Vector4 other) => IsNormalized && other.IsNormalized && IsOrthogonalTo(other); + + /// + /// Transforms a vector with a 4x4 matrix (matrix · vector). + /// + /// The vector. + /// The matrix. + /// The transformed vector. + public static Vector4 Transform(Vector4 vector, Matrix4x4 matrix) => matrix * vector; + + /// + /// Determines whether this vector equals another vector within the default tolerances (component-wise). + /// + /// The other vector. + /// if the vectors are approximately equal; otherwise, . + public bool ApproximatelyEquals(Vector4 other) => + ApproximatelyEquals(other, Tolerance.DefaultAbsolute, Tolerance.DefaultRelative); + + /// + /// Determines whether this vector equals another vector within the specified tolerances (component-wise). + /// + /// The other vector. + /// The absolute tolerance. + /// The relative tolerance. + /// if the vectors are approximately equal; otherwise, . + public bool ApproximatelyEquals(Vector4 other, double absolute, double relative) => + Tolerance.AreEqual(X, other.X, absolute, relative) && Tolerance.AreEqual(Y, other.Y, absolute, relative) && + Tolerance.AreEqual(Z, other.Z, absolute, relative) && Tolerance.AreEqual(W, other.W, absolute, relative); + + private bool IsExactlyZero => X == 0 && Y == 0 && Z == 0 && W == 0; + + /// + /// The area of the parallelogram spanned with another vector, from the 2x2 minors (Lagrange identity), which + /// avoids the cancellation of |a|²|b|² − (a·b)². + /// + private double SpannedArea(Vector4 other) => + Math.Sqrt(Square(X * other.Y - Y * other.X) + Square(X * other.Z - Z * other.X) + + Square(X * other.W - W * other.X) + Square(Y * other.Z - Z * other.Y) + + Square(Y * other.W - W * other.Y) + Square(Z * other.W - W * other.Z)); + + private static double Square(double value) => value * value; + + /// + /// Returns a LaTeX representation of the vector as a column vector. + /// + /// The LaTeX string. + public string ToLaTeXString() => VectorMath.ToLaTeX([X, Y, Z, W]); + + /// + public override string ToString() => + string.Create(CultureInfo.InvariantCulture, $"Vector4 {{X: {X}, Y: {Y}, Z: {Z}, W: {W}}}"); + + /// + public bool Equals(Vector4 other) => + X.Equals(other.X) && Y.Equals(other.Y) && Z.Equals(other.Z) && W.Equals(other.W); + + /// + public override bool Equals(object? obj) => obj is Vector4 other && Equals(other); + + /// + public override int GetHashCode() => HashCode.Combine(X, Y, Z, W); + + /// + /// Adds two vectors. + /// + /// The first vector. + /// The second vector. + /// The sum. + public static Vector4 operator +(Vector4 left, Vector4 right) => + new(left.X + right.X, left.Y + right.Y, left.Z + right.Z, left.W + right.W); + + /// + /// Subtracts a vector from another vector. + /// + /// The minuend. + /// The subtrahend. + /// The difference. + public static Vector4 operator -(Vector4 left, Vector4 right) => + new(left.X - right.X, left.Y - right.Y, left.Z - right.Z, left.W - right.W); + + /// + /// Negates a vector. + /// + /// The vector. + /// The negated vector. + public static Vector4 operator -(Vector4 vector) => new(-vector.X, -vector.Y, -vector.Z, -vector.W); + + /// + /// Multiplies a vector with a scalar. + /// + /// The vector. + /// The scalar. + /// The scaled vector. + public static Vector4 operator *(Vector4 vector, double scalar) => + new(vector.X * scalar, vector.Y * scalar, vector.Z * scalar, vector.W * scalar); + + /// + /// Multiplies a scalar with a vector. + /// + /// The scalar. + /// The vector. + /// The scaled vector. + public static Vector4 operator *(double scalar, Vector4 vector) => vector * scalar; + + /// + /// Divides a vector by a scalar. + /// + /// The vector. + /// The scalar. + /// The scaled vector. + public static Vector4 operator /(Vector4 vector, double scalar) => + new(vector.X / scalar, vector.Y / scalar, vector.Z / scalar, vector.W / scalar); + + /// + /// Determines whether two vectors are exactly equal (component-wise ==). + /// + /// The first vector. + /// The second vector. + /// if all components are equal; otherwise, . + public static bool operator ==(Vector4 left, Vector4 right) => + left.X == right.X && left.Y == right.Y && left.Z == right.Z && left.W == right.W; + + /// + /// Determines whether two vectors differ in at least one component. + /// + /// The first vector. + /// The second vector. + /// if any component differs; otherwise, . + public static bool operator !=(Vector4 left, Vector4 right) => !(left == right); +} diff --git a/src/SharpMath/Geometry/VectorMath.cs b/src/SharpMath/Geometry/VectorMath.cs new file mode 100644 index 0000000..9b6aff5 --- /dev/null +++ b/src/SharpMath/Geometry/VectorMath.cs @@ -0,0 +1,38 @@ +using System.Globalization; +using System.Text; + +namespace SharpMath.Geometry; + +/// +/// Shared helpers for the vector types. +/// +internal static class VectorMath +{ + /// + /// Computes the angle between two unit vectors from the length of their cross product (or of the parallelogram + /// they span) and their dot product. Unlike acos, this is accurate for (nearly) parallel vectors. + /// + public static double Angle(double sine, double cosine) => Math.Atan2(sine, cosine); + + /// + /// Determines whether a sine or cosine of two unit vectors is negligible (|value| ≤ 1e-9), independent of the + /// lengths of the original vectors. + /// + public static bool IsNegligible(double unitValue) => Math.Abs(unitValue) <= Tolerance.DefaultRelative; + + public static InvalidOperationException ZeroVectorAngle() => + new("The angle to or from the zero vector is undefined."); + + public static string ToLaTeX(ReadOnlySpan components) + { + var builder = new StringBuilder(@"\left( \begin{array}{c} "); + for (var i = 0; i < components.Length; i++) + { + if (i > 0) + builder.Append(@" \\ "); + builder.Append(components[i].ToString(CultureInfo.InvariantCulture)); + } + + return builder.Append(@" \end{array} \right)").ToString(); + } +} diff --git a/src/SharpMath/SharpMath.csproj b/src/SharpMath/SharpMath.csproj new file mode 100644 index 0000000..8bba334 --- /dev/null +++ b/src/SharpMath/SharpMath.csproj @@ -0,0 +1,7 @@ + + + + A .NET math library: vectors, matrices, lines, polygons, linear equation systems and an expression parser. + + + diff --git a/src/SharpMath/Tolerance.cs b/src/SharpMath/Tolerance.cs new file mode 100644 index 0000000..e73fbc5 --- /dev/null +++ b/src/SharpMath/Tolerance.cs @@ -0,0 +1,95 @@ +namespace SharpMath; + +/// +/// Provides tolerance-based comparisons of floating-point numbers. +/// +/// +/// Two numbers a and b are considered equal if +/// |a − b| ≤ max(absolute, relative · max(|a|, |b|)). The absolute part handles values close to zero, the +/// relative part scales with the magnitude of the compared values. is never equal to +/// anything. +/// +public static class Tolerance +{ + /// + /// The default absolute tolerance (1e-12). + /// + public const double DefaultAbsolute = 1e-12; + + /// + /// The default relative tolerance (1e-9). + /// + public const double DefaultRelative = 1e-9; + + /// + /// Determines whether two numbers are equal within the default tolerances. + /// + /// The first number. + /// The second number. + /// if the numbers are approximately equal; otherwise, . + public static bool AreEqual(double a, double b) => AreEqual(a, b, DefaultAbsolute, DefaultRelative); + + /// + /// Determines whether two numbers are equal within the specified tolerances. + /// + /// The first number. + /// The second number. + /// The absolute tolerance. Must not be negative. + /// The relative tolerance. Must not be negative. + /// if the numbers are approximately equal; otherwise, . + /// A tolerance is negative or NaN. + public static bool AreEqual(double a, double b, double absolute, double relative) + { + ValidateTolerances(absolute, relative); + + // Also covers infinities of the same sign. + if (a == b) + return true; + + var difference = Math.Abs(a - b); + if (!double.IsFinite(difference)) + return false; + + return difference <= Math.Max(absolute, relative * Math.Max(Math.Abs(a), Math.Abs(b))); + } + + /// + /// Determines whether a number is zero within the default absolute tolerance. + /// + /// The number. + /// if |value| ≤ ; otherwise, . + public static bool IsZero(double value) => Math.Abs(value) <= DefaultAbsolute; + + /// + /// Determines whether a number is zero within the specified absolute tolerance. + /// + /// The number. + /// The absolute tolerance. Must not be negative. + /// if |value| ≤ absolute; otherwise, . + /// is negative or NaN. + public static bool IsZero(double value, double absolute) + { + ValidateTolerances(absolute, 0); + return Math.Abs(value) <= absolute; + } + + /// + /// Determines whether a number is negligible compared to a reference magnitude, using the default tolerances. + /// + /// The number, e.g. a residual or a cross product. + /// The magnitude of the quantities was computed from. + /// + /// if |value| ≤ max(, + /// · |scale|); otherwise, . + /// + public static bool IsNegligible(double value, double scale) => + Math.Abs(value) <= Math.Max(DefaultAbsolute, DefaultRelative * Math.Abs(scale)); + + private static void ValidateTolerances(double absolute, double relative) + { + if (!(absolute >= 0)) + throw new ArgumentOutOfRangeException(nameof(absolute), absolute, "The tolerance must not be negative."); + if (!(relative >= 0)) + throw new ArgumentOutOfRangeException(nameof(relative), relative, "The tolerance must not be negative."); + } +} diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props new file mode 100644 index 0000000..00e1d0f --- /dev/null +++ b/tests/Directory.Build.props @@ -0,0 +1,19 @@ + + + + + Exe + true + + true + + $(NoWarn);CA1707;CA1305;CA1307;CA1310;CA1852;CA1861;CS1591 + + + + + + + + + diff --git a/tests/SharpMath.Canvas.Avalonia.Tests/Canvas2DViewTests.cs b/tests/SharpMath.Canvas.Avalonia.Tests/Canvas2DViewTests.cs new file mode 100644 index 0000000..fe46656 --- /dev/null +++ b/tests/SharpMath.Canvas.Avalonia.Tests/Canvas2DViewTests.cs @@ -0,0 +1,106 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Headless; +using Avalonia.Headless.XUnit; +using Avalonia.Input; +using System.Runtime.InteropServices; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using SharpMath.Geometry; + +namespace SharpMath.Canvas.Avalonia.Tests; + +public class Canvas2DViewTests +{ + private static (Window Window, Canvas2DView View) Show(int width = 200, int height = 100) + { + var view = new Canvas2DView(); + var window = new Window { Width = width, Height = height, Content = view }; + window.Show(); + return (window, view); + } + + private static Func Pixels(Bitmap frame) + { + var size = frame.PixelSize; + var bytes = new byte[size.Width * size.Height * 4]; + var handle = GCHandle.Alloc(bytes, GCHandleType.Pinned); + try + { + frame.CopyPixels(new PixelRect(size), handle.AddrOfPinnedObject(), bytes.Length, size.Width * 4); + } + finally + { + handle.Free(); + } + + var bgra = frame.Format == PixelFormat.Bgra8888; + return (x, y) => + { + var i = (y * size.Width + x) * 4; + return bgra ? (bytes[i + 2], bytes[i + 1], bytes[i]) : (bytes[i], bytes[i + 1], bytes[i + 2]); + }; + } + + [AvaloniaFact] + public void Render_DrawsTheCanvasWithSkia() + { + var (window, view) = Show(); + view.Canvas.AddPoint(Point2D.Origin, new ItemStyle(new Stroke(Color.Black), Color.Black, 4)); + using var frame = window.CaptureRenderedFrame(); + Assert.NotNull(frame); + var pixel = Pixels(frame); + + Assert.Equal(((byte) 0, (byte) 0, (byte) 0), pixel(100, 50)); + Assert.Equal(((byte) 255, (byte) 255, (byte) 255), pixel(10, 80)); + window.Close(); + } + + [AvaloniaFact] + public void MouseInput_PansAndZooms() + { + var (window, view) = Show(); + window.CaptureRenderedFrame(); + Assert.Equal(new CanvasPoint(100, 50), view.Canvas.Viewport.Origin); + + window.MouseDown(new Point(10, 10), MouseButton.Left); + window.MouseMove(new Point(30, 20)); + window.MouseUp(new Point(30, 20), MouseButton.Left); + Assert.Equal(new CanvasPoint(120, 60), view.Canvas.Viewport.Origin); + + window.MouseWheel(new Point(120, 60), new Vector(0, 1)); + Assert.Equal(25, view.Canvas.Viewport.ScaleX, 9); + Assert.Equal(new CanvasPoint(120, 60), view.Canvas.Viewport.Origin); + + window.MouseDown(new Point(50, 50), MouseButton.Right); + window.MouseMove(new Point(60, 60)); + window.MouseUp(new Point(60, 60), MouseButton.Right); + Assert.Equal(new CanvasPoint(120, 60), view.Canvas.Viewport.Origin); + window.Close(); + } + + [AvaloniaFact] + public void Properties_ForwardToTheCanvas() + { + var view = new Canvas2DView { ShowTrackingLines = true, ShowValueIndicator = true }; + Assert.True(view.Canvas.ShowTrackingLines); + Assert.True(view.Canvas.ShowValueIndicator); + view.ShowTrackingLines = false; + Assert.False(view.Canvas.ShowTrackingLines); + view.ShowValueIndicator = false; + Assert.False(view.Canvas.ShowValueIndicator); + } + + [AvaloniaFact] + public void CanvasChanges_TriggerARedraw() + { + var (window, view) = Show(); + window.CaptureRenderedFrame(); + view.Canvas.AddPolygon(new Polygon(new Point2D(-10, -10), new Point2D(10, -10), new Point2D(10, 10)), + new ItemStyle(new Stroke(Color.Black), Color.FromRgb(255, 0, 0))); + using var frame = window.CaptureRenderedFrame(); + Assert.NotNull(frame); + Assert.Equal(((byte) 255, (byte) 0, (byte) 0), Pixels(frame)(140, 40)); + window.Close(); + } +} diff --git a/tests/SharpMath.Canvas.Avalonia.Tests/DisplayListTests.cs b/tests/SharpMath.Canvas.Avalonia.Tests/DisplayListTests.cs new file mode 100644 index 0000000..6a9ad20 --- /dev/null +++ b/tests/SharpMath.Canvas.Avalonia.Tests/DisplayListTests.cs @@ -0,0 +1,45 @@ +namespace SharpMath.Canvas.Avalonia.Tests; + +public class DisplayListTests +{ + private sealed class CountingContext : IDrawingContext + { + public List Calls { get; } = []; + public void Clear(Color color) => Calls.Add(nameof(Clear)); + public void DrawLine(CanvasPoint startPoint, CanvasPoint endPoint, Stroke stroke) => Calls.Add(nameof(DrawLine)); + public void DrawPolyline(ReadOnlySpan points, Stroke stroke) => Calls.Add(nameof(DrawPolyline)); + public void DrawPolygon(ReadOnlySpan points, Stroke? stroke, Color? fill) => Calls.Add(nameof(DrawPolygon)); + public void DrawRectangle(CanvasRect rect, Stroke? stroke, Color? fill) => Calls.Add(nameof(DrawRectangle)); + public void DrawEllipse(CanvasPoint center, double radiusX, double radiusY, Stroke? stroke, Color? fill) => + Calls.Add(nameof(DrawEllipse)); + public void DrawText(string text, CanvasPoint position, FontSpec font, Color color, TextAlignment alignment) => + Calls.Add(nameof(DrawText)); + public TextSize MeasureText(string text, FontSpec font) => new(text.Length, font.Size); + public void PushClip(CanvasRect rect) => Calls.Add(nameof(PushClip)); + public void PopClip() => Calls.Add(nameof(PopClip)); + } + + [Fact] + public void Replay_ReproducesAllCallsInOrder() + { + var list = new DisplayList(new CountingContext()); + var points = new[] { new CanvasPoint(0, 0), new CanvasPoint(1, 1), new CanvasPoint(2, 0) }; + list.Clear(Color.White); + list.PushClip(new CanvasRect(0, 0, 1, 1)); + list.DrawLine(default, default, new Stroke(Color.Black)); + list.DrawPolyline(points, new Stroke(Color.Black)); + list.DrawPolygon(points, null, Color.Black); + list.DrawRectangle(default, null, Color.Black); + list.DrawEllipse(default, 1, 1, null, Color.Black); + list.DrawText("a", default, new FontSpec("Segoe UI", 10), Color.Black, TextAlignment.Left); + list.PopClip(); + points[0] = new CanvasPoint(99, 99); + + Assert.Equal(9, list.Count); + Assert.Equal(new TextSize(3, 10), list.MeasureText("abc", new FontSpec("Segoe UI", 10))); + var context = new CountingContext(); + list.Replay(context); + Assert.Equal(["Clear", "PushClip", "DrawLine", "DrawPolyline", "DrawPolygon", "DrawRectangle", "DrawEllipse", + "DrawText", "PopClip"], context.Calls); + } +} diff --git a/tests/SharpMath.Canvas.Avalonia.Tests/SharpMath.Canvas.Avalonia.Tests.csproj b/tests/SharpMath.Canvas.Avalonia.Tests/SharpMath.Canvas.Avalonia.Tests.csproj new file mode 100644 index 0000000..8da3a15 --- /dev/null +++ b/tests/SharpMath.Canvas.Avalonia.Tests/SharpMath.Canvas.Avalonia.Tests.csproj @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + diff --git a/tests/SharpMath.Canvas.Avalonia.Tests/TestApplication.cs b/tests/SharpMath.Canvas.Avalonia.Tests/TestApplication.cs new file mode 100644 index 0000000..1c85aa1 --- /dev/null +++ b/tests/SharpMath.Canvas.Avalonia.Tests/TestApplication.cs @@ -0,0 +1,18 @@ +using Avalonia; +using Avalonia.Headless; +using Avalonia.Themes.Fluent; +using SharpMath.Canvas.Avalonia.Tests; + +[assembly: AvaloniaTestApplication(typeof(TestApplication))] + +namespace SharpMath.Canvas.Avalonia.Tests; + +public sealed class TestApplication : Application +{ + public override void Initialize() => Styles.Add(new FluentTheme()); + + public static AppBuilder BuildAvaloniaApp() => + AppBuilder.Configure() + .UseSkia() + .UseHeadless(new AvaloniaHeadlessPlatformOptions { UseHeadlessDrawing = false }); +} diff --git a/tests/SharpMath.Canvas.Skia.Tests/SharpMath.Canvas.Skia.Tests.csproj b/tests/SharpMath.Canvas.Skia.Tests/SharpMath.Canvas.Skia.Tests.csproj new file mode 100644 index 0000000..5ee22e1 --- /dev/null +++ b/tests/SharpMath.Canvas.Skia.Tests/SharpMath.Canvas.Skia.Tests.csproj @@ -0,0 +1,7 @@ + + + + + + + diff --git a/tests/SharpMath.Canvas.Skia.Tests/SkiaRenderingTests.cs b/tests/SharpMath.Canvas.Skia.Tests/SkiaRenderingTests.cs new file mode 100644 index 0000000..b8dd773 --- /dev/null +++ b/tests/SharpMath.Canvas.Skia.Tests/SkiaRenderingTests.cs @@ -0,0 +1,112 @@ +using SharpMath.Geometry; +using SkiaSharp; + +namespace SharpMath.Canvas.Skia.Tests; + +public class SkiaRenderingTests +{ + private static SKColor ToSkia(Color color) => new(color.R, color.G, color.B, color.A); + + private static SKBitmap Render(Canvas2D canvas, int width = 400, int height = 300) + { + using var image = canvas.RenderImage(width, height); + return SKBitmap.FromImage(image); + } + + [Fact] + public void RenderImage_DrawsBackgroundAxesAndGrid() + { + using var bitmap = Render(new Canvas2D()); + Assert.Equal(400, bitmap.Width); + Assert.Equal(300, bitmap.Height); + + // Probes away from text: background between grid lines, the axes and a grid line. + Assert.Equal(SKColors.White, bitmap.GetPixel(10, 280)); + Assert.Equal(ToSkia(CanvasStyle.Default.AxisStroke.Color), bitmap.GetPixel(30, 150)); + Assert.Equal(ToSkia(CanvasStyle.Default.AxisStroke.Color), bitmap.GetPixel(200, 30)); + // Grid lines are snapped to pixel centers, so they are crisp and exactly the grid color. + Assert.Equal(ToSkia(CanvasStyle.Default.MinorGridStroke.Color), bitmap.GetPixel(40, 280)); + } + + [Fact] + public void Items_AreDrawnWithTheirColors() + { + var canvas = new Canvas2D(); + var red = Color.FromRgb(255, 0, 0); + canvas.AddPolygon(new Polygon(new Point2D(2, 2), new Point2D(6, 2), new Point2D(6, 6), new Point2D(2, 6)), + new ItemStyle(new Stroke(red, 2), red)); + canvas.AddPoint(new Point2D(-5, -5), new ItemStyle(new Stroke(Color.Black), Color.Black, 5)); + using var bitmap = Render(canvas); + + Assert.Equal(SKColors.Red, bitmap.GetPixel(285, 75)); + Assert.Equal(SKColors.Black, bitmap.GetPixel(100, 250)); + } + + [Fact] + public void Text_IsMeasuredAndDrawn() + { + using var surface = SKSurface.Create(new SKImageInfo(100, 40)); + using var context = new SkiaDrawingContext(surface.Canvas); + var font = new FontSpec("Segoe UI", 20); + context.Clear(Color.White); + var size = context.MeasureText("1234", font); + Assert.InRange(size.Width, 10, 100); + Assert.InRange(size.Height, 15, 40); + context.DrawText("1234", new CanvasPoint(50, 5), font, Color.Black, TextAlignment.Center); + context.DrawText("1", new CanvasPoint(0, 5), font, Color.Black, TextAlignment.Left); + context.DrawText("1", new CanvasPoint(100, 5), font, Color.Black, TextAlignment.Right); + using var image = surface.Snapshot(); + using var bitmap = SKBitmap.FromImage(image); + var dark = 0; + for (var x = 0; x < 100; x++) + for (var y = 0; y < 40; y++) + if (bitmap.GetPixel(x, y).Red < 128) + dark++; + Assert.True(dark > 20, $"Expected the text to cover some pixels, got {dark}."); + } + + [Fact] + public void Context_DrawsEveryPrimitive() + { + using var surface = SKSurface.Create(new SKImageInfo(50, 50)); + using var context = new SkiaDrawingContext(surface.Canvas); + var blue = new Stroke(Color.FromRgb(0, 0, 255), 3, DashStyle.Dashed); + context.Clear(Color.White); + context.PushClip(new CanvasRect(0, 0, 40, 40)); + context.DrawRectangle(new CanvasRect(5, 5, 10, 10), blue, Color.FromRgb(0, 255, 0)); + context.DrawEllipse(new CanvasPoint(30, 30), 5, 5, new Stroke(Color.Black, 1, DashStyle.Dotted), Color.Black); + context.DrawPolyline([new CanvasPoint(0, 45), new CanvasPoint(50, 45)], blue); + context.DrawPolyline([new CanvasPoint(0, 0)], blue); + context.DrawPolygon([new CanvasPoint(0, 0), new CanvasPoint(1, 1)], blue, null); + context.PopClip(); + context.DrawLine(new CanvasPoint(45, 0), new CanvasPoint(45, 50), new Stroke(Color.Black, 2)); + using var image = surface.Snapshot(); + using var bitmap = SKBitmap.FromImage(image); + + Assert.Equal(new SKColor(0, 255, 0), bitmap.GetPixel(10, 10)); + Assert.Equal(SKColors.Black, bitmap.GetPixel(30, 30)); + Assert.Equal(SKColors.White, bitmap.GetPixel(25, 45)); + Assert.Equal(SKColors.Black, bitmap.GetPixel(45, 25)); + } + + [Fact] + public void RenderPng_WritesAPngFile() + { + using var stream = new MemoryStream(); + new Canvas2D().RenderPng(stream, 64, 48); + var bytes = stream.ToArray(); + Assert.Equal([0x89, (byte) 'P', (byte) 'N', (byte) 'G'], bytes[..4]); + using var decoded = SKBitmap.Decode(bytes); + Assert.Equal(64, decoded.Width); + } + + [Fact] + public void Validation() + { + var canvas = new Canvas2D(); + Assert.Throws(() => canvas.RenderImage(0, 1)); + Assert.Throws(() => canvas.RenderPng(null!, 1, 1)); + Assert.Throws(() => ((Canvas2D) null!).RenderImage(1, 1)); + Assert.Throws(() => new SkiaDrawingContext(null!)); + } +} diff --git a/tests/SharpMath.Canvas.Tests/Canvas2DTests.cs b/tests/SharpMath.Canvas.Tests/Canvas2DTests.cs new file mode 100644 index 0000000..0bc3b42 --- /dev/null +++ b/tests/SharpMath.Canvas.Tests/Canvas2DTests.cs @@ -0,0 +1,270 @@ +using SharpMath.Geometry; + +namespace SharpMath.Canvas.Tests; + +public class Canvas2DTests +{ + private static RecordingDrawingContext Render(Canvas2D canvas, double width = 400, double height = 300) + { + var context = new RecordingDrawingContext(); + canvas.Render(context, width, height); + return context; + } + + [Fact] + public void Render_DrawsBackgroundGridAxesAndLabels() + { + var canvas = new Canvas2D(); + var context = Render(canvas); + + Assert.Equal([Color.White], context.Clears); + Assert.Equal(0, context.ClipDepth); + Assert.Equal(1, context.MaxClipDepth); + + // 1-unit grid at 20 px per unit: 21 vertical and 15 horizontal lines (−10…10, −7…7), plus two axes. + var axisColor = CanvasStyle.Default.AxisStroke; + Assert.Equal(2, context.Lines.Count(line => line.Stroke == axisColor)); + Assert.Equal(21 + 15, context.Lines.Count(line => line.Stroke == CanvasStyle.Default.MinorGridStroke)); + + // Labels every 5 units; "0" only once; labels that would be cut off at the edges are skipped. + var labels = context.Texts.Select(text => text.Text).ToList(); + Assert.Equal(["-5", "5", "-5", "0", "5"], labels); + Assert.All(context.Texts, text => Assert.Equal(Color.Black, text.Color)); + } + + [Fact] + public void Labels_SitOnGridLines_AtEveryZoomLevel() + { + // Regression: at 16 px per unit the grid step was 2 but labels were placed every 5 units. + var canvas = new Canvas2D(); + canvas.Viewport.Resize(400, 300); + canvas.Viewport.SetScale(16, 16); + var context = Render(canvas); + var gridX = context.Lines.Where(line => line.Start.X == line.End.X).Select(line => Math.Floor(line.Start.X)).ToHashSet(); + var xLabels = context.Texts.Where(text => text.Alignment == TextAlignment.Center).ToList(); + Assert.NotEmpty(xLabels); + Assert.All(xLabels, label => Assert.Contains(Math.Floor(label.Position.X), gridX)); + Assert.Equal(["-10", "10"], xLabels.Select(label => label.Text)); + } + + [Fact] + public void Labels_ThatWouldBeCutOff_AreSkipped() + { + var canvas = new Canvas2D(); + canvas.Viewport.Resize(400, 300); + canvas.Viewport.SetOrigin(new CanvasPoint(200, 297)); + var context = Render(canvas); + Assert.All(context.Texts, text => + { + Assert.InRange(text.Position.Y, 0, 300); + Assert.InRange(text.Position.Y + 11 * 1.2, 0, 300); + }); + } + + [Fact] + public void Labels_StickToTheEdgeWhenTheAxisIsOffScreen() + { + var canvas = new Canvas2D(); + canvas.Viewport.Resize(400, 300); + canvas.Viewport.Pan(0, 1000); + var context = Render(canvas); + var xLabels = context.Texts.Where(text => text.Alignment == TextAlignment.Center).ToList(); + Assert.NotEmpty(xLabels); + Assert.All(xLabels, text => Assert.InRange(text.Position.Y, 0, 300)); + Assert.DoesNotContain(context.Lines, line => line.Stroke == CanvasStyle.Default.AxisStroke && + line.Start.Y == line.End.Y); + } + + [Fact] + public void Render_DrawsEveryItemType() + { + var canvas = new Canvas2D(); + canvas.AddFunction(x => x); + canvas.AddFunction("x^2"); + canvas.AddVector(new Vector2(4, 8)); + canvas.AddPoint(new Point2D(1, 1)); + canvas.AddLine(Line2D.FromGeneralForm(1, 0, -2)); + canvas.AddSegment(new LineSegment2D(new Point2D(0, 0), new Point2D(3, -3))); + canvas.AddPolygon(new Polygon(new Point2D(0, 0), new Point2D(2, 0), new Point2D(1, 2))); + var context = Render(canvas); + var style = CanvasStyle.Default; + + Assert.Equal(2, context.Polylines.Count(polyline => polyline.Stroke == style.FunctionStyle.Stroke)); + Assert.Contains(context.Lines, line => line.Stroke == style.VectorStyle.Stroke); + var arrowhead = Assert.Single(context.Polygons, polygon => polygon.Fill == style.VectorStyle.Fill); + Assert.Equal(new CanvasPoint(280, -10), arrowhead.Points[0]); + var point = Assert.Single(context.Ellipses); + Assert.Equal(new CanvasPoint(220, 130), point.Center); + var line = Assert.Single(context.Lines, line => line.Stroke == style.LineStyle.Stroke); + Assert.Equal(240, line.Start.X, 9); + Assert.Equal(240, line.End.X, 9); + Assert.Contains(context.Lines, line => line.Stroke == style.SegmentStyle.Stroke && + line.Start == new CanvasPoint(200, 150) && + line.End == new CanvasPoint(260, 210)); + Assert.Single(context.Polygons, polygon => polygon.Fill == style.PolygonStyle.Fill); + } + + [Fact] + public void ItemStyles_OverrideTheDefaults() + { + var canvas = new Canvas2D(); + var red = new ItemStyle(new Stroke(Color.FromRgb(255, 0, 0), 3, DashStyle.Dashed)); + canvas.AddFunction(x => x, red); + canvas.AddPoint(new Point2D(0, 0), red with { Fill = Color.Black, PointRadius = 5 }); + var context = Render(canvas); + Assert.Equal(red.Stroke, Assert.Single(context.Polylines).Stroke); + Assert.Equal(5, Assert.Single(context.Ellipses).RadiusX); + } + + [Fact] + public void FarAwayItems_AreClippedOrSkipped() + { + // Regression for issue #3: huge coordinates must never reach the renderer. + var canvas = new Canvas2D(); + canvas.AddVector(new Vector2(1e12, 1e12)); + canvas.AddSegment(new LineSegment2D(new Point2D(-1e15, 0), new Point2D(1e15, 1))); + canvas.AddPolygon(new Polygon(new Point2D(-1e9, -1e9), new Point2D(1e9, -1e9), new Point2D(0, 1e9))); + canvas.AddPoint(new Point2D(1e9, 1e9)); + canvas.AddFunction(x => Math.Exp(x * 100)); + canvas.AddLine(Line2D.FromGeneralForm(1, 0, -1e9)); + canvas.Viewport.Resize(400, 300); + for (var i = 0; i < 50; i++) + canvas.Wheel(new CanvasPoint(390, 5), 1); + + var context = Render(canvas); + Assert.All(context.AllPoints, point => + { + Assert.InRange(point.X, -5000, 9000); + Assert.InRange(point.Y, -4000, 7000); + }); + Assert.Empty(context.Ellipses); + Assert.DoesNotContain(context.Lines, line => line.Stroke == CanvasStyle.Default.LineStyle.Stroke); + } + + [Fact] + public void DegenerateItems_AreIgnored() + { + var canvas = new Canvas2D(); + canvas.AddVector(Vector2.Zero); + canvas.Items.Add(new LineItem(default)); + var context = Render(canvas); + Assert.Empty(context.Polygons); + Assert.DoesNotContain(context.Lines, line => line.Stroke == CanvasStyle.Default.VectorStyle.Stroke); + } + + [Fact] + public void InvalidSpacing_DoesNotHang() + { + // Regression: a grid size of 0 used to make the drawing loops spin forever. + var canvas = new Canvas2D { Style = CanvasStyle.Default with { MinGridSpacing = 0, MinLabelSpacing = double.NaN } }; + var context = Render(canvas, 100, 100); + Assert.InRange(context.Lines.Count, 2, 300); + } + + [Fact] + public void Input_PansZoomsAndTracksThePointer() + { + var canvas = new Canvas2D(); + canvas.Viewport.Resize(400, 300); + canvas.PointerPressed(new CanvasPoint(100, 100)); + canvas.PointerMoved(new CanvasPoint(110, 90)); + canvas.PointerReleased(new CanvasPoint(120, 80)); + Assert.Equal(new CanvasPoint(220, 130), canvas.Viewport.Origin); + canvas.PointerMoved(new CanvasPoint(0, 0)); + Assert.Equal(new CanvasPoint(220, 130), canvas.Viewport.Origin); + Assert.Equal(new CanvasPoint(0, 0), canvas.PointerPosition); + + canvas.Wheel(new CanvasPoint(220, 130), 2); + Assert.Equal(20 * 1.25 * 1.25, canvas.Viewport.ScaleX, 9); + Assert.Equal(new CanvasPoint(220, 130), canvas.Viewport.Origin); + canvas.Wheel(new CanvasPoint(0, 0), 0); + canvas.Wheel(new CanvasPoint(0, 0), double.NaN); + Assert.Equal(20 * 1.25 * 1.25, canvas.Viewport.ScaleX, 9); + + canvas.PointerExited(); + Assert.Null(canvas.PointerPosition); + } + + [Fact] + public void TrackingLinesAndValueIndicator() + { + var canvas = new Canvas2D { ShowTrackingLines = true, ShowValueIndicator = true }; + canvas.Viewport.Resize(400, 300); + canvas.PointerMoved(new CanvasPoint(250, 100)); + var context = Render(canvas); + + Assert.Equal(2, context.Lines.Count(line => line.Stroke == CanvasStyle.Default.TrackingStroke)); + var box = Assert.Single(context.Rectangles); + Assert.Equal(265, box.Rect.X); + Assert.Contains(context.Texts, text => text.Text == "2.5 | 2.5" && text.Color == CanvasStyle.Default.IndicatorText); + + canvas.PointerExited(); + var hidden = Render(canvas); + Assert.Empty(hidden.Rectangles); + } + + [Fact] + public void Changed_IsRaisedForRelevantChanges() + { + var canvas = new Canvas2D(); + var count = 0; + canvas.Changed += (_, _) => count++; + + Render(canvas); + Assert.Equal(0, count); + + canvas.AddPoint(Point2D.Origin); + Assert.Equal(1, count); + canvas.Items.Clear(); + Assert.Equal(2, count); + canvas.Style = canvas.Style with { Background = Color.Black }; + Assert.Equal(3, count); + canvas.Style = canvas.Style; + Assert.Equal(3, count); + canvas.Viewport.Pan(1, 1); + Assert.Equal(4, count); + + canvas.PointerMoved(new CanvasPoint(1, 1)); + Assert.Equal(4, count); + canvas.ShowTrackingLines = true; + canvas.ShowTrackingLines = true; + Assert.Equal(5, count); + canvas.PointerMoved(new CanvasPoint(2, 2)); + Assert.Equal(6, count); + canvas.PointerMoved(new CanvasPoint(2, 2)); + Assert.Equal(6, count); + canvas.ShowValueIndicator = true; + Assert.Equal(7, count); + canvas.PointerExited(); + Assert.Equal(8, count); + canvas.PointerExited(); + Assert.Equal(8, count); + Assert.Throws(() => canvas.Style = null!); + Assert.Throws(() => canvas.Render(null!, 1, 1)); + Assert.Throws(() => canvas.AddPolygon(null!)); + } + + [Fact] + public void FunctionItems_FromExpressions() + { + var item = FunctionItem.FromExpression("2x + 1"); + Assert.Equal(7, item.Function(3)); + Assert.Equal("2x + 1", item.Expression!.Text); + Assert.Throws(() => FunctionItem.FromExpression("x + y")); + Assert.Throws(() => new Canvas2D().AddFunction("x +")); + var context = new SharpMath.Expressions.ExpressionContext().AddFunction("sq", x => x * x); + var custom = new Canvas2D().AddFunction("sq(x) + 1", context: context); + Assert.Equal(10, custom.Function(3)); + } + + [Fact] + public void Colors() + { + Assert.Equal("#F57D00", Color.FromRgb(245, 125, 0).ToHex()); + Assert.Equal("#11223344", Color.FromArgb(0x44, 0x11, 0x22, 0x33).ToHex()); + Assert.Equal(0, Color.Transparent.A); + Assert.True(new CanvasRect(0, 0, 10, 10).Contains(new CanvasPoint(10, 10))); + Assert.Equal(10, new CanvasRect(5, 5, 5, 5).Right); + Assert.Equal(10, new CanvasRect(5, 5, 5, 5).Bottom); + } +} diff --git a/tests/SharpMath.Canvas.Tests/ClippingTests.cs b/tests/SharpMath.Canvas.Tests/ClippingTests.cs new file mode 100644 index 0000000..8b5858a --- /dev/null +++ b/tests/SharpMath.Canvas.Tests/ClippingTests.cs @@ -0,0 +1,51 @@ +namespace SharpMath.Canvas.Tests; + +public class ClippingTests +{ + private static readonly CanvasRect Bounds = new(0, 0, 100, 100); + + [Fact] + public void ClipSegment_ShortensSegmentsToTheRectangle() + { + var from = new CanvasPoint(-50, 50); + var to = new CanvasPoint(150, 50); + Assert.True(Clipping.ClipSegment(Bounds, ref from, ref to)); + Assert.Equal(new CanvasPoint(0, 50), from); + Assert.Equal(new CanvasPoint(100, 50), to); + + var inside = new CanvasPoint(10, 10); + var alsoInside = new CanvasPoint(20, 20); + Assert.True(Clipping.ClipSegment(Bounds, ref inside, ref alsoInside)); + Assert.Equal(new CanvasPoint(10, 10), inside); + + var outside = new CanvasPoint(-10, -10); + var stillOutside = new CanvasPoint(-5, 200); + Assert.False(Clipping.ClipSegment(Bounds, ref outside, ref stillOutside)); + + var huge = new CanvasPoint(50, -1e300); + var end = new CanvasPoint(50, 1e300); + Assert.True(Clipping.ClipSegment(Bounds, ref huge, ref end)); + Assert.Equal(new CanvasPoint(50, 0), huge); + + var nan = new CanvasPoint(double.NaN, 0); + var other = new CanvasPoint(1, 1); + Assert.False(Clipping.ClipSegment(Bounds, ref nan, ref other)); + + var vertical = new CanvasPoint(150, 0); + var verticalEnd = new CanvasPoint(150, 50); + Assert.False(Clipping.ClipSegment(Bounds, ref vertical, ref verticalEnd)); + } + + [Fact] + public void ClipPolygon_KeepsOnlyTheInsidePart() + { + var clipped = Clipping.ClipPolygon(Bounds, + [new CanvasPoint(-50, -50), new CanvasPoint(50, -50), new CanvasPoint(50, 50), new CanvasPoint(-50, 50)]); + Assert.Equal(4, clipped.Count); + Assert.All(clipped, point => Assert.True(Bounds.Contains(point))); + Assert.Contains(new CanvasPoint(50, 50), clipped); + + Assert.Empty(Clipping.ClipPolygon(Bounds, + [new CanvasPoint(200, 200), new CanvasPoint(300, 200), new CanvasPoint(300, 300)])); + } +} diff --git a/tests/SharpMath.Canvas.Tests/FunctionSamplerTests.cs b/tests/SharpMath.Canvas.Tests/FunctionSamplerTests.cs new file mode 100644 index 0000000..95312db --- /dev/null +++ b/tests/SharpMath.Canvas.Tests/FunctionSamplerTests.cs @@ -0,0 +1,86 @@ +namespace SharpMath.Canvas.Tests; + +public class FunctionSamplerTests +{ + private static Viewport CreateViewport() + { + var viewport = new Viewport(); + viewport.Resize(400, 300); + return viewport; + } + + private static CanvasRect GuardBand(Viewport viewport) => + new(-10 * viewport.Width, -10 * viewport.Height, 21 * viewport.Width, 21 * viewport.Height); + + private static List Sample(Func function, Viewport viewport) + { + var sampler = new FunctionSampler(); + sampler.Sample(function, viewport, GuardBand(viewport)); + return Enumerable.Range(0, sampler.PolylineCount).Select(i => sampler.GetPolyline(i).ToArray()).ToList(); + } + + [Fact] + public void ContinuousFunction_IsOnePolyline() + { + var viewport = CreateViewport(); + var polylines = Sample(x => x * x * x, viewport); + Assert.Single(polylines); + Assert.True(polylines[0].Length >= 401); + Assert.All(polylines[0], point => Assert.InRange(point.Y, -3000, 3300)); + } + + [Fact] + public void Poles_BreakTheGraph() + { + // Regression: the graph used to be connected across asymptotes. + var viewport = CreateViewport(); + var polylines = Sample(Math.Tan, viewport); + + // tan has poles at ±π/2, ±3π/2, ±5π/2 within x ∈ [−10, 10]. + Assert.Equal(7, polylines.Count); + foreach (var polyline in polylines) + for (var i = 1; i < polyline.Length; i++) + Assert.True(polyline[i].X >= polyline[i - 1].X); + } + + [Fact] + public void NonFiniteValuesAndArithmeticExceptions_LeaveGaps() + { + var viewport = CreateViewport(); + var sqrt = Sample(Math.Sqrt, viewport); + Assert.Single(sqrt); + Assert.All(sqrt[0], point => Assert.True(point.X >= 199)); + + var throwing = Sample(x => x < 0 ? throw new OverflowException() : 1, viewport); + Assert.Single(throwing); + + Assert.Empty(Sample(_ => double.NaN, viewport)); + var reciprocal = Sample(x => 1 / x, viewport); + Assert.Equal(2, reciprocal.Count); + } + + [Fact] + public void Buffers_AreReusedBetweenCalls() + { + var viewport = CreateViewport(); + var sampler = new FunctionSampler(); + sampler.Sample(Math.Tan, viewport, GuardBand(viewport)); + Assert.Equal(7, sampler.PolylineCount); + sampler.Sample(x => x, viewport, GuardBand(viewport)); + Assert.Equal(1, sampler.PolylineCount); + Assert.Equal(401, sampler.GetPolyline(0).Length); + + var before = GC.GetAllocatedBytesForCurrentThread(); + sampler.Sample(Math.Tan, viewport, GuardBand(viewport)); + sampler.Sample(x => x, viewport, GuardBand(viewport)); + Assert.True(GC.GetAllocatedBytesForCurrentThread() - before < 1024); + } + + [Fact] + public void SteepParts_AreRefined() + { + var viewport = CreateViewport(); + var polylines = Sample(x => Math.Exp(3 * x), viewport); + Assert.True(polylines.Sum(polyline => polyline.Length) > 401); + } +} diff --git a/tests/SharpMath.Canvas.Tests/Golden/empty.svg b/tests/SharpMath.Canvas.Tests/Golden/empty.svg new file mode 100644 index 0000000..72a8795 --- /dev/null +++ b/tests/SharpMath.Canvas.Tests/Golden/empty.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + +0 + + + + diff --git a/tests/SharpMath.Canvas.Tests/Golden/items.svg b/tests/SharpMath.Canvas.Tests/Golden/items.svg new file mode 100644 index 0000000..51fe0c8 --- /dev/null +++ b/tests/SharpMath.Canvas.Tests/Golden/items.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +-5 +5 +-5 +0 +5 + + + + + + + + + + + + + +5.0 | 4.5 + + diff --git a/tests/SharpMath.Canvas.Tests/RecordingDrawingContext.cs b/tests/SharpMath.Canvas.Tests/RecordingDrawingContext.cs new file mode 100644 index 0000000..a929636 --- /dev/null +++ b/tests/SharpMath.Canvas.Tests/RecordingDrawingContext.cs @@ -0,0 +1,52 @@ +namespace SharpMath.Canvas.Tests; + +/// +/// Records all drawing calls so tests can assert on them without a graphics library. +/// +internal sealed class RecordingDrawingContext : IDrawingContext +{ + public List Clears { get; } = []; + public List<(CanvasPoint Start, CanvasPoint End, Stroke Stroke)> Lines { get; } = []; + public List<(CanvasPoint[] Points, Stroke Stroke)> Polylines { get; } = []; + public List<(CanvasPoint[] Points, Stroke? Stroke, Color? Fill)> Polygons { get; } = []; + public List<(CanvasRect Rect, Stroke? Stroke, Color? Fill)> Rectangles { get; } = []; + public List<(CanvasPoint Center, double RadiusX, double RadiusY, Stroke? Stroke, Color? Fill)> Ellipses { get; } = []; + public List<(string Text, CanvasPoint Position, Color Color, TextAlignment Alignment)> Texts { get; } = []; + public int ClipDepth { get; private set; } + public int MaxClipDepth { get; private set; } + + public IEnumerable AllPoints => + Lines.SelectMany(line => new[] { line.Start, line.End }) + .Concat(Polylines.SelectMany(polyline => polyline.Points)) + .Concat(Polygons.SelectMany(polygon => polygon.Points)) + .Concat(Ellipses.Select(ellipse => ellipse.Center)) + .Concat(Texts.Select(text => text.Position)); + + public void Clear(Color color) => Clears.Add(color); + + public void DrawLine(CanvasPoint startPoint, CanvasPoint endPoint, Stroke stroke) => + Lines.Add((startPoint, endPoint, stroke)); + + public void DrawPolyline(ReadOnlySpan points, Stroke stroke) => Polylines.Add((points.ToArray(), stroke)); + + public void DrawPolygon(ReadOnlySpan points, Stroke? stroke, Color? fill) => + Polygons.Add((points.ToArray(), stroke, fill)); + + public void DrawRectangle(CanvasRect rect, Stroke? stroke, Color? fill) => Rectangles.Add((rect, stroke, fill)); + + public void DrawEllipse(CanvasPoint center, double radiusX, double radiusY, Stroke? stroke, Color? fill) => + Ellipses.Add((center, radiusX, radiusY, stroke, fill)); + + public void DrawText(string text, CanvasPoint position, FontSpec font, Color color, TextAlignment alignment) => + Texts.Add((text, position, color, alignment)); + + public TextSize MeasureText(string text, FontSpec font) => new(text.Length * font.Size * 0.6, font.Size * 1.2); + + public void PushClip(CanvasRect rect) + { + ClipDepth++; + MaxClipDepth = Math.Max(MaxClipDepth, ClipDepth); + } + + public void PopClip() => ClipDepth--; +} diff --git a/tests/SharpMath.Canvas.Tests/SharpMath.Canvas.Tests.csproj b/tests/SharpMath.Canvas.Tests/SharpMath.Canvas.Tests.csproj new file mode 100644 index 0000000..84e310c --- /dev/null +++ b/tests/SharpMath.Canvas.Tests/SharpMath.Canvas.Tests.csproj @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/tests/SharpMath.Canvas.Tests/SvgTests.cs b/tests/SharpMath.Canvas.Tests/SvgTests.cs new file mode 100644 index 0000000..ab76a6a --- /dev/null +++ b/tests/SharpMath.Canvas.Tests/SvgTests.cs @@ -0,0 +1,82 @@ +using System.Runtime.CompilerServices; +using SharpMath.Geometry; + +namespace SharpMath.Canvas.Tests; + +public class SvgTests +{ + /// + /// Compares with the golden file copied to the output directory. Set SHARPMATH_UPDATE_GOLDEN=1 to rewrite the + /// golden files in the source tree (the caller's file path is only meaningful in local, non-CI builds). + /// + private static void AssertGolden(string actual, string name, [CallerFilePath] string sourceFile = "") + { + if (Environment.GetEnvironmentVariable("SHARPMATH_UPDATE_GOLDEN") == "1") + { + File.WriteAllText(Path.Combine(Path.GetDirectoryName(sourceFile)!, "Golden", name), actual); + Assert.Fail($"The golden file {name} was rewritten; review and commit it, then run the tests again."); + } + + var path = Path.Combine(AppContext.BaseDirectory, "Golden", name); + Assert.True(File.Exists(path), $"The golden file {name} is missing; run the tests with SHARPMATH_UPDATE_GOLDEN=1."); + Assert.Equal(File.ReadAllText(path).ReplaceLineEndings("\n"), actual.ReplaceLineEndings("\n")); + } + + [Fact] + public void EmptyCanvas_MatchesGoldenFile() => AssertGolden(new Canvas2D().RenderSvg(200, 150), "empty.svg"); + + [Fact] + public void AllItems_MatchGoldenFile() + { + var canvas = new Canvas2D { ShowTrackingLines = true, ShowValueIndicator = true }; + canvas.AddFunction("x^3 / 10"); + canvas.AddFunction(x => 1.5 * Math.Sin(x), new ItemStyle(new Stroke(Color.FromRgb(200, 0, 0), 2, DashStyle.Dashed))); + canvas.AddVector(new Vector2(4, 3)); + canvas.AddPoint(new Point2D(-3, 2)); + canvas.AddLine(Line2D.FromSlopeIntercept(-0.5, -2)); + canvas.AddSegment(new LineSegment2D(new Point2D(-6, -4), new Point2D(-2, -6)), + new ItemStyle(new Stroke(Color.Black, 1, DashStyle.Dotted))); + canvas.AddPolygon(new Polygon(new Point2D(2, -2), new Point2D(6, -2), new Point2D(4, -5))); + canvas.PointerMoved(new CanvasPoint(300, 60)); + AssertGolden(canvas.RenderSvg(400, 300), "items.svg"); + } + + [Fact] + public void RenderSvg_WritesToATextWriter() + { + using var writer = new StringWriter(); + new Canvas2D().RenderSvg(writer, 10, 10); + Assert.StartsWith("(() => ((Canvas2D) null!).RenderSvg(1, 1)); + Assert.Throws(() => new Canvas2D().RenderSvg(null!, 1, 1)); + } + + [Fact] + public void Context_EscapesTextAndHandlesEdgeCases() + { + var context = new SvgDrawingContext(10, 10); + context.DrawText("", new CanvasPoint(1, 1), new FontSpec("A\"B", 10), Color.Black, TextAlignment.Right); + context.DrawPolyline([new CanvasPoint(0, 0)], new Stroke(Color.Black)); + context.DrawPolygon([new CanvasPoint(0, 0), new CanvasPoint(1, 1)], null, Color.Black); + context.DrawRectangle(new CanvasRect(0, 0, 1, 1), new Stroke(Color.FromArgb(128, 0, 0, 0)), null); + context.DrawEllipse(new CanvasPoint(5, 5), 1, 2, null, Color.FromArgb(128, 255, 0, 0)); + context.PushClip(new CanvasRect(0, 0, 5, 5)); + var svg = context.ToString(); + + Assert.Contains("<a & b>", svg, StringComparison.Ordinal); + Assert.Contains("font-family=\"A"B\"", svg, StringComparison.Ordinal); + Assert.Contains("text-anchor=\"end\"", svg, StringComparison.Ordinal); + Assert.DoesNotContain("\n\n", svg, StringComparison.Ordinal); + context.PopClip(); + Assert.Throws(() => context.PopClip()); + Assert.Equal(new TextSize(18, 12), context.MeasureText("abc", new FontSpec("x", 10))); + Assert.Throws(() => new SvgDrawingContext(-1, 1)); + Assert.Throws(() => context.MeasureText(null!, new FontSpec("x", 1))); + Assert.Throws(() => + context.DrawText(null!, default, new FontSpec("x", 1), Color.Black, TextAlignment.Left)); + } +} diff --git a/tests/SharpMath.Canvas.Tests/TicksTests.cs b/tests/SharpMath.Canvas.Tests/TicksTests.cs new file mode 100644 index 0000000..c8043fb --- /dev/null +++ b/tests/SharpMath.Canvas.Tests/TicksTests.cs @@ -0,0 +1,69 @@ +namespace SharpMath.Canvas.Tests; + +public class TicksTests +{ + [Theory] + [InlineData(20, 20, 1)] + [InlineData(80, 20, 5)] + [InlineData(40, 20, 2)] + [InlineData(20, 2000, 0.01)] + [InlineData(20, 0.001, 20000)] + [InlineData(80, 16, 5)] + [InlineData(81, 16, 10)] + public void NiceStep_Uses1_2_5(double minPixels, double scale, double expected) => + Assert.Equal(expected, Ticks.NiceStep(minPixels, scale), 9); + + [Theory] + [InlineData(80, 16, 2, 10)] + [InlineData(80, 20, 1, 5)] + [InlineData(80, 40, 0.5, 2)] + [InlineData(80, 18, 2, 10)] + [InlineData(20, 20, 1, 1)] + [InlineData(80, 0.001, 20000, 100000)] + public void NiceMultiple_IsAMultipleOfTheGridStep(double minPixels, double scale, double gridStep, double expected) + { + var step = Ticks.NiceMultiple(minPixels, scale, gridStep); + Assert.Equal(expected, step, 9); + Assert.Equal(Math.Round(step / gridStep), step / gridStep, 9); + } + + [Theory] + [InlineData(2.5, 1, "2.5")] + [InlineData(5, 1, "5.0")] + [InlineData(0.123456, 0.01, "0.123")] + [InlineData(-1234.5678, 100, "-1234.6")] + public void FormatValue_UsesOneMoreDecimalThanTheStep(double value, double step, string expected) => + Assert.Equal(expected, Ticks.FormatValue(value, step)); + + [Fact] + public void GetNearestScaleRegression_LargeAndSmallSteps() + { + // Regression: the old algorithm normalized with (d − 1) · 10 instead of 10^(d − 1): 400 → 200, 4000 → 300. + Assert.Equal(500, Ticks.NiceStep(400, 1), 9); + Assert.Equal(5000, Ticks.NiceStep(4000, 1), 9); + Assert.Equal(0.005, Ticks.NiceStep(0.004, 1), 12); + } + + [Theory] + [InlineData(0, 1, "0")] + [InlineData(3, 1, "3")] + [InlineData(-3, 0.5, "-1.5")] + [InlineData(7, 0.1, "0.7")] + [InlineData(5, 0.02, "0.10")] + [InlineData(2, 5e7, "1E+8")] + [InlineData(3, 1e-7, "3E-7")] + public void Format_UsesStepDecimalsAndInvariantCulture(long index, double step, string expected) => + Assert.Equal(expected, Ticks.Format(index, step)); + + [Fact] + public void Range_IsEmptyWhenTooLargeOrImprecise() + { + Assert.Equal((-2L, 3L), Ticks.Range(-2.5, 3.5, 1)); + Assert.Equal((1L, 0L), Ticks.Range(0, 1e9, 1)); + Assert.Equal((1L, 0L), Ticks.Range(1e20, 1e20 + 1, 1)); + Assert.Equal((1L, 0L), Ticks.Range(double.NaN, 1, 1)); + Assert.Equal(1, Ticks.SafeSpacing(0)); + Assert.Equal(1, Ticks.SafeSpacing(double.NaN)); + Assert.Equal(25, Ticks.SafeSpacing(25)); + } +} diff --git a/tests/SharpMath.Canvas.Tests/ViewportTests.cs b/tests/SharpMath.Canvas.Tests/ViewportTests.cs new file mode 100644 index 0000000..3bc942b --- /dev/null +++ b/tests/SharpMath.Canvas.Tests/ViewportTests.cs @@ -0,0 +1,127 @@ +using SharpMath.Geometry; + +namespace SharpMath.Canvas.Tests; + +public class ViewportTests +{ + [Fact] + public void Resize_CentersTheOriginUntilItIsSet() + { + var viewport = new Viewport(); + viewport.Resize(400, 200); + Assert.Equal(new CanvasPoint(200, 100), viewport.Origin); + viewport.Resize(600, 300); + Assert.Equal(new CanvasPoint(300, 150), viewport.Origin); + + // Regression: an origin of (0, 0) used to be impossible because it was the "not set" sentinel. + viewport.SetOrigin(new CanvasPoint(0, 0)); + viewport.Resize(800, 400); + Assert.Equal(new CanvasPoint(0, 0), viewport.Origin); + } + + [Fact] + public void Conversions_FlipTheYAxis() + { + var viewport = new Viewport(); + viewport.Resize(400, 200); + Assert.Equal(new CanvasPoint(220, 60), viewport.WorldToScreen(new Point2D(1, 2))); + Assert.Equal(new Point2D(1, 2), viewport.ScreenToWorld(new CanvasPoint(220, 60))); + Assert.Equal(new BoundingBox2D(-10, -5, 10, 5), viewport.VisibleArea); + } + + [Fact] + public void Pan_MovesTheOrigin() + { + var viewport = new Viewport(); + viewport.Resize(400, 200); + viewport.Pan(10, -5); + Assert.Equal(new CanvasPoint(210, 95), viewport.Origin); + viewport.Pan(double.NaN, 1); + Assert.Equal(new CanvasPoint(210, 95), viewport.Origin); + } + + [Fact] + public void ZoomAt_KeepsTheAnchorFixed() + { + var viewport = new Viewport(); + viewport.Resize(400, 200); + var anchor = new CanvasPoint(300, 40); + var before = viewport.ScreenToWorld(anchor); + viewport.ZoomAt(anchor, 1.25); + Assert.Equal(25, viewport.ScaleX, 12); + Assert.True(viewport.ScreenToWorld(anchor).ApproximatelyEquals(before)); + viewport.ZoomAt(anchor, 0.8); + Assert.True(viewport.ScreenToWorld(anchor).ApproximatelyEquals(before)); + } + + [Fact] + public void ZoomAt_DoesNotDriftOrOverflow_Issue3() + { + // Regression for issue #3: zooming in repeatedly near a point far away used to move the origin by 1.25 times the + // distance per step, so the world point under the cursor drifted and coordinates overflowed. + var viewport = new Viewport(); + viewport.Resize(400, 300); + viewport.Pan(0, 5_000); + var anchor = new CanvasPoint(350, 10); + var target = viewport.ScreenToWorld(anchor); + for (var i = 0; i < 200; i++) + viewport.ZoomAt(anchor, 1.25); + + Assert.Equal(Viewport.MaxScale, viewport.ScaleX); + Assert.True(viewport.ScreenToWorld(anchor).ApproximatelyEquals(target, 1e-6, 1e-9)); + Assert.True(double.IsFinite(viewport.Origin.X) && double.IsFinite(viewport.Origin.Y)); + + for (var i = 0; i < 400; i++) + viewport.ZoomAt(anchor, 0.8); + Assert.Equal(Viewport.MinScale, viewport.ScaleY); + } + + [Fact] + public void PanBeforeTheFirstResize_IsRelativeToTheCenteredOrigin() + { + // Regression: panning or zooming before the first render moved the origin to the top-left corner for good. + var viewport = new Viewport(); + viewport.Pan(10, 5); + viewport.ZoomAt(new CanvasPoint(1, 1), 2); + viewport.Resize(400, 200); + Assert.Equal(new CanvasPoint(210, 105), viewport.Origin); + Assert.Equal(40, viewport.ScaleX); + viewport.Resize(600, 300); + Assert.Equal(new CanvasPoint(210, 105), viewport.Origin); + + var zoomedOnly = new Viewport(); + zoomedOnly.ZoomAt(new CanvasPoint(1, 1), 2); + zoomedOnly.Resize(400, 200); + zoomedOnly.Resize(600, 300); + Assert.Equal(new CanvasPoint(300, 150), zoomedOnly.Origin); + } + + [Fact] + public void Validation() + { + var viewport = new Viewport(); + Assert.Throws(() => viewport.ZoomAt(default, 0)); + Assert.Throws(() => viewport.ZoomAt(default, double.PositiveInfinity)); + Assert.Throws(() => viewport.SetScale(0, 1)); + Assert.Throws(() => viewport.SetScale(1, double.NaN)); + Assert.Throws(() => viewport.SetOrigin(new CanvasPoint(double.NaN, 0))); + Assert.Throws(() => viewport.Resize(-1, 1)); + Assert.Throws(() => viewport.Resize(1, double.PositiveInfinity)); + viewport.SetScale(2, 3); + Assert.Equal(2, viewport.ScaleX); + Assert.Equal(3, viewport.ScaleY); + } + + [Fact] + public void Changed_IsRaisedOnlyForRealChanges() + { + var viewport = new Viewport(); + var count = 0; + viewport.Changed += (_, _) => count++; + viewport.Resize(100, 100); + viewport.Resize(100, 100); + Assert.Equal(1, count); + viewport.Pan(1, 0); + Assert.Equal(2, count); + } +} diff --git a/tests/SharpMath.Canvas.WinForms.Tests/Canvas2DControlTests.cs b/tests/SharpMath.Canvas.WinForms.Tests/Canvas2DControlTests.cs new file mode 100644 index 0000000..6e44c45 --- /dev/null +++ b/tests/SharpMath.Canvas.WinForms.Tests/Canvas2DControlTests.cs @@ -0,0 +1,57 @@ +using SharpMath.Geometry; + +namespace SharpMath.Canvas.WinForms.Tests; + +public class Canvas2DControlTests +{ + private sealed class TestControl : Canvas2DControl + { + public int Invalidations { get; private set; } + + public void Press(int x, int y) => OnMouseDown(new MouseEventArgs(MouseButtons.Left, 1, x, y, 0)); + public void MovePointer(int x, int y) => OnMouseMove(new MouseEventArgs(MouseButtons.None, 0, x, y, 0)); + public void Release(int x, int y) => OnMouseUp(new MouseEventArgs(MouseButtons.Left, 1, x, y, 0)); + public void Scroll(int x, int y, int delta) => OnMouseWheel(new MouseEventArgs(MouseButtons.None, 0, x, y, delta)); + public void LeavePointer() => OnMouseLeave(EventArgs.Empty); + + protected override void OnInvalidated(InvalidateEventArgs e) + { + Invalidations++; + base.OnInvalidated(e); + } + } + + [Fact] + public void Paint_RendersTheCanvas() + { + using var control = new Canvas2DControl { Size = new Size(200, 100) }; + control.Canvas.AddPoint(new Point2D(0, 0), new ItemStyle(new Stroke(Color.Black), Color.Black, 4)); + using var bitmap = new Bitmap(200, 100); + control.DrawToBitmap(bitmap, new Rectangle(0, 0, 200, 100)); + Assert.Equal(System.Drawing.Color.Black.ToArgb(), bitmap.GetPixel(100, 50).ToArgb()); + } + + [Fact] + public void MouseInput_PansZoomsAndInvalidates() + { + using var control = new TestControl { Size = new Size(200, 100), ShowTrackingLines = true }; + + // Invalidate only raises Invalidated once the window handle exists. + _ = control.Handle; + control.Canvas.Viewport.Resize(200, 100); + Assert.True(control.ShowTrackingLines); + control.ShowValueIndicator = true; + Assert.True(control.Canvas.ShowValueIndicator); + + control.Press(10, 10); + control.MovePointer(30, 20); + control.Release(30, 20); + Assert.Equal(new CanvasPoint(120, 60), control.Canvas.Viewport.Origin); + + control.Scroll(120, 60, 120); + Assert.Equal(25, control.Canvas.Viewport.ScaleX, 9); + control.LeavePointer(); + Assert.Null(control.Canvas.PointerPosition); + Assert.True(control.Invalidations > 0); + } +} diff --git a/tests/SharpMath.Canvas.WinForms.Tests/GdiDrawingContextTests.cs b/tests/SharpMath.Canvas.WinForms.Tests/GdiDrawingContextTests.cs new file mode 100644 index 0000000..ac1868c --- /dev/null +++ b/tests/SharpMath.Canvas.WinForms.Tests/GdiDrawingContextTests.cs @@ -0,0 +1,80 @@ +using SharpMath.Geometry; +using CanvasColor = SharpMath.Canvas.Color; +using DrawingColor = System.Drawing.Color; + +namespace SharpMath.Canvas.WinForms.Tests; + +public class GdiDrawingContextTests +{ + private static DrawingColor ToGdi(CanvasColor color) => DrawingColor.FromArgb(color.A, color.R, color.G, color.B); + + private static Bitmap Render(Canvas2D canvas, int width = 400, int height = 300) + { + var bitmap = new Bitmap(width, height); + using var graphics = Graphics.FromImage(bitmap); + using var context = new GdiDrawingContext(graphics); + canvas.Render(context, width, height); + return bitmap; + } + + [Fact] + public void Render_DrawsBackgroundAxesAndGrid() + { + using var bitmap = Render(new Canvas2D()); + Assert.Equal(DrawingColor.White.ToArgb(), bitmap.GetPixel(10, 280).ToArgb()); + Assert.Equal(ToGdi(CanvasStyle.Default.AxisStroke.Color).ToArgb(), bitmap.GetPixel(30, 150).ToArgb()); + Assert.Equal(ToGdi(CanvasStyle.Default.AxisStroke.Color).ToArgb(), bitmap.GetPixel(200, 30).ToArgb()); + Assert.Equal(ToGdi(CanvasStyle.Default.MinorGridStroke.Color).ToArgb(), bitmap.GetPixel(40, 280).ToArgb()); + } + + [Fact] + public void Render_DrawsItems() + { + var canvas = new Canvas2D(); + var red = CanvasColor.FromRgb(255, 0, 0); + canvas.AddPolygon(new Polygon(new Point2D(2, 2), new Point2D(6, 2), new Point2D(6, 6), new Point2D(2, 6)), + new ItemStyle(new Stroke(red, 2), red)); + canvas.AddPoint(new Point2D(-5, -5), new ItemStyle(new Stroke(CanvasColor.Black), CanvasColor.Black, 5)); + canvas.AddFunction("x", new ItemStyle(new Stroke(CanvasColor.Black, 1, DashStyle.Dashed))); + canvas.AddVector(new Vector2(-4, 4)); + canvas.ShowTrackingLines = true; + canvas.ShowValueIndicator = true; + // Away from the probed pixels, so the tracking lines do not cover them. + canvas.PointerMoved(new CanvasPoint(370, 20)); + using var bitmap = Render(canvas); + + Assert.Equal(DrawingColor.Red.ToArgb(), bitmap.GetPixel(285, 75).ToArgb()); + Assert.Equal(DrawingColor.Black.ToArgb(), bitmap.GetPixel(100, 250).ToArgb()); + } + + [Fact] + public void Text_IsMeasured() + { + using var bitmap = new Bitmap(10, 10); + using var graphics = Graphics.FromImage(bitmap); + using var context = new GdiDrawingContext(graphics); + var size = context.MeasureText("1234", new FontSpec("Segoe UI", 20)); + Assert.InRange(size.Width, 10, 100); + Assert.InRange(size.Height, 15, 40); + context.DrawText("x", new CanvasPoint(0, 0), new FontSpec("Font that does not exist", 10), CanvasColor.Black, + TextAlignment.Right); + } + + [Fact] + public void Dispose_RestoresGraphicsSettings() + { + using var bitmap = new Bitmap(10, 10); + using var graphics = Graphics.FromImage(bitmap); + var smoothing = graphics.SmoothingMode; + var context = new GdiDrawingContext(graphics); + context.PushClip(new CanvasRect(0, 0, 5, 5)); + context.Dispose(); + context.Dispose(); + Assert.Equal(smoothing, graphics.SmoothingMode); + using (var clip = graphics.Clip) + Assert.True(clip.IsInfinite(graphics)); + using var other = new GdiDrawingContext(graphics); + Assert.Throws(() => other.PopClip()); + Assert.Throws(() => new GdiDrawingContext(null!)); + } +} diff --git a/tests/SharpMath.Canvas.WinForms.Tests/SharpMath.Canvas.WinForms.Tests.csproj b/tests/SharpMath.Canvas.WinForms.Tests/SharpMath.Canvas.WinForms.Tests.csproj new file mode 100644 index 0000000..5fcb4fd --- /dev/null +++ b/tests/SharpMath.Canvas.WinForms.Tests/SharpMath.Canvas.WinForms.Tests.csproj @@ -0,0 +1,19 @@ + + + + net10.0-windows + true + true + + + + + false + false + + + + + + + diff --git a/tests/SharpMath.Tests/AllocationTests.cs b/tests/SharpMath.Tests/AllocationTests.cs new file mode 100644 index 0000000..3c72f30 --- /dev/null +++ b/tests/SharpMath.Tests/AllocationTests.cs @@ -0,0 +1,88 @@ +using SharpMath.Expressions; +using SharpMath.Geometry; + +namespace SharpMath.Tests; + +/// +/// Guards the hot paths against regressions that reintroduce heap allocations (e.g. boxing, iterators or LINQ). +/// +public class AllocationTests +{ + private static double _sink; + + private static long AllocatedBytes(Action action) + { + // Warm up so that JIT compilation and static initialization are not counted. + for (var i = 0; i < 3; i++) + action(); + + var before = GC.GetAllocatedBytesForCurrentThread(); + for (var i = 0; i < 100; i++) + action(); + return GC.GetAllocatedBytesForCurrentThread() - before; + } + + [Fact] + public void VectorOperations_DoNotAllocate() + { + var a = new Vector3(1, 2, 3); + var b = new Vector3(-2, 0.5, 4); + Assert.Equal(0, AllocatedBytes(() => + { + var c = a + b * 2 - a.Cross(b) / 3; + _sink = c.Dot(a) + c.Normalize().Length + (a == b ? 1 : 0) + a.GetHashCode() + (a.IsZero ? 1 : 0) + + a.Angle(b) + (a.IsParallelTo(b) ? 1 : 0) + (a.ApproximatelyEquals(b) ? 1 : 0); + })); + var p = new Vector4(1, 2, 3, 4); + Assert.Equal(0, AllocatedBytes(() => _sink = (p * 2 + p).Normalize().Length + (p.IsParallelTo(p) ? 1 : 0))); + } + + [Fact] + public void MatrixOperations_DoNotAllocate() + { + var m = Matrix4x4.Rotation(0.3, 0.5, 0.7) * Matrix4x4.Translation(1, 2, 3) * Matrix4x4.Scaling(2); + var v = new Vector3(1, 2, 3); + Assert.Equal(0, AllocatedBytes(() => + { + var product = m * m.Inverse() * m.Transpose(); + _sink = product.Determinant + (m * v).X + (m == product ? 1 : 0) + m.GetHashCode() + + (m.IsOrthogonal ? 1 : 0) + (m.IsSingular ? 1 : 0) + m.Adjugate().Trace + + (m.ApproximatelyEquals(product) ? 1 : 0) + (m.IsDiagonal ? 1 : 0) + (m.IsSymmetric ? 1 : 0); + })); + + var small = new Matrix3x3(2, 5, 2, 3, -3, 1, 1, 4, -4); + Assert.Equal(0, AllocatedBytes(() => _sink = small.Inverse().Determinant + (small * Vector2.One).X + + small.Cofactors().Trace + (small.IsTriangular ? 1 : 0))); + var tiny = new Matrix2x2(8, 3, 4, 2); + Assert.Equal(0, AllocatedBytes(() => _sink = tiny.Inverse().Determinant + (tiny * tiny).Trace)); + } + + [Fact] + public void GeometryQueries_DoNotAllocate() + { + var polygon = new Polygon(new Point2D(0, 0), new Point2D(4, 0), new Point2D(4, 4), new Point2D(2, 1), + new Point2D(0, 4)); + var line = Line2D.FromPoints(new Point2D(0, 1), new Point2D(3, 2)); + var other = Line2D.FromSlopeIntercept(-1, 5); + var segment = new LineSegment2D(new Point2D(0, 0), new Point2D(5, 5)); + var line3 = Line3D.FromPointAndDirection(new Point3D(1, 2, 3), new Vector3(1, 1, 0)); + var skew = Line3D.FromPointAndDirection(new Point3D(0, 0, 5), new Vector3(0, 1, 1)); + Assert.Equal(0, AllocatedBytes(() => + _sink = (polygon.Contains(new Point2D(1, 1)) ? 1 : 0) + polygon.Area + polygon.Perimeter + + polygon.Centroid.X + polygon.BoundingBox.Width + line.Intersect(other).Point!.Value.X + + segment.Intersect(new LineSegment2D(new Point2D(0, 5), new Point2D(5, 0))).Point!.Value.Y + + line3.DistanceTo(skew) + (line3.Intersect(skew).Kind == IntersectionKind.Skew ? 1 : 0))); + } + + [Fact] + public void EvaluatingParsedExpressions_DoesNotAllocate() + { + var expression = MathExpression.Parse("2x^2 - 3x + sin(x) * max(x, 1, 2) + 5!"); + var function = expression.ToFunc("x"); + var x = 0.0; + Assert.Equal(0, AllocatedBytes(() => _sink = function(x += 0.1))); + Assert.Equal(0, AllocatedBytes(() => _sink = expression.Evaluate("x", x += 0.1))); + var constant = MathExpression.Parse("2 + 3 * 4"); + Assert.Equal(0, AllocatedBytes(() => _sink = constant.Evaluate())); + } +} diff --git a/tests/SharpMath.Tests/Equations/LinearEquationSystemTests.cs b/tests/SharpMath.Tests/Equations/LinearEquationSystemTests.cs new file mode 100644 index 0000000..d427fd7 --- /dev/null +++ b/tests/SharpMath.Tests/Equations/LinearEquationSystemTests.cs @@ -0,0 +1,80 @@ +using SharpMath.Equations; + +namespace SharpMath.Tests.Equations; + +public class LinearEquationSystemTests +{ + [Fact] + public void Solve_ThreeVariables() + { + var system = new LinearEquationSystem( + new LinearEquation([1.0, -1.0, 2.0], 6), + new LinearEquation([2.0, 3.0, 2.0], 11), + new LinearEquation([3.0, 2.0, 1.0], 8)); + var results = system.Solve(); + Assert.Equal(1, results[0], 12); + Assert.Equal(1, results[1], 12); + Assert.Equal(3, results[2], 12); + } + + [Fact] + public void Solve_SingleEquationAndParamsConstructor() + { + Assert.Equal(0.5, new LinearEquationSystem(new LinearEquation([2.0], 1)).Solve()[0]); + var equation = new LinearEquation([1.0, 1.0], 6); + Assert.Equal([1.0, 1.0], equation.Coefficients); + Assert.Equal(6, equation.Result); + } + + [Fact] + public void Solve_RequiresAPivotSearchDownTheColumn() + { + // Regression: y + z = 5, y = 2, x = 1 used to be reported as not solvable. + var system = new LinearEquationSystem(new List + { + new([0.0, 1.0, 1.0], 5), + new([0.0, 1.0, 0.0], 2), + new([1.0, 0.0, 0.0], 1) + }); + Assert.Equal([1.0, 2.0, 3.0], system.Solve()); + } + + [Fact] + public void Solve_WorksAtSmallScales() + { + // Regression: 1e-10·x = 1e-10 used to be rejected by an absolute epsilon. + Assert.Equal(1, new LinearEquationSystem(new LinearEquation([1e-10], 1e-10)).Solve()[0], 12); + } + + [Fact] + public void Solve_ThrowsWithoutUniqueSolution() + { + Assert.Throws(() => + new LinearEquationSystem(new LinearEquation([0.0], 0)).Solve()); + Assert.Throws(() => new LinearEquationSystem( + new LinearEquation([1.0, 2.0], 3), new LinearEquation([2.0, 4.0], 6)).Solve()); + } + + [Fact] + public void Constructor_ValidatesTheShape() + { + Assert.Equal("equations", Assert.Throws(() => new LinearEquationSystem()).ParamName); + + // Regression: too many coefficients used to throw IndexOutOfRangeException; too few were padded with zeros. + Assert.Throws(() => new LinearEquationSystem(new LinearEquation([1.0, 2.0], 3))); + Assert.Throws(() => new LinearEquationSystem( + new LinearEquation([1.0], 3), new LinearEquation([1.0, 2.0], 3))); + Assert.Throws(() => new LinearEquationSystem(new LinearEquation[] { null! })); + Assert.Throws(() => new LinearEquationSystem((IEnumerable) null!)); + Assert.Throws(() => new LinearEquation(null!, 1)); + } + + [Fact] + public void Exception_HasStandardConstructors() + { + Assert.NotEmpty(new EquationNotSolvableException().Message); + Assert.Equal("x", new EquationNotSolvableException("x").Message); + var inner = new InvalidOperationException(); + Assert.Same(inner, new EquationNotSolvableException("x", inner).InnerException); + } +} diff --git a/tests/SharpMath.Tests/Expressions/ExpressionContextTests.cs b/tests/SharpMath.Tests/Expressions/ExpressionContextTests.cs new file mode 100644 index 0000000..7bb0bcc --- /dev/null +++ b/tests/SharpMath.Tests/Expressions/ExpressionContextTests.cs @@ -0,0 +1,106 @@ +using SharpMath.Expressions; + +namespace SharpMath.Tests.Expressions; + +public class ExpressionContextTests +{ + [Fact] + public void CustomFunctionsAndConstants() + { + var context = new ExpressionContext() + .AddFunction("sec", x => 1 / Math.Cos(x)) + .AddFunction("hypot", (a, b) => Math.Sqrt(a * a + b * b)) + .AddFunction("sum", 0, int.MaxValue, values => + { + var sum = 0.0; + foreach (var value in values) + sum += value; + return sum; + }) + .AddConstant("phi", (1 + Math.Sqrt(5)) / 2); + + Assert.Equal(1, MathExpression.Parse("sec(0)", context).Evaluate()); + Assert.Equal(5, MathExpression.Parse("hypot(3, 4)", context).Evaluate()); + Assert.Equal(0, MathExpression.Parse("sum()", context).Evaluate()); + Assert.Equal(6, MathExpression.Parse("sum(1, 2, 3)", context).Evaluate()); + Assert.Equal((1 + Math.Sqrt(5)) / 2, MathExpression.Parse("PHI", context).Evaluate()); + } + + [Fact] + public void LongestMatch_PrefersLongerNames() + { + // Regression: the old tokenizer stopped at the shortest match, so "sinh" was read as "sin" + "h". + Assert.Equal(Math.Sinh(1), MathExpression.Parse("sinh(1)").Evaluate(), 12); + var context = new ExpressionContext().AddFunction("expm", x => Math.Exp(x) - 1); + Assert.Equal(0, MathExpression.Parse("expm(0)", context).Evaluate()); + } + + [Fact] + public void DeclaredVariables_MayHaveSeveralLetters() + { + var context = new ExpressionContext().DeclareVariable("speed").DeclareVariable("t1"); + var expression = MathExpression.Parse("speed * t1", context); + Assert.Equal(["speed", "t1"], expression.Variables); + Assert.Equal(6, expression.Evaluate(new Dictionary { ["speed"] = 2, ["T1"] = 3 })); + } + + [Fact] + public void Contexts_AreIndependentAndCapturedAtParseTime() + { + var context = new ExpressionContext().AddConstant("k", 2); + var expression = MathExpression.Parse("k", context); + context.AddConstant("k", 3); + Assert.Equal(2, expression.Evaluate()); + Assert.Equal(3, MathExpression.Parse("k", context).Evaluate()); + Assert.Equal(["k"], MathExpression.Parse("k").Variables); + } + + [Fact] + public void Clone_CreatesAnIndependentCopy() + { + var original = new ExpressionContext().AddConstant("k", 2).DeclareVariable("speed"); + var clone = original.Clone().AddConstant("k", 5); + Assert.Equal(2, MathExpression.Parse("k", original).Evaluate()); + Assert.Equal(5, MathExpression.Parse("k", clone).Evaluate()); + Assert.Equal(["speed"], MathExpression.Parse("speed", clone).Variables); + Assert.Equal(1, MathExpression.Parse("sin(pi/2)", clone).Evaluate(), 12); + Assert.Equal(6, MathExpression.Parse("speed(2)", clone).Evaluate("speed", 3)); + } + + [Fact] + public void ImpureFunctions_AreNotFolded() + { + var calls = 0; + var context = new ExpressionContext().AddFunction("count", _ => ++calls); + var expression = MathExpression.Parse("count(1)", context); + Assert.Equal(0, calls); + Assert.Equal(1, expression.Evaluate()); + Assert.Equal(2, expression.Evaluate()); + } + + [Theory] + [InlineData("")] + [InlineData("1x")] + [InlineData("a b")] + [InlineData("ä")] + [InlineData("a_b")] + public void Names_AreValidated(string name) + { + var context = new ExpressionContext(); + Assert.Throws(() => context.AddConstant(name, 1)); + } + + [Fact] + public void Arguments_AreValidated() + { + var context = new ExpressionContext(); + Assert.Throws(() => context.AddConstant(null!, 1)); + Assert.Throws(() => context.AddFunction("f", (Func) null!)); + Assert.Throws(() => context.AddFunction("f", (Func) null!)); + Assert.Throws(() => context.AddFunction("f", 1, 2, null!)); + Assert.Throws(() => context.AddFunction("f", -1, 2, _ => 0)); + Assert.Throws(() => context.AddFunction("f", 3, 2, _ => 0)); + Assert.Throws(() => MathExpression.Parse("f(1, 2, 3)", + new ExpressionContext().AddFunction("f", 1, 2, _ => 0))); + } +} diff --git a/tests/SharpMath.Tests/Expressions/MathExpressionTests.cs b/tests/SharpMath.Tests/Expressions/MathExpressionTests.cs new file mode 100644 index 0000000..720713b --- /dev/null +++ b/tests/SharpMath.Tests/Expressions/MathExpressionTests.cs @@ -0,0 +1,207 @@ +using SharpMath.Expressions; + +namespace SharpMath.Tests.Expressions; + +public class MathExpressionTests +{ + private static double Eval(string text) => MathExpression.Parse(text).Evaluate(); + + [Theory] + [InlineData("(-2+3)*(cos(0)*(2/3))", 2.0 / 3.0)] + [InlineData("3*4^2", 48)] + [InlineData("2^3^2", 512)] + [InlineData("-2^2", -4)] + [InlineData("(-2)^2", 4)] + [InlineData("10 - 4 - 3", 3)] + [InlineData("12 / 3 / 2", 2)] + [InlineData("1 + 2 * 3", 7)] + [InlineData(" 7 ", 7)] + public void Evaluate_RespectsPrecedenceAndAssociativity(string text, double expected) => + Assert.Equal(expected, Eval(text), 12); + + [Theory] + [InlineData("5-(-3)", 8)] + [InlineData("1-(-1)", 2)] + [InlineData("4-(-2)*3", 10)] + [InlineData("+3", 3)] + [InlineData("(+3)", 3)] + [InlineData("2*(+3)", 6)] + [InlineData("2*-3", -6)] + [InlineData("2^-1", 0.5)] + [InlineData("--3", 3)] + [InlineData("-(-(-3))", -3)] + public void Evaluate_HandlesSignsEverywhere(string text, double expected) => + Assert.Equal(expected, Eval(text), 12); + + [Theory] + [InlineData("5%3", 2)] + [InlineData("-7%3", -1)] + [InlineData("2*5%3", 1)] + [InlineData("5!", 120)] + [InlineData("0!", 1)] + [InlineData("3!^2", 36)] + [InlineData("2^3!", 64)] + [InlineData(".5 + 1.", 1.5)] + [InlineData("1e3", 1000)] + [InlineData("2.5E-3", 0.0025)] + [InlineData("1e+2", 100)] + public void Evaluate_SupportsOperatorsAndNumberFormats(string text, double expected) => + Assert.Equal(expected, Eval(text), 12); + + [Theory] + [InlineData("log(8, 2)", 3)] + [InlineData("atan2(1, 1)", Math.PI / 4)] + [InlineData("pow(2, 10)", 1024)] + [InlineData("min(3, -1, 2)", -1)] + [InlineData("max(3, -1, 2)", 3)] + [InlineData("max(4)", 4)] + [InlineData("sqrt(16) + abs(-2)", 6)] + [InlineData("ln(e) + lg(100) + log2(8)", 6)] + [InlineData("exp(0) + floor(2.7) + ceil(2.1) + round(2.5) + round(-2.5)", 6)] + [InlineData("sign(-5) + sign(0) + sign(3)", 0)] + [InlineData("sin(0) + cos(0) + tan(0)", 1)] + [InlineData("asin(1) + acos(1) + atan(0)", Math.PI / 2)] + [InlineData("sinh(0) + cosh(0) + tanh(0)", 1)] + [InlineData("SIN(PI/2)", 1)] + [InlineData("tau", Math.Tau)] + [InlineData("sin (pi / 2)", 1)] + public void Evaluate_KnowsBuiltIns(string text, double expected) => Assert.Equal(expected, Eval(text), 12); + + [Fact] + public void Factorial_IsNaNOutsideItsDomain() + { + Assert.True(double.IsNaN(Eval("2.5!"))); + Assert.True(double.IsNaN(Eval("(-1)!"))); + Assert.True(double.IsPositiveInfinity(Eval("171!"))); + Assert.True(double.IsNaN(Eval("sign(0/0)"))); + } + + [Theory] + [InlineData("2x", 3, 6)] + [InlineData("2x^2", 3, 18)] + [InlineData("3(x+1)", 3, 12)] + [InlineData("(x+1)(x-1)", 3, 8)] + [InlineData("2pi", 0, 2 * Math.PI)] + [InlineData("pix", 2, 2 * Math.PI)] + [InlineData("2sin(x)", 0, 0)] + [InlineData("x!", 3, 6)] + [InlineData("1/2x", 4, 2)] + [InlineData("2e", 0, 2 * Math.E)] + [InlineData("exp(x)", 0, 1)] + [InlineData("-x^2", 3, -9)] + public void Evaluate_SupportsImplicitMultiplication(string text, double x, double expected) => + Assert.Equal(expected, MathExpression.Parse(text).Evaluate("x", x), 12); + + [Fact] + public void Variables_AreCollectedInOrderAndCaseInsensitive() + { + var expression = MathExpression.Parse("x*Y + y - X + z"); + Assert.Equal(["x", "y", "z"], expression.Variables); + var result = expression.Evaluate(new Dictionary { ["X"] = 2, ["y"] = 3, ["Z"] = 1, ["w"] = 9 }); + Assert.Equal(2 * 3 + 3 - 2 + 1, result); + Assert.Equal(6, MathExpression.Parse("xy").Evaluate(new Dictionary { ["x"] = 2, ["y"] = 3 })); + } + + [Fact] + public void Evaluate_ReportsMissingVariables() + { + var expression = MathExpression.Parse("x + y"); + var exception = Assert.Throws(() => + expression.Evaluate(new Dictionary { ["x"] = 1 })); + Assert.Contains("y", exception.Message, StringComparison.Ordinal); + Assert.Throws(() => expression.Evaluate()); + Assert.Throws(() => expression.Evaluate("x", 1)); + Assert.Throws(() => MathExpression.Parse("y").Evaluate("x", 1)); + Assert.Equal(4, MathExpression.Parse("4").Evaluate("x", 1)); + Assert.Equal(2, MathExpression.Parse("X").Evaluate("x", 2)); + Assert.Throws(() => expression.Evaluate((IReadOnlyDictionary) null!)); + Assert.Throws(() => expression.Evaluate(null!, 1)); + } + + [Fact] + public void ToFunc_CreatesReusableDelegate() + { + var function = MathExpression.Parse("x^2 + 1").ToFunc("x"); + Assert.Equal(1, function(0)); + Assert.Equal(10, function(3)); + Assert.Equal(5, MathExpression.Parse("5").ToFunc("x")(42)); + Assert.Throws(() => MathExpression.Parse("x + y").ToFunc("x")); + Assert.Throws(() => MathExpression.Parse("x").ToFunc(null!)); + } + + [Fact] + public void TextAndToString_ReturnSource() + { + var expression = MathExpression.Parse("1 + x"); + Assert.Equal("1 + x", expression.Text); + Assert.Equal("1 + x", expression.ToString()); + Assert.Throws(() => MathExpression.Parse(null!)); + } + + [Theory] + [InlineData("", 0)] + [InlineData(" ", 0)] + [InlineData("3+", 2)] + [InlineData("*3", 0)] + [InlineData("()", 1)] + [InlineData("3+4)", 3)] + [InlineData("(3+4", 4)] + [InlineData("((3)", 4)] + [InlineData("2 3", 2)] + [InlineData("x2", 1)] + [InlineData("sin()", 0)] + [InlineData("sin 5", 4)] + [InlineData("log(8)", 0)] + [InlineData("atan2(1, 2, 3)", 0)] + [InlineData("3 # 4", 2)] + [InlineData(".", 0)] + [InlineData("1,2", 1)] + [InlineData("min()", 0)] + public void Parse_ReportsErrorsWithPosition(string text, int position) + { + var exception = Assert.Throws(() => MathExpression.Parse(text)); + Assert.Equal(position, exception.Position); + } + + [Theory] + [InlineData("cot(x)", 0)] + [InlineData("1 + foo(2)", 4)] + [InlineData("xy(2)", 0)] + [InlineData("2 * sinx (2)", 4)] + public void Parse_ReportsUnknownFunctions(string text, int position) + { + var exception = Assert.Throws(() => MathExpression.Parse(text)); + Assert.Equal(position, exception.Position); + Assert.Contains("Unknown function", exception.Message, StringComparison.Ordinal); + } + + [Theory] + [InlineData("x(x+1)", 2, 6)] + [InlineData("pi(2)", 0, 2 * Math.PI)] + [InlineData("xsin(x)", 0, 0)] + [InlineData("2 (x)", 3, 6)] + public void Parentheses_AfterVariablesAndConstants_StillMultiply(string text, double x, double expected) => + Assert.Equal(expected, MathExpression.Parse(text).Evaluate("x", x), 12); + + [Fact] + public void DeeplyNestedOrHugeInput_IsRejectedInsteadOfCrashing() + { + Assert.Throws(() => MathExpression.Parse(new string('(', 300) + "1" + new string(')', 300))); + Assert.Throws(() => MathExpression.Parse(new string('-', 2000) + "x")); + Assert.Throws(() => MathExpression.Parse(string.Join("^", Enumerable.Repeat("x", 2000)))); + Assert.Throws(() => MathExpression.Parse(string.Join("+", Enumerable.Repeat("x", 5000)))); + + Assert.Equal(1, MathExpression.Parse(new string('(', 100) + "1" + new string(')', 100)).Evaluate()); + Assert.Equal(3000, MathExpression.Parse(string.Join("+", Enumerable.Repeat("x", 3000))).Evaluate("x", 1)); + Assert.Equal(5000, MathExpression.Parse(string.Join("+", Enumerable.Repeat("1", 5000))).Evaluate()); + } + + [Fact] + public void ParserException_HasStandardConstructors() + { + Assert.Equal(-1, new ParserException().Position); + Assert.Equal(-1, new ParserException("x").Position); + Assert.Equal(-1, new ParserException("x", new InvalidOperationException()).Position); + Assert.Contains("position 3", new ParserException("x", 3).Message, StringComparison.Ordinal); + } +} diff --git a/tests/SharpMath.Tests/Geometry/BoundingBox2DTests.cs b/tests/SharpMath.Tests/Geometry/BoundingBox2DTests.cs new file mode 100644 index 0000000..2271c1e --- /dev/null +++ b/tests/SharpMath.Tests/Geometry/BoundingBox2DTests.cs @@ -0,0 +1,33 @@ +using SharpMath.Geometry; + +namespace SharpMath.Tests.Geometry; + +public class BoundingBox2DTests +{ + [Fact] + public void PropertiesAndOperations() + { + var box = new BoundingBox2D(1, 2, 5, 4); + Assert.Equal(4, box.Width); + Assert.Equal(2, box.Height); + Assert.Equal(new Point2D(3, 3), box.Center); + Assert.True(box.Contains(new Point2D(1, 2))); + Assert.False(box.Contains(new Point2D(0, 3))); + Assert.Equal(new BoundingBox2D(0, 0, 5, 4), box.Union(new BoundingBox2D(0, 0, 1, 1))); + Assert.Equal(box, BoundingBox2D.FromPoints([new Point2D(5, 2), new Point2D(1, 4)])); + Assert.Throws(() => BoundingBox2D.FromPoints([])); + Assert.Throws(() => new BoundingBox2D(2, 0, 1, 0)); + Assert.Throws(() => new BoundingBox2D(0, 2, 1, 1)); + } + + [Fact] + public void EqualityAndStrings() + { + var box = new BoundingBox2D(1, 2, 5, 4); + Assert.True(box == new BoundingBox2D(1, 2, 5, 4)); + Assert.True(box != new BoundingBox2D(1, 2, 5, 5)); + Assert.True(box.Equals((object) new BoundingBox2D(1, 2, 5, 4))); + Assert.Equal(box.GetHashCode(), new BoundingBox2D(1, 2, 5, 4).GetHashCode()); + Assert.Equal("BoundingBox2D [Min: (1, 2), Max: (5, 4)]", box.ToString()); + } +} diff --git a/tests/SharpMath.Tests/Geometry/Line2DTests.cs b/tests/SharpMath.Tests/Geometry/Line2DTests.cs new file mode 100644 index 0000000..baae7bf --- /dev/null +++ b/tests/SharpMath.Tests/Geometry/Line2DTests.cs @@ -0,0 +1,124 @@ +using SharpMath.Geometry; + +namespace SharpMath.Tests.Geometry; + +public class Line2DTests +{ + [Fact] + public void FromPoints_ProvidesSlopeAndIntercept() + { + var line = Line2D.FromPoints(new Point2D(0, 2), new Point2D(3, 1)); + Assert.Equal(-1d / 3d, line.Slope, 12); + Assert.Equal(2, line.YIntercept, 12); + + var second = Line2D.FromPoints(new Point2D(4, 5), new Point2D(1, 3)); + Assert.Equal(2d / 3d, second.Slope, 12); + Assert.Equal(7d / 3d, second.YIntercept, 12); + } + + [Fact] + public void FromPoints_RejectsIdenticalPoints() => + Assert.Throws(() => Line2D.FromPoints(new Point2D(1, 1), new Point2D(1, 1))); + + [Fact] + public void VerticalLines_AreSupported() + { + // Regression: vertical lines used to have an infinite slope and contain none of their points. + var line = Line2D.FromPoints(new Point2D(1, 0), new Point2D(1, 5)); + Assert.True(line.IsVertical); + Assert.False(line.IsHorizontal); + Assert.True(double.IsNaN(line.Slope)); + Assert.True(double.IsNaN(line.YIntercept)); + Assert.True(double.IsNaN(line.SolveForY(1))); + Assert.Equal(1, line.SolveForX(42), 12); + Assert.True(line.Contains(new Point2D(1, 0))); + Assert.True(line.Contains(new Point2D(1, 5))); + Assert.False(line.Contains(new Point2D(2, 5))); + } + + [Fact] + public void GeneralForm_IsNormalized() + { + // Issue #9: 4/3·x − y + 1/3 = 0 + var line = Line2D.FromGeneralForm(1.3333333333333333, -1, 0.3333333333333335); + Assert.Equal(1, line.A * line.A + line.B * line.B, 12); + Assert.True(line.A > 0); + Assert.Equal((-0.3333333333333335 - 1.3333333333333333 * 2) / -1, line.SolveForY(2), 12); + Assert.True(line.ApproximatelyEquals(Line2D.FromGeneralForm(-2.6666666666666666, 2, -0.666666666666667))); + Assert.True(Line2D.FromGeneralForm(0, -2, 4).ApproximatelyEquals(Line2D.FromGeneralForm(0, 1, -2))); + Assert.True(Line2D.FromGeneralForm(0, 1, -2).IsHorizontal); + Assert.True(double.IsNaN(Line2D.FromGeneralForm(0, 1, -2).SolveForX(1))); + Assert.Throws(() => Line2D.FromGeneralForm(0, 0, 1)); + Assert.Throws(() => Line2D.FromGeneralForm(double.NaN, 1, 1)); + } + + [Fact] + public void FactoriesProduceTheSameLine() + { + var fromSlope = Line2D.FromSlopeIntercept(2, 3); + var fromPoints = Line2D.FromPoints(new Point2D(0, 3), new Point2D(1, 5)); + var fromDirection = Line2D.FromPointAndDirection(new Point2D(-1, 1), new Vector2(-2, -4)); + Assert.True(fromSlope.ApproximatelyEquals(fromPoints)); + Assert.True(fromSlope.ApproximatelyEquals(fromDirection)); + Assert.Throws(() => Line2D.FromPointAndDirection(Point2D.Origin, Vector2.Zero)); + } + + [Fact] + public void Contains_DistanceAndProjection() + { + var line = Line2D.FromSlopeIntercept(1, 0); + Assert.True(line.Contains(new Point2D(1, 1))); + Assert.False(line.Contains(new Point2D(2, 1))); + Assert.True(Line2D.FromSlopeIntercept(2, 3).Contains(new Point2D(4, 11))); + Assert.Equal(Math.Sqrt(2), line.DistanceTo(new Point2D(2, 0)), 12); + Assert.Equal(-Math.Sqrt(2), line.SignedDistanceTo(new Point2D(0, 2)), 12); + Assert.True(line.ClosestPoint(new Point2D(2, 0)).ApproximatelyEquals(new Point2D(1, 1))); + Assert.Equal(new Point2D(1, 4), new Point2D(1, Line2D.FromSlopeIntercept(3, 1).SolveForY(1))); + Assert.True(line.Contains(line.PointAt(3.5))); + Assert.True(line.Direction.IsNormalized); + Assert.True(line.Normal.IsOrthogonalTo(line.Direction)); + } + + [Fact] + public void Parallelism() + { + Assert.True(Line2D.FromSlopeIntercept(2, 0).IsParallelTo(Line2D.FromSlopeIntercept(2, 3))); + Assert.False(Line2D.FromSlopeIntercept(2, 3).IsParallelTo(Line2D.FromSlopeIntercept(3, 1))); + } + + [Fact] + public void Intersect_ReportsPointNoneAndCoincident() + { + var intersection = Line2D.FromSlopeIntercept(0.5, -1).Intersect(Line2D.FromSlopeIntercept(-2, -6)); + Assert.Equal(IntersectionKind.Point, intersection.Kind); + Assert.True(intersection.Point!.Value.ApproximatelyEquals(new Point2D(-2, -2))); + + var second = Line2D.FromSlopeIntercept(2, 0).Intersect(Line2D.FromSlopeIntercept(3, 1)); + Assert.True(second.Point!.Value.ApproximatelyEquals(new Point2D(-1, -2))); + + var parallel = Line2D.FromSlopeIntercept(2, 0).Intersect(Line2D.FromSlopeIntercept(2, 3)); + Assert.Equal(new LineIntersection2D(IntersectionKind.None, null), parallel); + + // Regression: coincident lines used to report "no intersection" with a NaN point. + var line = Line2D.FromSlopeIntercept(2, 1); + Assert.Equal(IntersectionKind.Coincident, line.Intersect(Line2D.FromPoints(new Point2D(0, 1), new Point2D(1, 3))).Kind); + + var vertical = Line2D.FromGeneralForm(1, 0, -2).Intersect(Line2D.FromSlopeIntercept(1, 0)); + Assert.True(vertical.Point!.Value.ApproximatelyEquals(new Point2D(2, 2))); + } + + [Fact] + public void EqualityAndStrings() + { + var line = Line2D.FromGeneralForm(3, 4, 5); + Assert.Equal(0.6, line.A, 12); + Assert.Equal(0.8, line.B, 12); + Assert.Equal(1, line.C, 12); + Assert.True(line == Line2D.FromGeneralForm(3, 4, 5)); + Assert.True(line != Line2D.FromGeneralForm(3, 4, 6)); + Assert.True(line.Equals((object) Line2D.FromGeneralForm(6, 8, 10))); + Assert.False(line.Equals((object) 3)); + Assert.Equal(line.GetHashCode(), Line2D.FromGeneralForm(3, 4, 5).GetHashCode()); + Assert.Equal("Line2D [0.6·x + 0.8·y + 1 = 0]", line.ToString()); + } +} diff --git a/tests/SharpMath.Tests/Geometry/Line3DTests.cs b/tests/SharpMath.Tests/Geometry/Line3DTests.cs new file mode 100644 index 0000000..146f487 --- /dev/null +++ b/tests/SharpMath.Tests/Geometry/Line3DTests.cs @@ -0,0 +1,84 @@ +using SharpMath.Geometry; + +namespace SharpMath.Tests.Geometry; + +public class Line3DTests +{ + [Fact] + public void Factories_StoreCanonicalForm() + { + var line = Line3D.FromPoints(Point3D.Origin, new Point3D(1, 1, 1)); + Assert.Equal(Point3D.Origin, line.Point); + Assert.True(line.Direction.ApproximatelyEquals(Vector3.One.Normalize())); + + var reversed = Line3D.FromPoints(new Point3D(2, 2, 2), new Point3D(-1, -1, -1)); + Assert.True(line.ApproximatelyEquals(reversed)); + + var offset = Line3D.FromPointAndDirection(new Point3D(5, 1, 0), new Vector3(-3, 0, 0)); + Assert.Equal(new Point3D(0, 1, 0), offset.Point); + Assert.Equal(Vector3.UnitX, offset.Direction); + + Assert.Throws(() => Line3D.FromPoints(Point3D.Origin, Point3D.Origin)); + Assert.Throws(() => Line3D.FromPointAndDirection(Point3D.Origin, Vector3.Zero)); + } + + [Fact] + public void Contains_WorksForAxisAlignedDirections() + { + var line = Line3D.FromPointAndDirection(new Point3D(2, 3, 1), new Vector3(2, 1, 2)); + Assert.True(line.Contains(new Point3D(6, 5, 5))); + Assert.False(line.Contains(new Point3D(2, 3.5, 2))); + + // Regression: zero direction components used to break the check. + Assert.True(Line3D.FromPointAndDirection(Point3D.Origin, Vector3.UnitX).Contains(new Point3D(5, 0, 0))); + Assert.True(Line3D.FromPointAndDirection(Point3D.Origin, new Vector3(0, 1, 1)).Contains(new Point3D(0, 2, 2))); + } + + [Fact] + public void DistancesAndClosestPoints() + { + var xAxis = Line3D.FromPointAndDirection(Point3D.Origin, Vector3.UnitX); + Assert.Equal(5, xAxis.DistanceTo(new Point3D(7, 3, 4)), 12); + Assert.Equal(new Point3D(7, 0, 0), xAxis.ClosestPoint(new Point3D(7, 3, 4))); + Assert.Equal(new Point3D(2, 0, 0), xAxis.PointAt(2)); + + var skew = Line3D.FromPointAndDirection(new Point3D(0, 0, 3), Vector3.UnitY); + Assert.Equal(3, xAxis.DistanceTo(skew), 12); + var (onX, onSkew) = xAxis.ClosestPoints(skew); + Assert.True(onX.ApproximatelyEquals(Point3D.Origin)); + Assert.True(onSkew.ApproximatelyEquals(new Point3D(0, 0, 3))); + + var parallel = Line3D.FromPointAndDirection(new Point3D(0, 2, 0), Vector3.UnitX); + Assert.Equal(2, xAxis.DistanceTo(parallel), 12); + Assert.True(xAxis.IsParallelTo(parallel)); + Assert.False(xAxis.IsParallelTo(skew)); + } + + [Fact] + public void Intersect_ReportsAllKinds() + { + var xAxis = Line3D.FromPointAndDirection(Point3D.Origin, Vector3.UnitX); + var crossing = Line3D.FromPoints(new Point3D(1, -1, 0), new Point3D(1, 1, 0)); + var intersection = xAxis.Intersect(crossing); + Assert.Equal(IntersectionKind.Point, intersection.Kind); + Assert.True(intersection.Point!.Value.ApproximatelyEquals(new Point3D(1, 0, 0))); + + Assert.Equal(IntersectionKind.Skew, + xAxis.Intersect(Line3D.FromPointAndDirection(new Point3D(0, 0, 3), Vector3.UnitY)).Kind); + Assert.Equal(IntersectionKind.None, + xAxis.Intersect(Line3D.FromPointAndDirection(new Point3D(0, 2, 0), Vector3.UnitX)).Kind); + Assert.Equal(IntersectionKind.Coincident, + xAxis.Intersect(Line3D.FromPoints(new Point3D(3, 0, 0), new Point3D(-1, 0, 0))).Kind); + } + + [Fact] + public void EqualityAndStrings() + { + var line = Line3D.FromPointAndDirection(new Point3D(4, 1, 0), Vector3.UnitX); + Assert.True(line == Line3D.FromPointAndDirection(new Point3D(-2, 1, 0), -Vector3.UnitX)); + Assert.True(line != Line3D.FromPointAndDirection(new Point3D(0, 2, 0), Vector3.UnitX)); + Assert.True(line.Equals((object) Line3D.FromPointAndDirection(new Point3D(9, 1, 0), Vector3.UnitX))); + Assert.Equal(line.GetHashCode(), Line3D.FromPointAndDirection(new Point3D(1, 1, 0), Vector3.UnitX).GetHashCode()); + Assert.Equal("Line3D [(0, 1, 0) + t·(1, 0, 0)]", line.ToString()); + } +} diff --git a/tests/SharpMath.Tests/Geometry/LineSegment2DTests.cs b/tests/SharpMath.Tests/Geometry/LineSegment2DTests.cs new file mode 100644 index 0000000..fdc42ad --- /dev/null +++ b/tests/SharpMath.Tests/Geometry/LineSegment2DTests.cs @@ -0,0 +1,100 @@ +using SharpMath.Geometry; + +namespace SharpMath.Tests.Geometry; + +public class LineSegment2DTests +{ + private static readonly LineSegment2D Diagonal = new(new Point2D(0, 0), new Point2D(4, 4)); + + [Fact] + public void BasicProperties() + { + Assert.Equal(new Point2D(0, 0), Diagonal.Start); + Assert.Equal(new Point2D(4, 4), Diagonal.End); + Assert.Equal(new Vector2(4, 4), Diagonal.Direction); + Assert.Equal(Math.Sqrt(32), Diagonal.Length, 12); + Assert.Equal(new Point2D(2, 2), Diagonal.Midpoint); + Assert.True(Diagonal.ToLine().ApproximatelyEquals(Line2D.FromSlopeIntercept(1, 0))); + Assert.Throws(() => new LineSegment2D(Point2D.Origin, Point2D.Origin).ToLine()); + } + + [Fact] + public void ContainsDistanceAndClosestPoint() + { + Assert.True(Diagonal.Contains(new Point2D(1, 1))); + Assert.True(Diagonal.Contains(new Point2D(4, 4))); + Assert.False(Diagonal.Contains(new Point2D(5, 5))); + Assert.False(Diagonal.Contains(new Point2D(1, 2))); + Assert.Equal(Math.Sqrt(2), Diagonal.DistanceTo(new Point2D(5, 5)), 12); + Assert.Equal(new Point2D(0, 0), Diagonal.ClosestPoint(new Point2D(-3, -1))); + Assert.True(Diagonal.ClosestPoint(new Point2D(0, 2)).ApproximatelyEquals(new Point2D(1, 1))); + var point = new LineSegment2D(new Point2D(1, 1), new Point2D(1, 1)); + Assert.Equal(new Point2D(1, 1), point.ClosestPoint(new Point2D(5, 5))); + } + + [Fact] + public void Intersect_Segments() + { + var crossing = Diagonal.Intersect(new LineSegment2D(new Point2D(0, 4), new Point2D(4, 0))); + Assert.Equal(IntersectionKind.Point, crossing.Kind); + Assert.True(crossing.Point!.Value.ApproximatelyEquals(new Point2D(2, 2))); + Assert.Null(crossing.Segment); + + var touching = Diagonal.Intersect(new LineSegment2D(new Point2D(4, 4), new Point2D(6, 0))); + Assert.True(touching.Point!.Value.ApproximatelyEquals(new Point2D(4, 4))); + + Assert.Equal(IntersectionKind.None, + Diagonal.Intersect(new LineSegment2D(new Point2D(0, 4), new Point2D(1, 3))).Kind); + Assert.Equal(IntersectionKind.None, + Diagonal.Intersect(new LineSegment2D(new Point2D(0, 1), new Point2D(4, 5))).Kind); + Assert.Equal(IntersectionKind.None, + Diagonal.Intersect(new LineSegment2D(new Point2D(5, 5), new Point2D(6, 6))).Kind); + } + + [Fact] + public void Intersect_CollinearSegments() + { + var overlap = Diagonal.Intersect(new LineSegment2D(new Point2D(6, 6), new Point2D(2, 2))); + Assert.Equal(IntersectionKind.Overlap, overlap.Kind); + Assert.True(overlap.Segment!.Value.ApproximatelyEquals(new LineSegment2D(new Point2D(2, 2), new Point2D(4, 4)))); + + var endToEnd = Diagonal.Intersect(new LineSegment2D(new Point2D(4, 4), new Point2D(6, 6))); + Assert.Equal(IntersectionKind.Point, endToEnd.Kind); + Assert.True(endToEnd.Point!.Value.ApproximatelyEquals(new Point2D(4, 4))); + } + + [Fact] + public void Intersect_DegenerateSegments() + { + var point = new LineSegment2D(new Point2D(1, 1), new Point2D(1, 1)); + Assert.Equal(IntersectionKind.Point, point.Intersect(Diagonal).Kind); + Assert.Equal(IntersectionKind.Point, Diagonal.Intersect(point).Kind); + Assert.Equal(IntersectionKind.None, new LineSegment2D(new Point2D(1, 2), new Point2D(1, 2)).Intersect(Diagonal).Kind); + Assert.Equal(IntersectionKind.None, Diagonal.Intersect(new LineSegment2D(new Point2D(1, 2), new Point2D(1, 2))).Kind); + } + + [Fact] + public void Intersect_Lines() + { + var crossing = Diagonal.Intersect(Line2D.FromGeneralForm(1, 0, -3)); + Assert.True(crossing.Point!.Value.ApproximatelyEquals(new Point2D(3, 3))); + Assert.Equal(IntersectionKind.None, Diagonal.Intersect(Line2D.FromGeneralForm(1, 0, -5)).Kind); + Assert.Equal(IntersectionKind.None, Diagonal.Intersect(Line2D.FromSlopeIntercept(1, 1)).Kind); + var coincident = Diagonal.Intersect(Line2D.FromSlopeIntercept(1, 0)); + Assert.Equal(IntersectionKind.Overlap, coincident.Kind); + Assert.Equal(Diagonal, coincident.Segment); + var point = new LineSegment2D(new Point2D(1, 1), new Point2D(1, 1)); + Assert.Equal(IntersectionKind.Point, point.Intersect(Line2D.FromSlopeIntercept(1, 0)).Kind); + Assert.Equal(IntersectionKind.None, point.Intersect(Line2D.FromSlopeIntercept(1, 1)).Kind); + } + + [Fact] + public void EqualityAndStrings() + { + Assert.True(Diagonal == new LineSegment2D(new Point2D(0, 0), new Point2D(4, 4))); + Assert.True(Diagonal != new LineSegment2D(new Point2D(4, 4), new Point2D(0, 0))); + Assert.True(Diagonal.Equals((object) new LineSegment2D(new Point2D(0, 0), new Point2D(4, 4)))); + Assert.Equal(Diagonal.GetHashCode(), new LineSegment2D(new Point2D(0, 0), new Point2D(4, 4)).GetHashCode()); + Assert.Equal("LineSegment2D [(0, 0) → (4, 4)]", Diagonal.ToString()); + } +} diff --git a/tests/SharpMath.Tests/Geometry/Matrix2x2Tests.cs b/tests/SharpMath.Tests/Geometry/Matrix2x2Tests.cs new file mode 100644 index 0000000..a0408fb --- /dev/null +++ b/tests/SharpMath.Tests/Geometry/Matrix2x2Tests.cs @@ -0,0 +1,118 @@ +using SharpMath.Geometry; + +namespace SharpMath.Tests.Geometry; + +public class Matrix2x2Tests +{ + private static readonly Matrix2x2 Sample = new(8, 3, 4, 2); + + [Fact] + public void ConstructorsAndIndexers_AreRowMajor() + { + Assert.Equal(Sample, new Matrix2x2(new Vector2(8, 3), new Vector2(4, 2))); + Assert.Equal(3, Sample[0, 1]); + Assert.Equal(4, Sample[1, 0]); + Assert.Equal(3, Sample[1]); + Assert.Equal(2, Sample[3]); + Assert.Equal(8, Sample.M11); + Assert.Equal(2, Sample.RowCount); + Assert.Equal(2, Sample.ColumnCount); + Assert.Equal(2, Sample.Dimension); + + // Regression: [0, 2] used to silently return M21. + Assert.Throws(() => Sample[0, 2]); + Assert.Throws(() => Sample[2, 0]); + Assert.Throws(() => Sample[4]); + } + + [Fact] + public void DeterminantTraceAndInverse() + { + Assert.Equal(4, Sample.Determinant); + Assert.Equal(10, Sample.Trace); + + // Regression: Inverse used to return the identity matrix. + var inverse = Sample.Inverse(); + Assert.True(inverse.ApproximatelyEquals(new Matrix2x2(0.5, -0.75, -1, 2))); + Assert.True((Sample * inverse).IsIdentity); + Assert.Equal(new Matrix2x2(2, -3, -4, 8), Sample.Adjugate()); + Assert.Equal(new Matrix2x2(2, -4, -3, 8), Sample.Cofactors()); + } + + [Fact] + public void Inverse_OfSingularMatrixThrows() + { + var singular = new Matrix2x2(1, 2, 2, 4); + Assert.True(singular.IsSingular); + Assert.False(singular.TryInvert(out var inverse)); + Assert.Equal(Matrix2x2.Zero, inverse); + Assert.Throws(() => singular.Inverse()); + } + + [Fact] + public void IsSingular_IsScaleAware() + { + Assert.False((Matrix2x2.Identity * 1e-5).IsSingular); + Assert.True(Matrix2x2.Zero.IsSingular); + } + + [Fact] + public void Predicates() + { + // Regression: IsIdentity used to check M22 == 0. + Assert.True(Matrix2x2.Identity.IsIdentity); + Assert.False(new Matrix2x2(1, 0, 0, 0).IsIdentity); + Assert.False(new Matrix2x2(1, 1, 0, 1).IsIdentity); + + Assert.True(new Matrix2x2(1, 0, 0, 0).IsDiagonal); + Assert.True(Matrix2x2.Zero.IsDiagonal); + Assert.False(Sample.IsDiagonal); + Assert.True(new Matrix2x2(1, 2, 0, 3).IsTriangular); + Assert.False(Sample.IsTriangular); + Assert.True(new Matrix2x2(1, 2, 2, 3).IsSymmetric); + Assert.False(Sample.IsSymmetric); + Assert.True(new Matrix2x2(0, 2, -2, 0).IsSkewSymmetric); + Assert.False(new Matrix2x2(1, 2, -2, 0).IsSkewSymmetric); + Assert.True(Matrix2x2.Rotation(0.7).IsOrthogonal); + Assert.False(Sample.IsOrthogonal); + } + + [Fact] + public void Factories() + { + Assert.True((Matrix2x2.Rotation(Math.PI / 2) * Vector2.UnitX).ApproximatelyEquals(Vector2.UnitY)); + Assert.Equal(new Vector2(2, 3), Matrix2x2.Scaling(2, 3) * Vector2.One); + Assert.Equal(new Matrix2x2(4, 0, 0, 4), Matrix2x2.Scaling(4)); + Assert.Equal(Sample, Matrix2x2.FromMatrix(MatrixMxN.FromMatrix(Sample))); + Assert.Throws(() => Matrix2x2.FromMatrix(Matrix3x3.Identity)); + Assert.Throws(() => Matrix2x2.FromMatrix(null!)); + } + + [Fact] + public void Operators() + { + var other = new Matrix2x2(1, 2, 3, 4); + Assert.Equal(new Matrix2x2(9, 5, 7, 6), Sample + other); + Assert.Equal(new Matrix2x2(7, 1, 1, -2), Sample - other); + Assert.Equal(new Matrix2x2(-8, -3, -4, -2), -Sample); + Assert.Equal(new Matrix2x2(16, 6, 8, 4), Sample * 2); + Assert.Equal(new Matrix2x2(16, 6, 8, 4), 2 * Sample); + Assert.Equal(new Matrix2x2(4, 1.5, 2, 1), Sample / 2); + Assert.Equal(new Matrix2x2(17, 28, 10, 16), Sample * other); + Assert.Equal(new Vector2(11, 6), Sample * Vector2.One); + Assert.Equal(new Matrix2x2(8, 4, 3, 2), Sample.Transpose()); + } + + [Fact] + public void EqualityAndStrings() + { + Assert.True(Sample == new Matrix2x2(8, 3, 4, 2)); + Assert.True(Sample != Matrix2x2.Identity); + Assert.True(Sample.Equals((object) new Matrix2x2(8, 3, 4, 2))); + Assert.False(Sample.Equals((object) 1)); + Assert.Equal(Sample.GetHashCode(), new Matrix2x2(8, 3, 4, 2).GetHashCode()); + Assert.True(Sample.ApproximatelyEquals(new Matrix2x2(8, 3, 4, 2 + 1e-12))); + Assert.False(Sample.ApproximatelyEquals(new Matrix2x2(8, 3, 4, 2.1), 0.01, 0)); + Assert.Equal("Matrix2x2 [[8, 3], [4, 2]]", Sample.ToString()); + } +} diff --git a/tests/SharpMath.Tests/Geometry/Matrix3x3Tests.cs b/tests/SharpMath.Tests/Geometry/Matrix3x3Tests.cs new file mode 100644 index 0000000..e0b3ebb --- /dev/null +++ b/tests/SharpMath.Tests/Geometry/Matrix3x3Tests.cs @@ -0,0 +1,117 @@ +using SharpMath.Geometry; + +namespace SharpMath.Tests.Geometry; + +public class Matrix3x3Tests +{ + private static readonly Matrix3x3 A = new(2, 5, 2, 3, -3, 1, 1, 4, -4); + private static readonly Matrix3x3 B = new(-4, 2.5, 3, 5, 6, 4, 9, 10, -9); + + [Fact] + public void Determinant_UsesClosedForm() + { + Assert.Equal(111, A.Determinant); + Assert.Equal(566.5, B.Determinant); + Assert.Equal(-5, A.Trace); + } + + [Fact] + public void Multiplication_IsRowByColumn() + { + Assert.Equal(new Matrix3x3(35, 55, 8, -18, -0.5, -12, -20, -13.5, 55), A * B); + Assert.Equal(new Vector3(9, 1, 1), A * Vector3.One); + } + + [Fact] + public void RowConstructor_PlacesRowsAsRows() + { + // Regression: the vectors used to be placed as columns. + var matrix = new Matrix3x3(new Vector3(1, 2, 3), new Vector3(4, 5, 6), new Vector3(7, 8, 9)); + Assert.Equal(new Matrix3x3(1, 2, 3, 4, 5, 6, 7, 8, 9), matrix); + Assert.Equal(2, matrix[0, 1]); + Assert.Equal(4, matrix[1, 0]); + Assert.Equal(9, matrix[8]); + Assert.Throws(() => matrix[0, 3]); + Assert.Throws(() => matrix[9]); + } + + [Fact] + public void Inverse_AdjugateAndCofactors() + { + // Regression: Inverse used to return the identity matrix. + Assert.True((A * A.Inverse()).IsIdentity); + Assert.True((A.Inverse() * A).IsIdentity); + Assert.True((A * A.Adjugate()).ApproximatelyEquals(Matrix3x3.Identity * A.Determinant)); + Assert.Equal(A.Adjugate().Transpose(), A.Cofactors()); + + var singular = new Matrix3x3(1, 2, 3, 2, 4, 6, 1, 1, 1); + Assert.True(singular.IsSingular); + Assert.False(singular.TryInvert(out _)); + Assert.Throws(() => singular.Inverse()); + + // Regression: a permutation matrix used to be reported as not invertible. + var permutation = new Matrix3x3(0, 1, 0, 1, 0, 0, 0, 0, 1); + Assert.Equal(permutation, permutation.Inverse()); + + // Regression: small but regular matrices used to count as singular. + Assert.False((Matrix3x3.Identity * 0.001).IsSingular); + } + + [Fact] + public void Predicates() + { + Assert.True(Matrix3x3.Identity.IsIdentity); + // Regression: off-diagonal entries used to be ignored. + Assert.False(new Matrix3x3(1, 5, 0, 0, 1, 0, 0, 0, 1).IsIdentity); + Assert.True(new Matrix3x3(1, 0, 0, 0, 1, 0, 0, 0, 0).IsDiagonal); + Assert.True(new Matrix3x3(1, 3, 4, 0, 2, 7, 0, 0, 3).IsTriangular); + Assert.True(new Matrix3x3(1, 0, 0, 3, 2, 0, 4, 7, 3).IsTriangular); + Assert.False(A.IsTriangular); + Assert.True(new Matrix3x3(1, 2, 3, 2, 5, 6, 3, 6, 9).IsSymmetric); + Assert.False(A.IsSymmetric); + Assert.True(new Matrix3x3(0, 1, -2, -1, 0, 3, 2, -3, 0).IsSkewSymmetric); + Assert.True(Matrix3x3.Rotation(1.2).IsOrthogonal); + Assert.False(A.IsOrthogonal); + } + + [Fact] + public void TwoDimensionalTransformations() + { + Assert.Equal(new Vector2(11, 11), Matrix3x3.Translation(10, 10) * Vector2.One); + Assert.Equal(new Vector2(2, 3), Matrix3x3.Scaling(2, 3) * Vector2.One); + Assert.Equal(new Vector2(2, 2), Matrix3x3.Scaling(2) * Vector2.One); + + // Regression: the old test checked the input vector instead of the rotated one. + var rotated = Matrix3x3.Rotation(Math.PI) * new Vector2(0, 1); + Assert.True(rotated.ApproximatelyEquals(new Vector2(0, -1))); + + // Composition from right to left: scale, then rotate, then translate. + var transform = Matrix3x3.Translation(5, 0) * Matrix3x3.Rotation(Math.PI / 2) * Matrix3x3.Scaling(2); + Assert.True((transform * Vector2.UnitX).ApproximatelyEquals(new Vector2(5, 2))); + + var projective = new Matrix3x3(1, 0, 0, 0, 1, 0, 0, 0, 2); + Assert.Equal(new Vector2(0.5, 0.5), projective * Vector2.One); + } + + [Fact] + public void ElementWiseOperatorsEqualityAndStrings() + { + Assert.Equal(A * 2, A + A); + Assert.Equal(Matrix3x3.Zero, A - A); + Assert.Equal(A * -1, -A); + Assert.Equal(A * 2, 2 * A); + Assert.Equal(A, A * 2 / 2); + Assert.Equal(A, Matrix3x3.FromMatrix(MatrixMxN.FromMatrix(A))); + Assert.Throws(() => Matrix3x3.FromMatrix(Matrix2x2.Identity)); + Assert.True(A == new Matrix3x3(2, 5, 2, 3, -3, 1, 1, 4, -4)); + Assert.True(A != B); + Assert.True(A.Equals((object) new Matrix3x3(2, 5, 2, 3, -3, 1, 1, 4, -4))); + Assert.Equal(A.GetHashCode(), new Matrix3x3(2, 5, 2, 3, -3, 1, 1, 4, -4).GetHashCode()); + Assert.True(A.ApproximatelyEquals(A + Matrix3x3.Identity * 1e-12)); + Assert.True(A.ApproximatelyEquals(A + Matrix3x3.Identity * 0.01, 0.1, 0)); + Assert.Equal(3, A.RowCount); + Assert.Equal(3, A.ColumnCount); + Assert.Equal(3, A.Dimension); + Assert.Equal("Matrix3x3 [[2, 5, 2], [3, -3, 1], [1, 4, -4]]", A.ToString()); + } +} diff --git a/tests/SharpMath.Tests/Geometry/Matrix4x4Tests.cs b/tests/SharpMath.Tests/Geometry/Matrix4x4Tests.cs new file mode 100644 index 0000000..49f1803 --- /dev/null +++ b/tests/SharpMath.Tests/Geometry/Matrix4x4Tests.cs @@ -0,0 +1,165 @@ +using SharpMath.Geometry; + +namespace SharpMath.Tests.Geometry; + +public class Matrix4x4Tests +{ + private static readonly Matrix4x4 A = new(1, 0, 0, 1, 1, 2, 3, 2, 2, 3, 4, 0, 1, 2, -1, -2); + + [Fact] + public void Determinant_UsesClosedForm() + { + Assert.Equal(-24, A.Determinant, 10); + Assert.Equal(1, Matrix4x4.Identity.Determinant); + Assert.Equal(5, A.Trace); + } + + [Fact] + public void Inverse_AdjugateAndCofactors() + { + // Regression: Inverse used to return the identity matrix. + Assert.True((A * A.Inverse()).IsIdentity); + Assert.True((A.Adjugate() * A).ApproximatelyEquals(Matrix4x4.Identity * A.Determinant)); + Assert.Equal(A.Adjugate().Transpose(), A.Cofactors()); + + var singular = new Matrix4x4(1, 2, 3, 4, 2, 4, 6, 8, 0, 1, 0, 1, 1, 0, 1, 0); + Assert.True(singular.IsSingular); + Assert.False(singular.TryInvert(out _)); + Assert.Throws(() => singular.Inverse()); + + var transform = Matrix4x4.Translation(1, 2, 3) * Matrix4x4.Rotation(0.3, 0.5, 0.7) * Matrix4x4.Scaling(2); + Assert.True((transform * transform.Inverse()).IsIdentity); + } + + [Fact] + public void Predicates() + { + Assert.True(Matrix4x4.Identity.IsIdentity); + // Regression: M44 and off-diagonal entries used to be ignored. + Assert.False(new Matrix4x4(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 7).IsIdentity); + Assert.False(Matrix4x4.Translation(1, 0, 0).IsIdentity); + + // Regression: a zero on the diagonal used to make a matrix "not diagonal". + Assert.True(new Matrix4x4(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0).IsDiagonal); + Assert.True(Matrix4x4.Identity.IsDiagonal); + Assert.False(new Matrix4x4(1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1).IsDiagonal); + + Assert.True(new Matrix4x4(1, 3, 4, 7, 0, 2, 2, 3, 0, 0, 3, 9, 0, 0, 0, 8).IsTriangular); + Assert.False(new Matrix4x4(5, 0, 0, 2, 0, 7, 0, 0, 9, 0, 4, 0, 0, 0, 0, 3).IsTriangular); + Assert.True(Matrix4x4.Identity.IsTriangular); + + // Regression: rotations used to be reported as not orthogonal because of exact comparisons. + Assert.True(Matrix4x4.Rotation(0.3, 0.5, 0.7).IsOrthogonal); + Assert.False(A.IsOrthogonal); + Assert.True((A + A.Transpose()).IsSymmetric); + Assert.True((A - A.Transpose()).IsSkewSymmetric); + Assert.False(A.IsSkewSymmetric); + } + + [Fact] + public void AffineTransformations_UseColumnVectors() + { + Assert.Equal(new Vector3(2, 3, 4), Matrix4x4.Translation(1, 2, 3) * Vector3.One); + Assert.Equal(new Vector3(2, 3, 4), Matrix4x4.Translation(new Vector3(1, 2, 3)) * Vector3.One); + Assert.Equal(new Vector3(2, 3, 4), Matrix4x4.Scaling(2, 3, 4) * Vector3.One); + Assert.Equal(new Vector3(5, 5, 5), Matrix4x4.Scaling(5) * Vector3.One); + Assert.True((Matrix4x4.RotationX(Math.PI / 2) * Vector3.UnitY).ApproximatelyEquals(Vector3.UnitZ)); + Assert.True((Matrix4x4.RotationY(Math.PI / 2) * Vector3.UnitZ).ApproximatelyEquals(Vector3.UnitX)); + Assert.True((Matrix4x4.RotationZ(Math.PI / 2) * Vector3.UnitX).ApproximatelyEquals(Vector3.UnitY)); + Assert.Equal(Matrix4x4.RotationX(0.1) * Matrix4x4.RotationY(0.2) * Matrix4x4.RotationZ(0.3), + Matrix4x4.Rotation(0.1, 0.2, 0.3)); + + var composed = Matrix4x4.Translation(0, 0, -5) * Matrix4x4.RotationY(Math.PI / 2); + Assert.True((composed * Vector3.UnitX).ApproximatelyEquals(new Vector3(0, 0, -6))); + } + + [Fact] + public void Perspective_IsRightHandedWithDepthZeroToOne() + { + var projection = Matrix4x4.PerspectiveFieldOfView(Math.PI / 2, 1, 1, 10); + + // Regression: M44 used to stay 1 and the layout was row-vector style. + Assert.Equal(0, projection.M44); + Assert.Equal(-1, projection.M43); + + var near = projection * new Vector3(0, 0, -1); + var far = projection * new Vector3(0, 0, -10); + Assert.Equal(0, near.Z, 12); + Assert.Equal(1, far.Z, 12); + Assert.True((projection * new Vector3(1, 1, -1)).ApproximatelyEquals(new Vector3(1, 1, 0))); + + var fromData = Matrix4x4.PerspectiveFieldOfView(new ProjectionData(100, 100, 1, Math.PI / 2, 1, 10)); + Assert.Equal(projection, fromData); + } + + [Theory] + [InlineData(0, 1, 1, 10)] + [InlineData(Math.PI, 1, 1, 10)] + [InlineData(1, 0, 1, 10)] + [InlineData(1, 1, 0, 10)] + [InlineData(1, 1, 10, 10)] + public void Perspective_ValidatesArguments(double fov, double aspect, double near, double far) => + Assert.Throws(() => Matrix4x4.PerspectiveFieldOfView(fov, aspect, near, far)); + + [Fact] + public void LookAt_KeepsTranslationAndIsOrthonormal() + { + // Regression: the translation of the camera used to be lost. + var view = Matrix4x4.LookAt(new Vector3(0, 0, 5), Vector3.Zero, Vector3.Up); + Assert.True((view * Vector3.Zero).ApproximatelyEquals(new Vector3(0, 0, -5))); + + // Regression: the basis used to be scaled by the length of the up vector. + var scaledUp = Matrix4x4.LookAt(new Vector3(1, 2, 3), new Vector3(4, 0, -1), new Vector3(0, 2, 0)); + var rotationPart = new Matrix3x3(scaledUp.M11, scaledUp.M12, scaledUp.M13, scaledUp.M21, scaledUp.M22, + scaledUp.M23, scaledUp.M31, scaledUp.M32, scaledUp.M33); + Assert.True(rotationPart.IsOrthogonal); + Assert.True((scaledUp * new Vector3(1, 2, 3)).ApproximatelyEquals(Vector3.Zero)); + + Assert.Throws(() => Matrix4x4.LookAt(Vector3.One, Vector3.One, Vector3.Up)); + Assert.Throws(() => Matrix4x4.LookAt(Vector3.Zero, Vector3.Up, Vector3.Up)); + Assert.Throws(() => Matrix4x4.LookAt(Vector3.Zero, Vector3.Forward, Vector3.Zero)); + } + + [Fact] + public void World_PlacesAndOrientsObjects() + { + // Regression: the position used to be lost and the basis was not normalized. + var world = Matrix4x4.World(new Vector3(1, 2, 3), new Vector3(0, 0, -6), new Vector3(0, 12, 0)); + Assert.True((world * Vector3.Zero).ApproximatelyEquals(new Vector3(1, 2, 3))); + Assert.True((world * Vector3.Forward).ApproximatelyEquals(new Vector3(1, 2, 2))); + Assert.True(world.ApproximatelyEquals(Matrix4x4.Translation(1, 2, 3))); + + var rotated = Matrix4x4.World(Vector3.Zero, Vector3.Right, Vector3.Up); + Assert.True((rotated * Vector3.Forward).ApproximatelyEquals(Vector3.Right)); + Assert.Throws(() => Matrix4x4.World(Vector3.Zero, Vector3.Zero, Vector3.Up)); + Assert.Throws(() => Matrix4x4.World(Vector3.Zero, Vector3.Up, Vector3.Up)); + } + + [Fact] + public void ElementWiseOperatorsIndexersEqualityAndStrings() + { + Assert.Equal(A * 2, A + A); + Assert.Equal(Matrix4x4.Zero, A - A); + Assert.Equal(A * -1, -A); + Assert.Equal(2 * A, A * 2); + Assert.Equal(A, A * 4 / 4); + Assert.Equal(new Vector4(2, 8, 9, 0), A * Vector4.One); + Assert.Equal(A, Matrix4x4.FromMatrix(MatrixMxN.FromMatrix(A))); + Assert.Throws(() => Matrix4x4.FromMatrix(Matrix3x3.Identity)); + Assert.Equal(-1, A[3, 2]); + Assert.Equal(-2, A[15]); + Assert.Throws(() => A[4, 0]); + Assert.Throws(() => A[16]); + Assert.Equal(4, A.RowCount); + Assert.Equal(4, A.ColumnCount); + Assert.Equal(4, A.Dimension); + Assert.True(A == new Matrix4x4(1, 0, 0, 1, 1, 2, 3, 2, 2, 3, 4, 0, 1, 2, -1, -2)); + Assert.True(A != Matrix4x4.Identity); + Assert.True(A.Equals((object) new Matrix4x4(1, 0, 0, 1, 1, 2, 3, 2, 2, 3, 4, 0, 1, 2, -1, -2))); + Assert.Equal(A.GetHashCode(), new Matrix4x4(1, 0, 0, 1, 1, 2, 3, 2, 2, 3, 4, 0, 1, 2, -1, -2).GetHashCode()); + Assert.True(A.ApproximatelyEquals(A + Matrix4x4.Identity * 1e-12)); + Assert.Equal(new Matrix4x4(new Vector4(1, 0, 0, 1), new Vector4(1, 2, 3, 2), new Vector4(2, 3, 4, 0), + new Vector4(1, 2, -1, -2)), A); + Assert.StartsWith("Matrix4x4 [[1, 0, 0, 1], [1, 2, 3, 2]", A.ToString()); + } +} diff --git a/tests/SharpMath.Tests/Geometry/MatrixMxNTests.cs b/tests/SharpMath.Tests/Geometry/MatrixMxNTests.cs new file mode 100644 index 0000000..c7f54a0 --- /dev/null +++ b/tests/SharpMath.Tests/Geometry/MatrixMxNTests.cs @@ -0,0 +1,126 @@ +using SharpMath.Geometry; + +namespace SharpMath.Tests.Geometry; + +public class MatrixMxNTests +{ + private static readonly MatrixMxN Rectangular = new(new double[,] { { 1, 2, 3 }, { 4, 5, 6 } }); + + [Fact] + public void Constructors_CopyTheirInput() + { + var values = new double[,] { { 1, 2 }, { 3, 4 } }; + var matrix = new MatrixMxN(values); + values[0, 0] = 99; + Assert.Equal(1, matrix[0, 0]); + Assert.Equal(new MatrixMxN(2, 2, [1, 2, 3, 4]), matrix); + Assert.Equal(2, Rectangular.RowCount); + Assert.Equal(3, Rectangular.ColumnCount); + Assert.False(Rectangular.IsSquare); + Assert.Equal(6, Rectangular[1, 2]); + Assert.Throws(() => Rectangular[2, 0]); + Assert.Throws(() => Rectangular[0, 3]); + Assert.Throws(() => new MatrixMxN(new double[0, 2])); + Assert.Throws(() => new MatrixMxN(2, 2, [1, 2, 3])); + Assert.Throws(() => new MatrixMxN(0, 2, [])); + Assert.Throws(() => MatrixMxN.Zero(1, 0)); + Assert.Throws(() => new MatrixMxN(null!)); + } + + [Fact] + public void Factories_AndConversions() + { + Assert.Equal(new MatrixMxN(2, 2, [1, 0, 0, 1]), MatrixMxN.Identity(2)); + Assert.Equal(new MatrixMxN(1, 2, [0, 0]), MatrixMxN.Zero(1, 2)); + Assert.Equal(MatrixMxN.Identity(4), MatrixMxN.FromMatrix(Matrix4x4.Identity)); + Assert.Equal(new double[,] { { 1, 2, 3 }, { 4, 5, 6 } }, Rectangular.ToArray()); + Assert.Equal(new MatrixMxN(new double[,] { { 1, 4 }, { 2, 5 }, { 3, 6 } }), Rectangular.Transpose()); + } + + [Fact] + public void Arithmetic() + { + Assert.Equal(Rectangular * 2, Rectangular + Rectangular); + Assert.Equal(MatrixMxN.Zero(2, 3), Rectangular - Rectangular); + Assert.Equal(Rectangular * -1, -Rectangular); + Assert.Equal(Rectangular * 2, 2 * Rectangular); + Assert.Equal(Rectangular, Rectangular * 2 / 2); + Assert.Equal(new MatrixMxN(new double[,] { { 14, 32 }, { 32, 77 } }), Rectangular * Rectangular.Transpose()); + Assert.Throws(() => Rectangular * Rectangular); + Assert.Throws(() => Rectangular + MatrixMxN.Identity(2)); + } + + [Fact] + public void DeterminantInverseAndSolve() + { + var matrix = MatrixMxN.FromMatrix(new Matrix4x4(1, 0, 0, 1, 1, 2, 3, 2, 2, 3, 4, 0, 1, 2, -1, -2)); + Assert.Equal(-24, matrix.Determinant(), 10); + Assert.True((matrix * matrix.Inverse()).ApproximatelyEquals(MatrixMxN.Identity(4))); + + var system = new MatrixMxN(new double[,] { { 1, -1, 2 }, { 2, 3, 2 }, { 3, 2, 1 } }); + var solution = system.Solve([6, 11, 8]); + Assert.Equal(1, solution[0], 12); + Assert.Equal(1, solution[1], 12); + Assert.Equal(3, solution[2], 12); + + var multiple = system.Solve(new MatrixMxN(new double[,] { { 6, 12 }, { 11, 22 }, { 8, 16 } })); + Assert.True(multiple.ApproximatelyEquals(new MatrixMxN(new double[,] { { 1, 2 }, { 1, 2 }, { 3, 6 } }))); + + Assert.Throws(() => system.Solve([1, 2])); + Assert.Throws(() => system.Solve(MatrixMxN.Zero(2, 1))); + Assert.Throws(() => Rectangular.Determinant()); + Assert.Throws(() => Rectangular.Inverse()); + } + + [Fact] + public void SingularMatrices() + { + var singular = new MatrixMxN(new double[,] { { 1, 2 }, { 2, 4 } }); + Assert.Equal(0, singular.Determinant()); + Assert.False(singular.TryInvert(out var inverse)); + Assert.Null(inverse); + Assert.Throws(() => singular.Inverse()); + Assert.Throws(() => singular.Solve([1, 2])); + Assert.Throws(() => singular.Solve(MatrixMxN.Identity(2))); + Assert.Equal(0, MatrixMxN.Zero(2, 2).Determinant()); + } + + [Fact] + public void Solve_HandlesSmallButRegularScales() + { + // Regression: an absolute pivot threshold rejected well-conditioned systems at small scales. + var tiny = new MatrixMxN(1, 1, [1e-10]); + Assert.Equal(1, tiny.Solve([1e-10])[0], 12); + + // Partial pivoting keeps precision with a tiny leading entry. + var pivoting = new MatrixMxN(2, 2, [2e-9, 1, 1, 1]); + var solution = pivoting.Solve([1, 2]); + Assert.Equal(1 / (1 - 2e-9), solution[0], 12); + } + + [Fact] + public void Rank() + { + Assert.Equal(2, Rectangular.Rank()); + Assert.Equal(1, new MatrixMxN(new double[,] { { 1, 2 }, { 2, 4 } }).Rank()); + Assert.Equal(0, MatrixMxN.Zero(3, 3).Rank()); + Assert.Equal(3, MatrixMxN.Identity(3).Rank()); + } + + [Fact] + public void EqualityAndStrings() + { + var copy = new MatrixMxN(Rectangular.ToArray()); + Assert.True(Rectangular == copy); + Assert.False(Rectangular != copy); + Assert.True(Rectangular.Equals((object) copy)); + Assert.False(Rectangular.Equals(null)); + Assert.False(Rectangular == null); + Assert.True((MatrixMxN?) null == null); + Assert.False(Rectangular.Equals(Rectangular.Transpose())); + Assert.Equal(Rectangular.GetHashCode(), copy.GetHashCode()); + Assert.True(Rectangular.ApproximatelyEquals(Rectangular + MatrixMxN.Zero(2, 3))); + Assert.False(Rectangular.ApproximatelyEquals(Rectangular.Transpose())); + Assert.Equal("MatrixMxN [[1, 2, 3], [4, 5, 6]]", Rectangular.ToString()); + } +} diff --git a/tests/SharpMath.Tests/Geometry/PointTests.cs b/tests/SharpMath.Tests/Geometry/PointTests.cs new file mode 100644 index 0000000..216a6ca --- /dev/null +++ b/tests/SharpMath.Tests/Geometry/PointTests.cs @@ -0,0 +1,69 @@ +using SharpMath.Geometry; + +namespace SharpMath.Tests.Geometry; + +public class PointTests +{ + [Fact] + public void Point2D_FollowsAffineRules() + { + var p = new Point2D(1, 2); + var q = new Point2D(4, 6); + Assert.Equal(new Vector2(3, 4), q - p); + Assert.Equal(q, p + new Vector2(3, 4)); + Assert.Equal(p, q - new Vector2(3, 4)); + Assert.Equal(5, p.Distance(q)); + Assert.Equal(5, Point2D.Distance(p, q)); + Assert.Equal(25, p.DistanceSquared(q)); + Assert.Equal(25, Point2D.DistanceSquared(p, q)); + Assert.Equal(new Vector2(1, 2), p.ToVector()); + Assert.Equal(Point2D.Origin, new Point2D(0, 0)); + Assert.Equal(1, p[0]); + Assert.Equal(2, p[1]); + Assert.Throws(() => p[2]); + } + + [Fact] + public void Point2D_EqualityAndStrings() + { + var p = new Point2D(1, 2); + Assert.True(p == new Point2D(1, 2)); + Assert.True(p != new Point2D(1, 3)); + + // Like System.Numerics: == follows IEEE (NaN differs from itself), Equals treats NaN as equal. + var nan = new Point2D(double.NaN, 0); + var otherNan = new Point2D(double.NaN, 0); + Assert.False(nan == otherNan); + Assert.True(nan.Equals(otherNan)); + Assert.True(p.Equals((object) new Point2D(1, 2))); + Assert.False(p.Equals((object) new Vector2(1, 2))); + Assert.Equal(p.GetHashCode(), new Point2D(1, 2).GetHashCode()); + Assert.True(new Point2D(0.1 + 0.2, 0).ApproximatelyEquals(new Point2D(0.3, 0))); + Assert.True(p.ApproximatelyEquals(new Point2D(1.05, 2), 0.1, 0)); + Assert.Equal("Point2D [X: 1, Y: 2]", p.ToString()); + } + + [Fact] + public void Point3D_FollowsAffineRules() + { + var p = new Point3D(1, 2, 3); + var q = new Point3D(2, 4, 5); + Assert.Equal(new Vector3(1, 2, 2), q - p); + Assert.Equal(q, p + new Vector3(1, 2, 2)); + Assert.Equal(p, q - new Vector3(1, 2, 2)); + Assert.Equal(3, p.Distance(q)); + Assert.Equal(3, Point3D.Distance(p, q)); + Assert.Equal(9, p.DistanceSquared(q)); + Assert.Equal(9, Point3D.DistanceSquared(p, q)); + Assert.Equal(new Vector3(1, 2, 3), p.ToVector()); + Assert.Equal(Point3D.Origin, new Point3D(0, 0, 0)); + Assert.Equal(3, p[2]); + Assert.Throws(() => p[3]); + Assert.True(p == new Point3D(1, 2, 3)); + Assert.True(p != new Point3D(1, 2, 4)); + Assert.True(p.Equals((object) new Point3D(1, 2, 3))); + Assert.Equal(p.GetHashCode(), new Point3D(1, 2, 3).GetHashCode()); + Assert.True(p.ApproximatelyEquals(new Point3D(1, 2, 3 + 1e-12))); + Assert.Equal("Point3D [X: 1, Y: 2, Z: 3]", p.ToString()); + } +} diff --git a/tests/SharpMath.Tests/Geometry/PolygonTests.cs b/tests/SharpMath.Tests/Geometry/PolygonTests.cs new file mode 100644 index 0000000..c5a5d4a --- /dev/null +++ b/tests/SharpMath.Tests/Geometry/PolygonTests.cs @@ -0,0 +1,113 @@ +using SharpMath.Geometry; + +namespace SharpMath.Tests.Geometry; + +public class PolygonTests +{ + private static readonly Polygon Square = new(new Point2D(0, 0), new Point2D(2, 0), new Point2D(2, 2), new Point2D(0, 2)); + + private static readonly Polygon Pentagon = new(new Point2D(-2, -2), new Point2D(2, -2), new Point2D(4, 1), + new Point2D(0, 2), new Point2D(-4, 1)); + + [Fact] + public void Constructor_ValidatesAndCopies() + { + Assert.Throws(() => new Polygon(new Point2D(0, 1), new Point2D(3, 4))); + Assert.Throws(() => new Polygon((IEnumerable) null!)); + var points = new List { new(0, 0), new(1, 0), new(0, 1) }; + var polygon = new Polygon(points); + points.Add(new Point2D(5, 5)); + Assert.Equal(3, polygon.Points.Count); + } + + [Fact] + public void Area_UsesShoelaceFormula() + { + Assert.Equal(4, Square.Area); + Assert.Equal(2, new Polygon(new Point2D(0, 0), new Point2D(2, 0), new Point2D(1, 2)).Area); + Assert.Equal(22, Pentagon.Area); + + // Regression: polygons that do not contain the origin used to get wrong areas. + Assert.Equal(1, new Polygon(new Point2D(1, 1), new Point2D(2, 1), new Point2D(2, 2), new Point2D(1, 2)).Area); + Assert.Equal(0.5, new Polygon(new Point2D(5, 5), new Point2D(6, 5), new Point2D(5, 6)).Area); + var concave = new Polygon(new Point2D(0, 0), new Point2D(4, 0), new Point2D(4, 4), new Point2D(2, 1), + new Point2D(0, 4)); + Assert.Equal(10, concave.Area); + + Assert.Equal(4, Square.SignedArea); + Assert.Equal(-4, new Polygon(Square.Points.Reverse()).SignedArea); + } + + [Fact] + public void Perimeter() + { + Assert.Equal(8, Square.Perimeter); + Assert.Equal(4 + 2 * Math.Sqrt(13) + 2 * Math.Sqrt(17), Pentagon.Perimeter, 12); + } + + [Fact] + public void Centroid_IsCenterOfMass() + { + // Regression: the centroid of this square used to be (16, 16). + Assert.True(Square.Centroid.ApproximatelyEquals(new Point2D(1, 1))); + Assert.True(new Polygon(new Point2D(0, 0), new Point2D(3, 0), new Point2D(0, 3)).Centroid + .ApproximatelyEquals(new Point2D(1, 1))); + Assert.Equal(new Point2D(1, 1), new Polygon(new Point2D(0, 0), new Point2D(1, 1), new Point2D(2, 2)).Centroid); + } + + [Fact] + public void BoundingBox_DoesNotIncludeOrigin() + { + // Regression: the box used to always include the origin. + var polygon = new Polygon(new Point2D(1, 1), new Point2D(3, 1), new Point2D(2, 4)); + Assert.Equal(new BoundingBox2D(1, 1, 3, 4), polygon.BoundingBox); + } + + [Fact] + public void Contains_Points() + { + Assert.True(Square.Contains(new Point2D(1, 1))); + Assert.True(Square.Contains(new Point2D(2, 1))); + Assert.True(Square.Contains(new Point2D(0, 0))); + Assert.False(Square.Contains(new Point2D(3, 1))); + Assert.False(Square.Contains(new Point2D(-0.1, 1))); + var concave = new Polygon(new Point2D(0, 0), new Point2D(4, 0), new Point2D(4, 4), new Point2D(2, 1), + new Point2D(0, 4)); + Assert.False(concave.Contains(new Point2D(2, 3))); + Assert.True(concave.Contains(new Point2D(1, 1))); + } + + [Fact] + public void Contains_Polygons() + { + var inner = new Polygon(new Point2D(0.5, 0.5), new Point2D(1.5, 0.5), new Point2D(1, 1.5)); + Assert.True(Square.Contains(inner)); + Assert.True(Square.Contains(Square)); + Assert.False(inner.Contains(Square)); + + var concave = new Polygon(new Point2D(0, 0), new Point2D(4, 0), new Point2D(4, 4), new Point2D(2, 1), + new Point2D(0, 4)); + var bridge = new Polygon(new Point2D(0.5, 3), new Point2D(3.5, 3), new Point2D(3.5, 3.2), new Point2D(0.5, 3.2)); + Assert.False(concave.Contains(bridge)); + Assert.Throws(() => Square.Contains((Polygon) null!)); + } + + [Fact] + public void Equality_DoesNotRecurse() + { + // Regression: comparing two distinct polygons with == used to overflow the stack. + var other = new Polygon(new Point2D(0, 0), new Point2D(1, 0), new Point2D(0, 1)); + Assert.False(Square == other); + Assert.True(Square != other); + Assert.True(Square == new Polygon(Square.Points)); + Assert.False(Square == null); + Assert.False(null == Square); + Assert.True((Polygon?) null == null); + Assert.True(Square.Equals((object) new Polygon(Square.Points))); + Assert.False(Square.Equals((object?) null)); + + // Regression: equal polygons used to have different hash codes. + Assert.Equal(Square.GetHashCode(), new Polygon(Square.Points).GetHashCode()); + Assert.Equal("Polygon [(0, 0), (2, 0), (2, 2), (0, 2)]", Square.ToString()); + } +} diff --git a/tests/SharpMath.Tests/Geometry/ProjectionDataTests.cs b/tests/SharpMath.Tests/Geometry/ProjectionDataTests.cs new file mode 100644 index 0000000..26c2271 --- /dev/null +++ b/tests/SharpMath.Tests/Geometry/ProjectionDataTests.cs @@ -0,0 +1,41 @@ +using SharpMath.Geometry; + +namespace SharpMath.Tests.Geometry; + +public class ProjectionDataTests +{ + [Fact] + public void Default_HasDocumentedValues() + { + var data = ProjectionData.Default; + Assert.Equal(100, data.Width); + Assert.Equal(100, data.Height); + Assert.Equal(16.0 / 9.0, data.AspectRatio); + Assert.Equal(Math.PI / 3, data.FieldOfView); + Assert.Equal(1, data.NearPlane); + Assert.Equal(100, data.FarPlane); + Assert.Equal(99, data.Depth); + } + + [Theory] + [InlineData(-1, 1, 1, 1, 1, 2)] + [InlineData(1, double.NaN, 1, 1, 1, 2)] + [InlineData(1, 1, 0, 1, 1, 2)] + [InlineData(1, 1, double.PositiveInfinity, 1, 1, 2)] + [InlineData(1, 1, 1, 0, 1, 2)] + [InlineData(1, 1, 1, 4, 1, 2)] + [InlineData(1, 1, 1, 1, -1, 2)] + [InlineData(1, 1, 1, 1, 2, 1)] + public void Constructor_ValidatesArguments(double width, double height, double aspect, double fov, double near, + double far) => + Assert.Throws(() => new ProjectionData(width, height, aspect, fov, near, far)); + + [Fact] + public void Equality() + { + Assert.True(ProjectionData.Default == ProjectionData.Default); + Assert.True(ProjectionData.Default != new ProjectionData(1, 1, 1, 1, 1, 2)); + Assert.True(ProjectionData.Default.Equals((object) ProjectionData.Default)); + Assert.Equal(ProjectionData.Default.GetHashCode(), ProjectionData.Default.GetHashCode()); + } +} diff --git a/tests/SharpMath.Tests/Geometry/ReviewRegressionTests.cs b/tests/SharpMath.Tests/Geometry/ReviewRegressionTests.cs new file mode 100644 index 0000000..235edf3 --- /dev/null +++ b/tests/SharpMath.Tests/Geometry/ReviewRegressionTests.cs @@ -0,0 +1,172 @@ +using SharpMath.Geometry; + +namespace SharpMath.Tests.Geometry; + +/// +/// Regression tests for numerical problems found in the review of 2.0: scale dependence, cancellation and +/// degenerate inputs. +/// +public class ReviewRegressionTests +{ + [Fact] + public void Transformations_WithLargeTranslationsOrSmallScales_AreInvertible() + { + Matrix4x4[] transformations = + [ + Matrix4x4.Translation(178, 0, 0), + Matrix4x4.Translation(1e6, -2e6, 3e6), + Matrix4x4.LookAt(new Vector3(1e4, 2e3, -5e3), Vector3.Zero, Vector3.Up), + Matrix4x4.Scaling(0.001), + Matrix4x4.Translation(500, 0, 0) * Matrix4x4.Rotation(0.3, 0.2, 0.1) * Matrix4x4.Scaling(1e-3) + ]; + foreach (var transformation in transformations) + { + Assert.False(transformation.IsSingular); + Assert.True(transformation.TryInvert(out var inverse)); + Assert.True((transformation * inverse).ApproximatelyEquals(Matrix4x4.Identity, 1e-9, 1e-9)); + } + + Assert.False(Matrix3x3.Translation(1000, 0).IsSingular); + Assert.Equal(new Vector2(0, 5), Matrix3x3.Translation(1000, 0).Inverse() * new Vector2(1000, 5)); + var shear = new Matrix2x2(1, 1e5, 0, 1); + Assert.False(shear.IsSingular); + Assert.Equal(new Matrix2x2(1, -1e5, 0, 1), shear.Inverse()); + } + + [Fact] + public void SingularMatrices_AreStillDetected() + { + Assert.True(new Matrix4x4(1, 2, 3, 4, 2, 4, 6, 8, 1, 1, 1, 1, 0, 1, 0, 1).IsSingular); + Assert.True(new Matrix3x3(1, 2, 3, 4, 5, 6, 7, 8, 9).IsSingular); + Assert.True((Matrix4x4.Identity with { M44 = 0 }).IsSingular); + Assert.True(new Matrix2x2(double.NaN, 0, 0, 1).IsSingular); + } + + [Fact] + public void StructuralPredicates_DoNotDependOnTheScale() + { + Assert.False(new Matrix2x2(1e-15, 1e-15, 1e-15, 1e-15).IsDiagonal); + Assert.True(new Matrix2x2(1e-15, 0, 0, 2e-15).IsDiagonal); + Assert.False(new Matrix2x2(1e-15, 2e-15, 0, 1e-15).IsSymmetric); + Assert.True(new Matrix3x3(1e12, 1, 0, 0, 1e12, 0, 0, 0, 1e12).IsDiagonal); + } + + [Fact] + public void MatrixMxN_Determinant_IsTheProductOfPivots() + { + var illConditioned = new MatrixMxN(2, 2, [1, 0, 0, 1e-17]); + Assert.Equal(1e-17, illConditioned.Determinant()); + Assert.False(illConditioned.TryInvert(out _)); + Assert.Equal(0, new MatrixMxN(2, 2, [1, 2, 2, 4]).Determinant()); + Assert.Equal(-2, new MatrixMxN(2, 2, [0, 1, 2, 0]).Determinant()); + } + + [Fact] + public void VectorPredicates_DoNotDependOnTheScale() + { + var tinyX = new Vector2(1e-6, 0); + var tinyY = new Vector2(0, 1e-6); + Assert.False(tinyX.IsParallelTo(tinyY)); + Assert.True(tinyX.IsOrthogonalTo(tinyY)); + Assert.False(tinyX.IsOrthogonalTo(tinyX)); + Assert.True(tinyX.IsParallelTo(tinyX * -3)); + Assert.False(new Vector3(1e-7, 0, 0).IsParallelTo(new Vector3(1e-7, 1e-9, 0))); + Assert.False(new Vector4(1e-7, 0, 0, 0).IsParallelTo(new Vector4(0, 1e-7, 0, 0))); + Assert.True(new Vector3(1e200, 1e200, 0).IsParallelTo(new Vector3(2e200, 2e200, 0))); + } + + [Fact] + public void Angle_HandlesTinyAndHugeVectors() + { + Assert.Equal(0, new Vector2(1e-200, 0).Angle(new Vector2(3e-200, 0))); + Assert.Equal(0, new Vector3(1e100, 1e100, 0).Angle(new Vector3(2e100, 2e100, 0)), 12); + Assert.Equal(Math.PI / 2, new Vector4(1e300, 0, 0, 0).Angle(new Vector4(0, 0, 0, 1e300)), 12); + Assert.True(Tolerance.AreEqual(1e-12, new Vector3(1, 0, 0).Angle(new Vector3(1, 1e-12, 0)), 0, 1e-6)); + Assert.Throws(() => new Vector4(0, 0, 0, 0).Angle(Vector4.UnitX)); + } + + [Fact] + public void NearlyParallel3DLines_HaveFiniteClosestPoints() + { + // Regression: 1 − (d·e)² cancelled to 0 for angles around 1e-8 rad, giving NaN. + var first = Line3D.FromPointAndDirection(Point3D.Origin, Vector3.UnitX); + var second = Line3D.FromPointAndDirection(Point3D.Origin, new Vector3(1, 5e-9, 0)); + var (onFirst, onSecond) = first.ClosestPoints(second); + Assert.True(double.IsFinite(onFirst.X) && double.IsFinite(onSecond.Y)); + Assert.Equal(0, first.DistanceTo(second), 9); + Assert.Equal(IntersectionKind.Point, first.Intersect(second).Kind); + + var offset = Line3D.FromPointAndDirection(new Point3D(0, 0, 1), new Vector3(1, 1e-8, 0)); + Assert.Equal(1, first.DistanceTo(offset), 6); + Assert.Equal(IntersectionKind.Skew, first.Intersect(offset).Kind); + } + + [Fact] + public void TinySegments_BehaveLikeLargeOnes() + { + // Regression: disjoint segments of length 1e-6 were reported to intersect. + var horizontal = new LineSegment2D(new Point2D(0, 0), new Point2D(1e-6, 0)); + var vertical = new LineSegment2D(new Point2D(5e-7, 1e-7), new Point2D(5e-7, 2e-7)); + Assert.Equal(IntersectionKind.None, horizontal.Intersect(vertical).Kind); + var crossing = new LineSegment2D(new Point2D(5e-7, -1e-7), new Point2D(5e-7, 1e-7)); + Assert.True(horizontal.Intersect(crossing).Point!.Value.ApproximatelyEquals(new Point2D(5e-7, 0), 1e-15, 1e-9)); + + // Regression: segments shorter than about 1e-12 threw from ToLine and Intersect(Line2D). + var minute = new LineSegment2D(new Point2D(0, 0), new Point2D(1e-13, 1e-13)); + Assert.True(minute.ToLine().IsParallelTo(Line2D.FromSlopeIntercept(1, 0))); + Assert.Equal(IntersectionKind.Overlap, minute.Intersect(Line2D.FromSlopeIntercept(1, 0)).Kind); + } + + [Fact] + public void PolygonContainment_ChecksEveryPieceOfAnEdge() + { + // A U shape; the edge from (1, 1.5) to (4, 6) leaves through the notch corner (2, 3) and ends at the corner (4, 6). + var u = new Polygon(new Point2D(0, 0), new Point2D(6, 0), new Point2D(6, 6), new Point2D(4, 6), new Point2D(4, 3), + new Point2D(2, 3), new Point2D(2, 6), new Point2D(0, 6)); + Assert.False(u.Contains(new Polygon(new Point2D(1, 1.5), new Point2D(4, 6), new Point2D(5, 1)))); + + // Touching the border from inside, along the bottom of the notch, is allowed. + Assert.True(u.Contains(new Polygon(new Point2D(1, 1), new Point2D(5, 1), new Point2D(5, 3), new Point2D(1, 3)))); + Assert.True(u.Contains(u)); + } + + [Fact] + public void ApproximateEqualityOverloads() + { + Assert.True(Line2D.FromSlopeIntercept(1, 0).ApproximatelyEquals(Line2D.FromSlopeIntercept(1.001, 0), 0.01, 0)); + Assert.True(Line3D.FromPointAndDirection(Point3D.Origin, Vector3.UnitX) + .ApproximatelyEquals(Line3D.FromPointAndDirection(new Point3D(0, 0.001, 0), Vector3.UnitX), 0.01, 0)); + var segment = new LineSegment2D(Point2D.Origin, new Point2D(1, 1)); + Assert.True(segment.ApproximatelyEquals(segment with { End = new Point2D(1.001, 1) }, 0.01, 0)); + Assert.False(segment.ApproximatelyEquals(segment with { End = new Point2D(1.001, 1) })); + var box = new BoundingBox2D(0, 0, 1, 1); + Assert.True(box.ApproximatelyEquals(new BoundingBox2D(0, 0, 1 + 1e-12, 1))); + Assert.True(box.ApproximatelyEquals(new BoundingBox2D(0, 0, 1.05, 1), 0.1, 0)); + var triangle = new Polygon(Point2D.Origin, new Point2D(1, 0), new Point2D(0, 1)); + Assert.True(triangle.ApproximatelyEquals(new Polygon(Point2D.Origin, new Point2D(1 + 1e-12, 0), new Point2D(0, 1)))); + Assert.False(triangle.ApproximatelyEquals(new Polygon(Point2D.Origin, new Point2D(1, 0), new Point2D(0, 1), new Point2D(-1, 0)))); + Assert.True(triangle.ApproximatelyEquals(new Polygon(Point2D.Origin, new Point2D(1.05, 0), new Point2D(0, 1)), 0.1, 0)); + Assert.Throws(() => triangle.ApproximatelyEquals(null!)); + } + + [Fact] + public void EqualityOperators_FollowIeee_WhileEqualsTreatsNaNAsEqual() + { + var nanLine = Line2D.FromGeneralForm(1, 0, 0) with { }; + Assert.True(nanLine == Line2D.FromGeneralForm(1, 0, 0)); + var nanBox = new BoundingBox2D(0, 0, 1, 1); + Assert.True(nanBox == new BoundingBox2D(0, 0, 1, 1)); + var nanMatrix = new MatrixMxN(1, 1, [double.NaN]); + var otherNanMatrix = new MatrixMxN(1, 1, [double.NaN]); + Assert.False(nanMatrix == otherNanMatrix); + Assert.True(nanMatrix.Equals(otherNanMatrix)); + var nanPolygon = new Polygon(new Point2D(double.NaN, 0), new Point2D(1, 0), new Point2D(0, 1)); + var otherNanPolygon = new Polygon(new Point2D(double.NaN, 0), new Point2D(1, 0), new Point2D(0, 1)); + Assert.False(nanPolygon == otherNanPolygon); + Assert.True(nanPolygon.Equals(otherNanPolygon)); + Assert.False(nanPolygon == new Polygon(new Point2D(double.NaN, 0), new Point2D(1, 0), new Point2D(0, 1), new Point2D(1, 1))); + var segment = new LineSegment2D(new Point2D(double.NaN, 0), Point2D.Origin); + Assert.False(segment == new LineSegment2D(new Point2D(double.NaN, 0), Point2D.Origin)); + Assert.True(ProjectionData.Default == ProjectionData.Default); + } +} diff --git a/tests/SharpMath.Tests/Geometry/Vector2Tests.cs b/tests/SharpMath.Tests/Geometry/Vector2Tests.cs new file mode 100644 index 0000000..d48fe2a --- /dev/null +++ b/tests/SharpMath.Tests/Geometry/Vector2Tests.cs @@ -0,0 +1,197 @@ +using SharpMath.Geometry; + +namespace SharpMath.Tests.Geometry; + +public class Vector2Tests +{ + [Fact] + public void Constructors_SetComponents() + { + var vector = new Vector2(3, 4); + Assert.Equal(3, vector.X); + Assert.Equal(4, vector.Y); + Assert.Equal(new Vector2(1, 2), new Vector2(new Point2D(1, 2))); + Assert.Equal(2, vector.Dimension); + } + + [Fact] + public void StaticVectors_HaveExpectedValues() + { + Assert.Equal(new Vector2(0, 0), Vector2.Zero); + Assert.Equal(new Vector2(1, 1), Vector2.One); + Assert.Equal(new Vector2(1, 0), Vector2.UnitX); + Assert.Equal(new Vector2(0, 1), Vector2.UnitY); + Assert.Equal(new Vector2(0, 1), Vector2.Up); + Assert.Equal(new Vector2(0, -1), Vector2.Down); + Assert.Equal(new Vector2(-1, 0), Vector2.Left); + Assert.Equal(new Vector2(1, 0), Vector2.Right); + } + + [Fact] + public void Indexer_ReturnsComponentsAndValidatesIndex() + { + var vector = new Vector2(10, 5); + Assert.Equal(10, vector[0]); + Assert.Equal(5, vector[1]); + Assert.Throws(() => vector[2]); + Assert.Throws(() => vector[-1]); + } + + [Fact] + public void Length_IsEuclidean() + { + var vector = new Vector2(3, 4); + Assert.Equal(5, vector.Length); + Assert.Equal(25, vector.LengthSquared); + } + + [Fact] + public void IsZero_AndIsNormalized_UseTolerance() + { + Assert.True(new Vector2(1e-13, -1e-13).IsZero); + Assert.False(new Vector2(1e-3, 0).IsZero); + Assert.True(new Vector2(0.6, 0.8).IsNormalized); + Assert.False(new Vector2(1, 1).IsNormalized); + } + + [Fact] + public void Normalize_ReturnsUnitVector() + { + Assert.True(new Vector2(3, 4).Normalize().ApproximatelyEquals(new Vector2(0.6, 0.8))); + Assert.True(new Vector2(1e-200, 0).Normalize().ApproximatelyEquals(Vector2.UnitX)); + Assert.True(new Vector2(1e200, 1e200).Normalize().IsNormalized); + } + + [Fact] + public void Normalize_ThrowsForZeroVector() => + Assert.Throws(() => Vector2.Zero.Normalize()); + + [Fact] + public void DotAndCross_AreComputed() + { + Assert.Equal(0, Vector2.UnitX.Dot(Vector2.UnitY)); + Assert.Equal(20, Vector2.Dot(new Vector2(2, 4), new Vector2(4, 3))); + Assert.Equal(1, Vector2.UnitX.Cross(Vector2.UnitY)); + Assert.Equal(-10, Vector2.Cross(new Vector2(2, 4), new Vector2(3, 1))); + } + + [Fact] + public void Angle_IsComputedAndClamped() + { + Assert.Equal(Math.PI / 2, Vector2.UnitX.Angle(Vector2.UnitY), 12); + Assert.Equal(Math.PI, Vector2.Angle(Vector2.UnitX, -Vector2.UnitX), 12); + + // Regression: parallel vectors used to produce NaN because the cosine slightly exceeded 1. + var vector = new Vector2(0.1, 0.7); + Assert.Equal(0, vector.Angle(vector * 3), 6); + } + + [Fact] + public void Angle_ThrowsForZeroVector() => + Assert.Throws(() => Vector2.Zero.Angle(Vector2.UnitX)); + + [Fact] + public void Distance_IsComputed() + { + Assert.Equal(5, new Vector2(1, 1).Distance(new Vector2(4, 5))); + Assert.Equal(5, Vector2.Distance(new Vector2(1, 1), new Vector2(4, 5))); + Assert.Equal(25, new Vector2(1, 1).DistanceSquared(new Vector2(4, 5))); + Assert.Equal(25, Vector2.DistanceSquared(new Vector2(1, 1), new Vector2(4, 5))); + } + + [Fact] + public void ParallelogramArea_IsAbsoluteCross() => + Assert.Equal(10, new Vector2(2, 4).ParallelogramArea(new Vector2(3, 1))); + + [Fact] + public void Perpendicular_RotatesCounterclockwise() => + Assert.Equal(new Vector2(-2, 1), new Vector2(1, 2).Perpendicular); + + [Fact] + public void Lerp_ClampsAndLerpUnclamped_Extrapolates() + { + var a = new Vector2(0, 0); + var b = new Vector2(10, 20); + Assert.Equal(new Vector2(5, 10), Vector2.Lerp(a, b, 0.5)); + Assert.Equal(b, Vector2.Lerp(a, b, 2)); + Assert.Equal(a, Vector2.Lerp(a, b, -1)); + Assert.Equal(new Vector2(20, 40), Vector2.LerpUnclamped(a, b, 2)); + } + + [Fact] + public void MoveTowards_ClampsAtTargetAndHandlesEqualPoints() + { + var source = new Vector2(0, 0); + var target = new Vector2(3, 4); + Assert.Equal(new Vector2(0.6, 0.8), Vector2.MoveTowards(source, target, 1)); + Assert.Equal(target, Vector2.MoveTowards(source, target, 10)); + + // Regression: equal points produced NaN. + Assert.Equal(target, Vector2.MoveTowards(target, target, 1)); + } + + [Fact] + public void ParallelismAndOrthogonality_DoNotDivideByComponents() + { + // Regression: zero components produced NaN and "not parallel". + Assert.True(new Vector2(1, 0).IsParallelTo(new Vector2(2, 0))); + Assert.True(new Vector2(1, 0).IsParallelTo(new Vector2(-4, 0))); + Assert.False(new Vector2(1, 0).IsParallelTo(new Vector2(1, 1))); + Assert.True(Vector2.Zero.IsParallelTo(Vector2.UnitX)); + + Assert.True(Vector2.UnitX.IsOrthogonalTo(Vector2.UnitY)); + Assert.False(Vector2.UnitX.IsOrthogonalTo(Vector2.One)); + Assert.True(Vector2.Zero.IsOrthogonalTo(Vector2.UnitX)); + + Assert.True(Vector2.UnitX.IsOrthonormalTo(Vector2.UnitY)); + Assert.False(Vector2.UnitX.IsOrthonormalTo(new Vector2(0, 2))); + } + + [Fact] + public void Transform_UsesColumnVectors() + { + Assert.True(Vector2.Transform(Vector2.UnitX, Matrix2x2.Rotation(Math.PI / 2)).ApproximatelyEquals(Vector2.UnitY)); + + // Regression (issue #6): translations are applied in homogeneous coordinates. + Assert.Equal(new Vector2(11, 11), Vector2.Transform(Vector2.One, Matrix3x3.Translation(10, 10))); + } + + [Fact] + public void Operators_ComputeComponentWise() + { + var a = new Vector2(1, 2); + var b = new Vector2(3, 5); + Assert.Equal(new Vector2(4, 7), a + b); + Assert.Equal(new Vector2(-2, -3), a - b); + Assert.Equal(new Vector2(-1, -2), -a); + Assert.Equal(new Vector2(2, 4), a * 2); + Assert.Equal(new Vector2(2, 4), 2 * a); + Assert.Equal(new Vector2(0.5, 1), a / 2); + } + + [Fact] + public void Equality_IsExactAndApproximatelyEquals_IsTolerant() + { + var a = new Vector2(0.1 + 0.2, 0); + var b = new Vector2(0.3, 0); + Assert.False(a == b); + Assert.True(a != b); + Assert.False(a.Equals(b)); + Assert.True(a.ApproximatelyEquals(b)); + Assert.False(a.ApproximatelyEquals(new Vector2(0.31, 0))); + Assert.True(a.ApproximatelyEquals(new Vector2(0.31, 0), 0.1, 0)); + + Assert.True(new Vector2(1, 2).Equals((object) new Vector2(1, 2))); + Assert.False(new Vector2(1, 2).Equals((object) new Vector3(1, 2, 0))); + Assert.Equal(new Vector2(1, 2).GetHashCode(), new Vector2(1, 2).GetHashCode()); + Assert.Equal(new Vector2(0.0, 0).GetHashCode(), new Vector2(-0.0, 0).GetHashCode()); + } + + [Fact] + public void Conversions_AndStrings() + { + Assert.Equal(new Point2D(1, 2), new Vector2(1, 2).ToPoint()); + Assert.Equal("Vector2 {X: 1.5, Y: -2}", new Vector2(1.5, -2).ToString()); + Assert.Equal(@"\left( \begin{array}{c} 1 \\ 6 \end{array} \right)", new Vector2(1, 6).ToLaTeXString()); + } +} diff --git a/tests/SharpMath.Tests/Geometry/Vector3Tests.cs b/tests/SharpMath.Tests/Geometry/Vector3Tests.cs new file mode 100644 index 0000000..a6f719d --- /dev/null +++ b/tests/SharpMath.Tests/Geometry/Vector3Tests.cs @@ -0,0 +1,153 @@ +using SharpMath.Geometry; + +namespace SharpMath.Tests.Geometry; + +public class Vector3Tests +{ + [Fact] + public void Constructors_SetComponents() + { + Assert.Equal(new Vector3(1, 2, 3), new Vector3(new Vector2(1, 2), 3)); + Assert.Equal(new Vector3(1, 2, 3), new Vector3(new Point3D(1, 2, 3))); + Assert.Equal(3, Vector3.One.Dimension); + } + + [Fact] + public void StaticVectors_FollowRightHandedConvention() + { + Assert.Equal(new Vector3(0, 0, -1), Vector3.Forward); + Assert.Equal(new Vector3(0, 0, 1), Vector3.Back); + Assert.Equal(new Vector3(0, 1, 0), Vector3.Up); + Assert.Equal(new Vector3(0, -1, 0), Vector3.Down); + Assert.Equal(new Vector3(-1, 0, 0), Vector3.Left); + Assert.Equal(new Vector3(1, 0, 0), Vector3.Right); + Assert.Equal(Vector3.UnitZ, Vector3.UnitX.Cross(Vector3.UnitY)); + Assert.Equal(Vector3.Zero, default); + Assert.Equal(new Vector3(1, 1, 1), Vector3.One); + Assert.Equal(new Vector3(0, 1, 0), Vector3.UnitY); + } + + [Fact] + public void Indexer_ReturnsComponentsAndValidatesIndex() + { + var vector = new Vector3(10, 5, 3); + Assert.Equal(10, vector[0]); + Assert.Equal(5, vector[1]); + Assert.Equal(3, vector[2]); + Assert.Throws(() => vector[3]); + } + + [Fact] + public void LengthAndDistance() + { + Assert.Equal(13, new Vector3(13, 0, 0).Length); + Assert.Equal(3, new Vector3(1, 2, 2).Length); + Assert.Equal(Math.Sqrt(30), new Vector3(10, 5, 3).Distance(new Vector3(5, 6, 1))); + Assert.Equal(Math.Sqrt(30), Vector3.Distance(new Vector3(10, 5, 3), new Vector3(5, 6, 1))); + Assert.Equal(30, Vector3.DistanceSquared(new Vector3(10, 5, 3), new Vector3(5, 6, 1))); + Assert.Equal(30, new Vector3(10, 5, 3).DistanceSquared(new Vector3(5, 6, 1))); + } + + [Fact] + public void Products() + { + Assert.Equal(8, new Vector3(2, 3, 1).Dot(new Vector3(-1, 2, 4))); + Assert.Equal(0, Vector3.Dot(Vector3.Forward, Vector3.Up)); + Assert.Equal(new Vector3(-15, 1, 10), new Vector3(1, -5, 2).Cross(new Vector3(2, 0, 3))); + Assert.Equal(new Vector3(-15, 1, 10), Vector3.Cross(new Vector3(1, -5, 2), new Vector3(2, 0, 3))); + // Behaviour change: the old implementation returned the absolute value; a·(b×c) = det([a; b; c]) is signed. + Assert.Equal(-31, Vector3.ScalarTripleProduct(new Vector3(2, 0, 5), new Vector3(-1, 5, -2), new Vector3(2, 1, 2))); + Assert.Equal(Math.Sqrt(525), new Vector3(3, 4, 4).ParallelogramArea(new Vector3(1, -2, 3)), 12); + } + + [Fact] + public void AngleNormalizeAndPredicates() + { + Assert.Equal(Math.PI / 2, Vector3.Angle(Vector3.UnitX, Vector3.UnitZ), 12); + Assert.True(new Vector3(1, 2, 2).Normalize().ApproximatelyEquals(new Vector3(1 / 3.0, 2 / 3.0, 2 / 3.0))); + Assert.Throws(() => Vector3.Zero.Normalize()); + Assert.Throws(() => Vector3.Zero.Angle(Vector3.UnitX)); + + Assert.True(new Vector3(2, 3, 3).IsParallelTo(new Vector3(4, 6, 6))); + // Regression: zero components produced NaN and "not parallel". + Assert.True(new Vector3(0, 1, 3).IsParallelTo(new Vector3(0, 2, 6))); + Assert.True(new Vector3(1, 0, 0).IsParallelTo(new Vector3(-4, 0, 0))); + Assert.False(new Vector3(0, 1, 3).IsParallelTo(new Vector3(0, 3, 2))); + + Assert.True(Vector3.Forward.IsOrthogonalTo(Vector3.Up)); + Assert.False(Vector3.Forward.IsOrthogonalTo(Vector3.Back)); + Assert.True(Vector3.Zero.IsOrthogonalTo(Vector3.UnitX)); + Assert.True(Vector3.Back.IsOrthonormalTo(Vector3.Down)); + Assert.False(Vector3.Forward.IsOrthonormalTo(new Vector3(2, 3, 2))); + Assert.True(new Vector3(0, 0.6, 0.8).IsNormalized); + Assert.True(new Vector3(0, 0, 1e-13).IsZero); + } + + [Fact] + public void InterpolationAndMovement() + { + Assert.Equal(new Vector3(4, 8, 9), Vector3.Lerp(new Vector3(2, 6, 8), new Vector3(6, 10, 10), 0.5)); + Assert.Equal(new Vector3(10.5, 4, 8), Vector3.Lerp(new Vector3(13, 2, 9), new Vector3(3, 10, 5), 0.25)); + Assert.Equal(new Vector3(2, 0, 0), Vector3.LerpUnclamped(Vector3.Zero, Vector3.UnitX, 2)); + Assert.Equal(new Vector3(1, 0, 0), Vector3.MoveTowards(Vector3.Zero, new Vector3(5, 0, 0), 1)); + Assert.Equal(Vector3.One, Vector3.MoveTowards(Vector3.One, Vector3.One, 1)); + Assert.Equal(new Vector3(5, 0, 0), Vector3.MoveTowards(Vector3.Zero, new Vector3(5, 0, 0), 9)); + } + + [Fact] + public void Transform_AppliesMatricesAndSinglePerspectiveDivide() + { + Assert.Equal(new Vector3(2, 4, 6), Vector3.Transform(new Vector3(1, 2, 3), Matrix3x3.Identity * 2)); + Assert.Equal(new Vector3(2, 3, 4), Vector3.Transform(new Vector3(1, 2, 3), Matrix4x4.Translation(1, 1, 1))); + + // Regression: Z used to be divided by W twice. + var halve = new Matrix4x4(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 2); + Assert.Equal(new Vector3(1, 1, 1), Vector3.Transform(new Vector3(2, 2, 2), halve)); + } + + [Fact] + public void With_CreatesModifiedCopies() + { + var vector = new Vector3(1, 2, 3); + Assert.Equal(new Vector3(1, 2, 0), vector with { Z = 0 }); + Assert.Equal(new Vector3(1, 2, 3), vector); + Assert.Equal(new Point3D(0, 2, 3), new Point3D(1, 2, 3) with { X = 0 }); + Assert.Equal(new Vector4(1, 2, 3, 1), new Vector4(1, 2, 3, 4) with { W = 1 }); + Assert.Equal(new Matrix4x4(1, 0, 0, 5, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1), Matrix4x4.Identity with { M14 = 5 }); + Assert.Equal(new Matrix3x3(9, 0, 0, 0, 1, 0, 0, 0, 1), Matrix3x3.Identity with { M11 = 9 }); + Assert.Equal(new Matrix2x2(1, 2, 0, 1), Matrix2x2.Identity with { M12 = 2 }); + Assert.Equal(new Vector2(0, 1), Vector2.One with { X = 0 }); + Assert.Equal(new Point2D(1, 0), new Point2D(1, 1) with { Y = 0 }); + var segment = new LineSegment2D(Point2D.Origin, new Point2D(1, 1)) with { End = new Point2D(2, 2) }; + Assert.Equal(new Point2D(2, 2), segment.End); + } + + [Fact] + public void FromHomogeneous_DividesByW() + { + Assert.Equal(new Vector3(1, 2, 3), Vector3.FromHomogeneous(new Vector4(2, 4, 6, 2))); + Assert.Throws(() => Vector3.FromHomogeneous(new Vector4(1, 2, 3, 0))); + } + + [Fact] + public void OperatorsEqualityAndStrings() + { + var a = new Vector3(1, 2, 3); + Assert.Equal(new Vector3(2, 4, 6), a + a); + Assert.Equal(Vector3.Zero, a - a); + Assert.Equal(new Vector3(-1, -2, -3), -a); + Assert.Equal(new Vector3(2, 4, 6), 2 * a); + Assert.Equal(new Vector3(2, 4, 6), a * 2); + Assert.Equal(new Vector3(0.5, 1, 1.5), a / 2); + Assert.True(a == new Vector3(1, 2, 3)); + Assert.True(a != new Vector3(2, 2, 3)); + Assert.True(a.Equals((object) new Vector3(1, 2, 3))); + Assert.False(a.Equals((object) "a")); + Assert.Equal(a.GetHashCode(), new Vector3(1, 2, 3).GetHashCode()); + Assert.True(a.ApproximatelyEquals(new Vector3(1, 2, 3 + 1e-12))); + Assert.False(a.ApproximatelyEquals(new Vector3(1, 2, 3.1), 0.01, 0)); + Assert.Equal(new Point3D(1, 2, 3), a.ToPoint()); + Assert.Equal("Vector3 {X: 1, Y: 2, Z: 3}", a.ToString()); + Assert.Equal(@"\left( \begin{array}{c} 1 \\ 6 \\ 8 \end{array} \right)", new Vector3(1, 6, 8).ToLaTeXString()); + } +} diff --git a/tests/SharpMath.Tests/Geometry/Vector4Tests.cs b/tests/SharpMath.Tests/Geometry/Vector4Tests.cs new file mode 100644 index 0000000..3f3dd8d --- /dev/null +++ b/tests/SharpMath.Tests/Geometry/Vector4Tests.cs @@ -0,0 +1,88 @@ +using SharpMath.Geometry; + +namespace SharpMath.Tests.Geometry; + +public class Vector4Tests +{ + [Fact] + public void Constructors_StaticVectorsAndIndexer() + { + var vector = new Vector4(new Vector3(1, 2, 3), 4); + Assert.Equal(new Vector4(1, 2, 3, 4), vector); + Assert.Equal(4, vector.Dimension); + Assert.Equal(4, vector[3]); + Assert.Equal(3, vector[2]); + Assert.Throws(() => vector[4]); + + // Regression: UnitW used to be (0, 0, 1, 0). + Assert.Equal(new Vector4(0, 0, 0, 1), Vector4.UnitW); + Assert.Equal(new Vector4(1, 0, 0, 0), Vector4.UnitX); + Assert.Equal(new Vector4(0, 1, 0, 0), Vector4.UnitY); + Assert.Equal(new Vector4(0, 0, 1, 0), Vector4.UnitZ); + Assert.Equal(new Vector4(1, 1, 1, 1), Vector4.One); + Assert.Equal(new Vector4(0, 0, 0, 0), Vector4.Zero); + } + + [Fact] + public void ArithmeticOperators_Exist() + { + var a = new Vector4(1, 2, 3, 4); + Assert.Equal(new Vector4(2, 4, 6, 8), a + a); + Assert.Equal(Vector4.Zero, a - a); + Assert.Equal(new Vector4(-1, -2, -3, -4), -a); + Assert.Equal(new Vector4(2, 4, 6, 8), a * 2); + Assert.Equal(new Vector4(2, 4, 6, 8), 2 * a); + Assert.Equal(new Vector4(0.5, 1, 1.5, 2), a / 2); + } + + [Fact] + public void MetricOperations() + { + var a = new Vector4(1, 1, 1, 1); + Assert.Equal(2, a.Length); + Assert.Equal(4, a.LengthSquared); + Assert.Equal(4, a.Dot(Vector4.One)); + Assert.Equal(4, Vector4.Dot(a, Vector4.One)); + Assert.True(a.Normalize().ApproximatelyEquals(new Vector4(0.5, 0.5, 0.5, 0.5))); + Assert.Throws(() => Vector4.Zero.Normalize()); + Assert.Equal(Math.PI / 2, Vector4.Angle(Vector4.UnitX, Vector4.UnitW), 12); + Assert.Equal(Math.PI / 2, Vector4.UnitX.Angle(Vector4.UnitW), 12); + Assert.Equal(2, Vector4.Distance(Vector4.Zero, a)); + Assert.Equal(2, a.Distance(Vector4.Zero)); + Assert.Equal(4, Vector4.DistanceSquared(Vector4.Zero, a)); + Assert.Equal(4, a.DistanceSquared(Vector4.Zero)); + Assert.True(new Vector4(0, 0, 0, 1).IsNormalized); + Assert.True(new Vector4(0, 0, 0, 1e-13).IsZero); + } + + [Fact] + public void Predicates() + { + Assert.True(new Vector4(1, 0, 2, 0).IsParallelTo(new Vector4(-2, 0, -4, 0))); + Assert.False(new Vector4(1, 0, 2, 0).IsParallelTo(new Vector4(1, 1, 2, 0))); + Assert.True(Vector4.UnitX.IsOrthogonalTo(Vector4.UnitW)); + Assert.True(Vector4.UnitX.IsOrthonormalTo(Vector4.UnitW)); + Assert.False(Vector4.UnitX.IsOrthonormalTo(Vector4.UnitX)); + } + + [Fact] + public void InterpolationTransformEqualityStrings() + { + Assert.Equal(new Vector4(1, 1, 1, 1), Vector4.Lerp(Vector4.Zero, new Vector4(2, 2, 2, 2), 0.5)); + Assert.Equal(new Vector4(2, 2, 2, 2), Vector4.Lerp(Vector4.Zero, new Vector4(2, 2, 2, 2), 3)); + Assert.Equal(new Vector4(4, 4, 4, 4), Vector4.LerpUnclamped(Vector4.Zero, new Vector4(2, 2, 2, 2), 2)); + Assert.Equal(new Vector4(1, 0, 0, 0), Vector4.MoveTowards(Vector4.Zero, new Vector4(3, 0, 0, 0), 1)); + Assert.Equal(new Vector4(3, 0, 0, 0), Vector4.MoveTowards(Vector4.Zero, new Vector4(3, 0, 0, 0), 5)); + Assert.Equal(new Vector4(2, 3, 4, 1), Vector4.Transform(new Vector4(1, 2, 3, 1), Matrix4x4.Translation(1, 1, 1))); + Assert.Equal(new Vector4(1, 2, 3, 0), Vector4.Transform(new Vector4(1, 2, 3, 0), Matrix4x4.Translation(1, 1, 1))); + + var a = new Vector4(1, 2, 3, 4); + Assert.True(a == new Vector4(1, 2, 3, 4)); + Assert.True(a != new Vector4(1, 2, 3, 5)); + Assert.True(a.Equals((object) new Vector4(1, 2, 3, 4))); + Assert.Equal(a.GetHashCode(), new Vector4(1, 2, 3, 4).GetHashCode()); + Assert.True(a.ApproximatelyEquals(new Vector4(1, 2, 3, 4 + 1e-12))); + Assert.Equal("Vector4 {X: 1, Y: 2, Z: 3, W: 4}", a.ToString()); + Assert.Equal(@"\left( \begin{array}{c} 1 \\ 2 \\ 3 \\ 4 \end{array} \right)", a.ToLaTeXString()); + } +} diff --git a/tests/SharpMath.Tests/Properties/ExpressionProperties.cs b/tests/SharpMath.Tests/Properties/ExpressionProperties.cs new file mode 100644 index 0000000..d8ebb44 --- /dev/null +++ b/tests/SharpMath.Tests/Properties/ExpressionProperties.cs @@ -0,0 +1,47 @@ +using System.Globalization; +using CsCheck; +using SharpMath.Expressions; + +namespace SharpMath.Tests.Properties; + +public class ExpressionProperties +{ + private sealed record Expr(string Text, Func Evaluate); + + private static readonly Gen Leaf = Gen.OneOf( + Gen.Int[0, 50].Select(n => new Expr(n.ToString(CultureInfo.InvariantCulture), _ => n)), + Gen.Const(new Expr("x", x => x)), + Gen.Const(new Expr("pi", _ => Math.PI))); + + private static Gen Expressions(int depth) => + depth == 0 + ? Leaf + : Gen.Frequency( + (2, Leaf), + (6, Gen.Select(Expressions(depth - 1), Expressions(depth - 1), Gen.Int[0, 3], Combine)), + (1, Expressions(depth - 1).Select(e => new Expr($"-({e.Text})", x => -e.Evaluate(x)))), + (1, Expressions(depth - 1).Select(e => new Expr($"sin({e.Text})", x => Math.Sin(e.Evaluate(x))))), + (1, Expressions(depth - 1).Select(e => new Expr($"({e.Text})^2", x => Math.Pow(e.Evaluate(x), 2))))); + + private static Expr Combine(Expr left, Expr right, int op) => op switch + { + 0 => new Expr($"({left.Text}) + ({right.Text})", x => left.Evaluate(x) + right.Evaluate(x)), + 1 => new Expr($"({left.Text}) - ({right.Text})", x => left.Evaluate(x) - right.Evaluate(x)), + 2 => new Expr($"({left.Text})({right.Text})", x => left.Evaluate(x) * right.Evaluate(x)), + _ => new Expr($"({left.Text}) / ({right.Text})", x => left.Evaluate(x) / right.Evaluate(x)) + }; + + [Fact] + public void ParsedExpressions_MatchTheEquivalentCSharp() + { + Gen.Select(Expressions(4), Gen.Double[-10, 10]).Sample((expression, x) => + { + var expected = expression.Evaluate(x); + var actual = MathExpression.Parse(expression.Text).ToFunc("x")(x); + if (double.IsNaN(expected)) + Assert.True(double.IsNaN(actual)); + else + Assert.True(Tolerance.AreEqual(expected, actual, 1e-9, 1e-9), $"{expression.Text} at x = {x}: {expected} vs {actual}"); + }); + } +} diff --git a/tests/SharpMath.Tests/Properties/GeometryProperties.cs b/tests/SharpMath.Tests/Properties/GeometryProperties.cs new file mode 100644 index 0000000..aa6b481 --- /dev/null +++ b/tests/SharpMath.Tests/Properties/GeometryProperties.cs @@ -0,0 +1,96 @@ +using CsCheck; +using SharpMath.Geometry; + +namespace SharpMath.Tests.Properties; + +public class GeometryProperties +{ + private static readonly Gen Coordinate = Gen.Double[-100, 100]; + private static readonly Gen Points = Gen.Select(Coordinate, Coordinate, (x, y) => new Point2D(x, y)); + private static readonly Gen Vectors3 = Gen.Select(Coordinate, Coordinate, Coordinate, (x, y, z) => new Vector3(x, y, z)); + + [Fact] + public void VectorProducts() + { + Gen.Select(Vectors3, Vectors3).Sample((a, b) => + { + var cross = a.Cross(b); + var scale = a.Length * b.Length; + Assert.True(Tolerance.IsNegligible(cross.Dot(a), scale * a.Length)); + Assert.True(Tolerance.IsNegligible(cross.Dot(b), scale * b.Length)); + Assert.Equal(a.Dot(b), b.Dot(a)); + Assert.True(Tolerance.AreEqual(cross.LengthSquared + a.Dot(b) * a.Dot(b), scale * scale, 1e-6, 1e-9)); + if (a.LengthSquared > 0) + Assert.True(a.Normalize().IsNormalized); + if (a.LengthSquared > 0 && b.LengthSquared > 0) + Assert.InRange(a.Angle(b), 0, Math.PI); + }); + } + + [Fact] + public void DirectionPredicates_DoNotDependOnTheScale() + { + Gen.Select(Vectors3, Vectors3, Gen.Int[-150, 150]).Sample((a, b, exponent) => + { + var scale = Math.Pow(10, exponent); + Assert.Equal(a.IsParallelTo(b), (a * scale).IsParallelTo(b * scale)); + Assert.Equal(a.IsOrthogonalTo(b), (a * scale).IsOrthogonalTo(b * scale)); + if (a.LengthSquared > 0) + Assert.True((a * scale).IsParallelTo(a * -scale * 3)); + }); + } + + [Fact] + public void PolygonArea_IsInvariantUnderTranslationAndVertexRotation() + { + Gen.Select(Points.Array[3, 12], Coordinate, Coordinate, Gen.Int[0, 11]).Sample((points, dx, dy, shift) => + { + var polygon = new Polygon(points); + var translated = new Polygon(points.Select(p => p + new Vector2(dx, dy))); + var rotated = new Polygon(points.Skip(shift % points.Length).Concat(points.Take(shift % points.Length))); + Assert.True(Tolerance.AreEqual(polygon.SignedArea, translated.SignedArea, 1e-6, 1e-9)); + Assert.True(Tolerance.AreEqual(polygon.SignedArea, rotated.SignedArea, 1e-6, 1e-9)); + Assert.True(Tolerance.AreEqual(polygon.Perimeter, rotated.Perimeter, 1e-6, 1e-9)); + var box = polygon.BoundingBox; + Assert.All(points, p => Assert.True(box.Contains(p))); + }); + } + + [Fact] + public void LineIntersections_LieOnBothLines() + { + Gen.Select(Points, Points, Points, Points).Sample((a, b, c, d) => + { + if (a.Distance(b) < 1e-3 || c.Distance(d) < 1e-3) + return; + var first = Line2D.FromPoints(a, b); + var second = Line2D.FromPoints(c, d); + Assert.True(first.Contains(a) && first.Contains(b)); + var intersection = first.Intersect(second); + if (intersection.Kind != IntersectionKind.Point) + return; + var point = intersection.Point!.Value; + var scale = Math.Max(1, Math.Max(Math.Abs(point.X), Math.Abs(point.Y))); + Assert.True(first.DistanceTo(point) < 1e-6 * scale); + Assert.True(second.DistanceTo(point) < 1e-6 * scale); + }); + } + + [Fact] + public void ClosestPointsOnSkewLines_AreConnectedByACommonPerpendicular() + { + Gen.Select(Vectors3, Vectors3, Vectors3, Vectors3).Sample((p, d, q, e) => + { + if (d.Length < 1e-3 || e.Length < 1e-3 || d.Normalize().Cross(e.Normalize()).Length < 1e-3) + return; + var first = Line3D.FromPointAndDirection(p.ToPoint(), d); + var second = Line3D.FromPointAndDirection(q.ToPoint(), e); + var (onFirst, onSecond) = first.ClosestPoints(second); + var connection = onSecond - onFirst; + Assert.True(Tolerance.IsNegligible(connection.Dot(first.Direction), 1e3)); + Assert.True(Tolerance.IsNegligible(connection.Dot(second.Direction), 1e3)); + Assert.True(first.Contains(onFirst)); + Assert.True(second.Contains(onSecond)); + }); + } +} diff --git a/tests/SharpMath.Tests/Properties/MatrixProperties.cs b/tests/SharpMath.Tests/Properties/MatrixProperties.cs new file mode 100644 index 0000000..d9cadfb --- /dev/null +++ b/tests/SharpMath.Tests/Properties/MatrixProperties.cs @@ -0,0 +1,193 @@ +using CsCheck; +using SharpMath.Geometry; + +namespace SharpMath.Tests.Properties; + +public class MatrixProperties +{ + private static readonly Gen Entry = Gen.Double[-10, 10]; + + private static readonly Gen Matrices2 = Entry.Array[4].Select(v => new Matrix2x2(v[0], v[1], v[2], v[3])); + + private static readonly Gen Matrices3 = + Entry.Array[9].Select(v => new Matrix3x3(v[0], v[1], v[2], v[3], v[4], v[5], v[6], v[7], v[8])); + + private static readonly Gen Matrices4 = Entry.Array[16].Select(v => new Matrix4x4(v[0], v[1], v[2], v[3], + v[4], v[5], v[6], v[7], v[8], v[9], v[10], v[11], v[12], v[13], v[14], v[15])); + + // Inverses of nearly singular matrices are legitimately inaccurate, so only well-conditioned ones are checked. The + // Hadamard ratio |det| / Π‖rowᵢ‖ lies in [0, 1] and does not depend on the scale of the rows. Rows of wildly + // different magnitude can still be numerically singular relative to the largest entry, so TryInvert decides. + private static bool WellConditioned(IMatrix matrix, double determinant) + { + var product = 1.0; + for (var row = 0; row < matrix.RowCount; row++) + { + var normSquared = 0.0; + for (var column = 0; column < matrix.ColumnCount; column++) + normSquared += matrix[row, column] * matrix[row, column]; + product *= Math.Sqrt(normSquared); + } + + return Math.Abs(determinant) > 1e-3 * product; + } + + // The rounding error of M · M⁻¹ is proportional to ‖M‖ · ‖M⁻¹‖ (here with max norms), not absolute. + private static void AssertNearIdentity(IMatrix product, IMatrix matrix, IMatrix inverse) + { + var bound = 1e-12 * product.RowCount * MaxAbs(matrix) * MaxAbs(inverse); + for (var row = 0; row < product.RowCount; row++) + for (var column = 0; column < product.ColumnCount; column++) + Assert.True(Math.Abs(product[row, column] - (row == column ? 1 : 0)) <= bound, + $"Entry ({row}, {column}) of {product} exceeds {bound}."); + } + + private static double MaxAbs(IMatrix matrix) + { + var max = 0.0; + for (var row = 0; row < matrix.RowCount; row++) + for (var column = 0; column < matrix.ColumnCount; column++) + max = Math.Max(max, Math.Abs(matrix[row, column])); + return max; + } + + [Fact] + public void Inverse_TimesMatrix_IsIdentity() + { + Matrices2.Sample(m => + { + if (WellConditioned(m, m.Determinant) && m.TryInvert(out var inverse)) + AssertNearIdentity(m * inverse, m, inverse); + }); + Matrices3.Sample(m => + { + if (WellConditioned(m, m.Determinant) && m.TryInvert(out var inverse)) + AssertNearIdentity(m * inverse, m, inverse); + }); + Matrices4.Sample(m => + { + if (WellConditioned(m, m.Determinant) && m.TryInvert(out var inverse)) + AssertNearIdentity(m * inverse, m, inverse); + }); + } + + [Fact] + public void AffineTransformations_AreInvertibleAtAnyScale() + { + Gen.Select(Gen.Double[-1e6, 1e6].Array[3], Gen.Double[-Math.PI, Math.PI].Array[3], Gen.Double[-3, 3].Array[3]) + .Sample((translation, angles, exponents) => + { + var transformation = Matrix4x4.Translation(translation[0], translation[1], translation[2]) * + Matrix4x4.Rotation(angles[0], angles[1], angles[2]) * + Matrix4x4.Scaling(Math.Pow(10, exponents[0]), Math.Pow(10, exponents[1]), + Math.Pow(10, exponents[2])); + Assert.False(transformation.IsSingular); + var point = new Vector3(1, 2, 3); + var roundTrip = transformation.Inverse() * (transformation * point); + Assert.True(roundTrip.ApproximatelyEquals(point, 1e-6, 1e-6), $"{roundTrip} for {transformation}"); + }); + } + + [Fact] + public void Determinant_IsMultiplicative() + { + Gen.Select(Matrices3, Matrices3).Sample((a, b) => + { + var expected = a.Determinant * b.Determinant; + Assert.True(Tolerance.AreEqual((a * b).Determinant, expected, 1e-6, 1e-9)); + }); + Gen.Select(Matrices4, Matrices4).Sample((a, b) => + { + var expected = a.Determinant * b.Determinant; + Assert.True(Tolerance.AreEqual((a * b).Determinant, expected, 1e-4, 1e-8)); + }); + } + + [Fact] + public void Adjugate_TimesMatrix_IsDeterminantTimesIdentity() + { + Matrices3.Sample(m => + Assert.True((m * m.Adjugate()).ApproximatelyEquals(Matrix3x3.Identity * m.Determinant, 1e-9, 1e-9))); + Matrices4.Sample(m => + Assert.True((m.Adjugate() * m).ApproximatelyEquals(Matrix4x4.Identity * m.Determinant, 1e-7, 1e-9))); + } + + [Fact] + public void Determinants_MatchTheLuDecomposition() + { + Matrices4.Sample(m => + Assert.True(Tolerance.AreEqual(m.Determinant, MatrixMxN.FromMatrix(m).Determinant(), 1e-8, 1e-9))); + } + + [Fact] + public void Transpose_ReversesProducts() + { + Gen.Select(Matrices4, Matrices4).Sample((a, b) => + Assert.True((a * b).Transpose().ApproximatelyEquals(b.Transpose() * a.Transpose()))); + } + + [Fact] + public void Rotations_PreserveLengthsAndAreOrthogonal() + { + Gen.Select(Gen.Double[-10, 10], Gen.Double[-10, 10], Gen.Double[-10, 10], Entry, Entry, Entry) + .Sample((ax, ay, az, x, y, z) => + { + var rotation = Matrix4x4.Rotation(ax, ay, az); + var vector = new Vector3(x, y, z); + Assert.True(Tolerance.AreEqual((rotation * vector).Length, vector.Length, 1e-9, 1e-9)); + Assert.True(rotation.IsOrthogonal); + Assert.True(Tolerance.AreEqual(rotation.Determinant, 1)); + var planar = Matrix2x2.Rotation(ax) * new Vector2(x, y); + Assert.True(Tolerance.AreEqual(planar.Length, new Vector2(x, y).Length, 1e-9, 1e-9)); + }); + } + + [Fact] + public void LookAtAndWorld_UseOrthonormalBases() + { + Gen.Select(Entry.Array[3], Entry.Array[3], Entry.Array[3]).Sample((e, t, u) => + { + var eye = new Vector3(e[0], e[1], e[2]); + var target = new Vector3(t[0], t[1], t[2]); + var up = new Vector3(u[0], u[1], u[2]); + var direction = target - eye; + if (direction.Length < 1e-3 || up.Length < 1e-3 || up.Cross(direction).Length < 1e-3 * up.Length * direction.Length) + return; + + var view = Matrix4x4.LookAt(eye, target, up); + Assert.True(RotationPart(view).IsOrthogonal); + Assert.True((view * eye).ApproximatelyEquals(Vector3.Zero, 1e-9, 1e-9)); + var forward = view * target; + Assert.True(forward.Z < 0); + + var world = Matrix4x4.World(eye, direction, up); + Assert.True(RotationPart(world).IsOrthogonal); + Assert.True((world * Vector3.Zero).ApproximatelyEquals(eye)); + Assert.True((view * world).ApproximatelyEquals(Matrix4x4.Identity, 1e-9, 1e-9)); + }); + } + + [Fact] + public void MatrixMxN_SolveHasSmallResiduals() + { + Gen.Int[1, 8].SelectMany(n => Gen.Double[-1, 1].Array[n * n].Select(Gen.Double[-10, 10].Array[n], (a, b) => (n, a, b))) + .Sample(sample => + { + var (n, values, rightHandSide) = sample; + + // Diagonal dominance guarantees a regular, well-conditioned matrix. + for (var i = 0; i < n; i++) + values[i * n + i] += n + 1; + var matrix = new MatrixMxN(n, n, values); + var solution = matrix.Solve(rightHandSide); + var residual = matrix * new MatrixMxN(n, 1, solution) - new MatrixMxN(n, 1, rightHandSide); + for (var i = 0; i < n; i++) + Assert.True(Math.Abs(residual[i, 0]) < 1e-9); + Assert.True((matrix * matrix.Inverse()).ApproximatelyEquals(MatrixMxN.Identity(n), 1e-9, 1e-9)); + Assert.Equal(n, matrix.Rank()); + }); + } + + private static Matrix3x3 RotationPart(Matrix4x4 m) => + new(m.M11, m.M12, m.M13, m.M21, m.M22, m.M23, m.M31, m.M32, m.M33); +} diff --git a/tests/SharpMath.Tests/SharpMath.Tests.csproj b/tests/SharpMath.Tests/SharpMath.Tests.csproj new file mode 100644 index 0000000..8d33d49 --- /dev/null +++ b/tests/SharpMath.Tests/SharpMath.Tests.csproj @@ -0,0 +1,7 @@ + + + + + + + diff --git a/tests/SharpMath.Tests/ToleranceTests.cs b/tests/SharpMath.Tests/ToleranceTests.cs new file mode 100644 index 0000000..0d3dcb7 --- /dev/null +++ b/tests/SharpMath.Tests/ToleranceTests.cs @@ -0,0 +1,51 @@ +namespace SharpMath.Tests; + +public class ToleranceTests +{ + [Theory] + [InlineData(1.0, 1.0)] + [InlineData(0.0, 1e-13)] + [InlineData(1e10 * (0.1 + 0.2), 3e9)] + [InlineData(double.PositiveInfinity, double.PositiveInfinity)] + public void AreEqual_AcceptsValuesWithinTolerance(double a, double b) => Assert.True(Tolerance.AreEqual(a, b)); + + [Theory] + [InlineData(1e-10, 1e-12)] + [InlineData(1.0, 1.0001)] + [InlineData(double.NaN, double.NaN)] + [InlineData(double.PositiveInfinity, double.NegativeInfinity)] + [InlineData(double.PositiveInfinity, 1.0)] + public void AreEqual_RejectsValuesOutsideTolerance(double a, double b) => Assert.False(Tolerance.AreEqual(a, b)); + + [Fact] + public void AreEqual_UsesCustomTolerances() + { + Assert.True(Tolerance.AreEqual(1, 1.05, 0.1, 0)); + Assert.True(Tolerance.AreEqual(100, 101, 0, 0.01)); + Assert.False(Tolerance.AreEqual(100, 102, 0, 0.01)); + } + + [Fact] + public void AreEqual_RejectsNegativeTolerances() + { + Assert.Throws(() => Tolerance.AreEqual(1, 1, -1, 0)); + Assert.Throws(() => Tolerance.AreEqual(1, 1, 0, double.NaN)); + } + + [Fact] + public void IsZero_UsesAbsoluteTolerance() + { + Assert.True(Tolerance.IsZero(1e-13)); + Assert.False(Tolerance.IsZero(1e-11)); + Assert.True(Tolerance.IsZero(0.05, 0.1)); + Assert.Throws(() => Tolerance.IsZero(0, -0.1)); + } + + [Fact] + public void IsNegligible_ScalesWithReference() + { + Assert.True(Tolerance.IsNegligible(1e-4, 1e6)); + Assert.False(Tolerance.IsNegligible(1e-4, 1)); + Assert.True(Tolerance.IsNegligible(1e-13, 0)); + } +}