diff --git a/.github/workflows/build-test-deploy.yml b/.github/workflows/build-test-deploy.yml index 6e8e1cf9..d6ccc9d1 100644 --- a/.github/workflows/build-test-deploy.yml +++ b/.github/workflows/build-test-deploy.yml @@ -17,7 +17,7 @@ jobs: - name: "Clone Repository" uses: actions/checkout@v4 - name: "CAKE (Build -> Test -> Package)" - run: dotnet run --project ./build/Build.csproj -- --target=Default + run: dotnet run --project ./src/cs/Build/Build.csproj -- --target=Default - name: "Upload Artifacts For Deploy" if: github.event_name != 'pull_request' uses: actions/upload-artifact@main diff --git a/.gitignore b/.gitignore index 4c156a7f..ea7d8869 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ # cake build output artifacts + +# Visual Studio +.vs/* diff --git a/MonoGame.Extended.sln b/MonoGame.Extended.sln index 195a7acc..0ada71ab 100644 --- a/MonoGame.Extended.sln +++ b/MonoGame.Extended.sln @@ -1,4 +1,4 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.28803.156 MinimumVisualStudioVersion = 10.0.40219.1 @@ -43,11 +43,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MonoGame.Extended.Collision EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MonoGame.Extended", "src\cs\MonoGame.Extended\MonoGame.Extended.csproj", "{4170BBC0-BF49-4307-8272-768EEA77034A}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Benchmarks", "Benchmarks", "{AAB81CB9-9C71-4499-A51E-933C384D6DBF}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MonoGame.Extended.Tweening.Tests", "src\cs\Tests\MonoGame.Extended.Tweening.Tests\MonoGame.Extended.Tweening.Tests.csproj", "{DAF853E1-57EE-4CD3-8CF9-4B49D733C58F}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MonoGame.Extended.Benchmarks.Collisions", "MonoGame.Extended.Benchmarks.Collisions\MonoGame.Extended.Benchmarks.Collisions.csproj", "{6B5939ED-E99D-4842-B4FE-A7624C59A60A}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{5DC42D13-F2ED-48F9-8408-A28D9DACFCF4}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MonoGame.Extended.Tweening.Tests", "MonoGame.Extended.Tweening.Tests\MonoGame.Extended.Tweening.Tests.csproj", "{A1BD6FDC-5A76-4D14-8FDA-ACD153C8EBCC}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Build", "src\cs\Build\Build.csproj", "{91B0F8A8-9C6C-4779-AC75-228B9856B4A9}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -193,22 +193,22 @@ Global {4170BBC0-BF49-4307-8272-768EEA77034A}.Release|Any CPU.Build.0 = Release|Any CPU {4170BBC0-BF49-4307-8272-768EEA77034A}.Release|x86.ActiveCfg = Release|Any CPU {4170BBC0-BF49-4307-8272-768EEA77034A}.Release|x86.Build.0 = Release|Any CPU - {6B5939ED-E99D-4842-B4FE-A7624C59A60A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6B5939ED-E99D-4842-B4FE-A7624C59A60A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6B5939ED-E99D-4842-B4FE-A7624C59A60A}.Debug|x86.ActiveCfg = Debug|Any CPU - {6B5939ED-E99D-4842-B4FE-A7624C59A60A}.Debug|x86.Build.0 = Debug|Any CPU - {6B5939ED-E99D-4842-B4FE-A7624C59A60A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6B5939ED-E99D-4842-B4FE-A7624C59A60A}.Release|Any CPU.Build.0 = Release|Any CPU - {6B5939ED-E99D-4842-B4FE-A7624C59A60A}.Release|x86.ActiveCfg = Release|Any CPU - {6B5939ED-E99D-4842-B4FE-A7624C59A60A}.Release|x86.Build.0 = Release|Any CPU - {A1BD6FDC-5A76-4D14-8FDA-ACD153C8EBCC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A1BD6FDC-5A76-4D14-8FDA-ACD153C8EBCC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A1BD6FDC-5A76-4D14-8FDA-ACD153C8EBCC}.Debug|x86.ActiveCfg = Debug|Any CPU - {A1BD6FDC-5A76-4D14-8FDA-ACD153C8EBCC}.Debug|x86.Build.0 = Debug|Any CPU - {A1BD6FDC-5A76-4D14-8FDA-ACD153C8EBCC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A1BD6FDC-5A76-4D14-8FDA-ACD153C8EBCC}.Release|Any CPU.Build.0 = Release|Any CPU - {A1BD6FDC-5A76-4D14-8FDA-ACD153C8EBCC}.Release|x86.ActiveCfg = Release|Any CPU - {A1BD6FDC-5A76-4D14-8FDA-ACD153C8EBCC}.Release|x86.Build.0 = Release|Any CPU + {DAF853E1-57EE-4CD3-8CF9-4B49D733C58F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DAF853E1-57EE-4CD3-8CF9-4B49D733C58F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DAF853E1-57EE-4CD3-8CF9-4B49D733C58F}.Debug|x86.ActiveCfg = Debug|Any CPU + {DAF853E1-57EE-4CD3-8CF9-4B49D733C58F}.Debug|x86.Build.0 = Debug|Any CPU + {DAF853E1-57EE-4CD3-8CF9-4B49D733C58F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DAF853E1-57EE-4CD3-8CF9-4B49D733C58F}.Release|Any CPU.Build.0 = Release|Any CPU + {DAF853E1-57EE-4CD3-8CF9-4B49D733C58F}.Release|x86.ActiveCfg = Release|Any CPU + {DAF853E1-57EE-4CD3-8CF9-4B49D733C58F}.Release|x86.Build.0 = Release|Any CPU + {91B0F8A8-9C6C-4779-AC75-228B9856B4A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {91B0F8A8-9C6C-4779-AC75-228B9856B4A9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {91B0F8A8-9C6C-4779-AC75-228B9856B4A9}.Debug|x86.ActiveCfg = Debug|Any CPU + {91B0F8A8-9C6C-4779-AC75-228B9856B4A9}.Debug|x86.Build.0 = Debug|Any CPU + {91B0F8A8-9C6C-4779-AC75-228B9856B4A9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {91B0F8A8-9C6C-4779-AC75-228B9856B4A9}.Release|Any CPU.Build.0 = Release|Any CPU + {91B0F8A8-9C6C-4779-AC75-228B9856B4A9}.Release|x86.ActiveCfg = Release|Any CPU + {91B0F8A8-9C6C-4779-AC75-228B9856B4A9}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -221,8 +221,8 @@ Global {43CBA868-9BFC-4E02-8A7A-142C603B087E} = {E5A148A1-DE7B-4D17-ABE8-831B9673B863} {34841DEB-EEC0-477C-B19D-CBB6DF49ED39} = {E5A148A1-DE7B-4D17-ABE8-831B9673B863} {CB439E84-F0F6-4790-8CD1-8A66C3D7B4DA} = {E5A148A1-DE7B-4D17-ABE8-831B9673B863} - {6B5939ED-E99D-4842-B4FE-A7624C59A60A} = {AAB81CB9-9C71-4499-A51E-933C384D6DBF} - {A1BD6FDC-5A76-4D14-8FDA-ACD153C8EBCC} = {E5A148A1-DE7B-4D17-ABE8-831B9673B863} + {DAF853E1-57EE-4CD3-8CF9-4B49D733C58F} = {E5A148A1-DE7B-4D17-ABE8-831B9673B863} + {91B0F8A8-9C6C-4779-AC75-228B9856B4A9} = {5DC42D13-F2ED-48F9-8408-A28D9DACFCF4} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {8ED5A62D-25EC-4331-9F99-BDD1E10C02A0} diff --git a/MonoGame.Extended.Benchmarks.Collisions/DifferentPoolSizeCollision.cs b/src/cs/Benchmarks/MonoGame.Extended.Benchmarks.Collisions/DifferentPoolSizeCollision.cs similarity index 100% rename from MonoGame.Extended.Benchmarks.Collisions/DifferentPoolSizeCollision.cs rename to src/cs/Benchmarks/MonoGame.Extended.Benchmarks.Collisions/DifferentPoolSizeCollision.cs diff --git a/MonoGame.Extended.Benchmarks.Collisions/MonoGame.Extended.Benchmarks.Collisions.csproj b/src/cs/Benchmarks/MonoGame.Extended.Benchmarks.Collisions/MonoGame.Extended.Benchmarks.Collisions.csproj similarity index 100% rename from MonoGame.Extended.Benchmarks.Collisions/MonoGame.Extended.Benchmarks.Collisions.csproj rename to src/cs/Benchmarks/MonoGame.Extended.Benchmarks.Collisions/MonoGame.Extended.Benchmarks.Collisions.csproj diff --git a/MonoGame.Extended.Benchmarks.Collisions/Program.cs b/src/cs/Benchmarks/MonoGame.Extended.Benchmarks.Collisions/Program.cs similarity index 100% rename from MonoGame.Extended.Benchmarks.Collisions/Program.cs rename to src/cs/Benchmarks/MonoGame.Extended.Benchmarks.Collisions/Program.cs diff --git a/MonoGame.Extended.Benchmarks.Collisions/SpaceAlgorithms.cs b/src/cs/Benchmarks/MonoGame.Extended.Benchmarks.Collisions/SpaceAlgorithms.cs similarity index 100% rename from MonoGame.Extended.Benchmarks.Collisions/SpaceAlgorithms.cs rename to src/cs/Benchmarks/MonoGame.Extended.Benchmarks.Collisions/SpaceAlgorithms.cs diff --git a/MonoGame.Extended.Benchmarks.Collisions/Utils/Collider.cs b/src/cs/Benchmarks/MonoGame.Extended.Benchmarks.Collisions/Utils/Collider.cs similarity index 100% rename from MonoGame.Extended.Benchmarks.Collisions/Utils/Collider.cs rename to src/cs/Benchmarks/MonoGame.Extended.Benchmarks.Collisions/Utils/Collider.cs diff --git a/build/Build.csproj b/src/cs/Build/Build.csproj similarity index 100% rename from build/Build.csproj rename to src/cs/Build/Build.csproj diff --git a/build/BuildContext.cs b/src/cs/Build/BuildContext.cs similarity index 100% rename from build/BuildContext.cs rename to src/cs/Build/BuildContext.cs diff --git a/build/BuildTask.cs b/src/cs/Build/BuildTask.cs similarity index 100% rename from build/BuildTask.cs rename to src/cs/Build/BuildTask.cs diff --git a/build/DeployToGithubTask.cs b/src/cs/Build/DeployToGithubTask.cs similarity index 100% rename from build/DeployToGithubTask.cs rename to src/cs/Build/DeployToGithubTask.cs diff --git a/build/DeployToMyGetTask.cs b/src/cs/Build/DeployToMyGetTask.cs similarity index 100% rename from build/DeployToMyGetTask.cs rename to src/cs/Build/DeployToMyGetTask.cs diff --git a/build/DeployToNuGetTask.cs b/src/cs/Build/DeployToNuGetTask.cs similarity index 100% rename from build/DeployToNuGetTask.cs rename to src/cs/Build/DeployToNuGetTask.cs diff --git a/build/PackageTask.cs b/src/cs/Build/PackageTask.cs similarity index 100% rename from build/PackageTask.cs rename to src/cs/Build/PackageTask.cs diff --git a/build/Program.cs b/src/cs/Build/Program.cs similarity index 90% rename from build/Program.cs rename to src/cs/Build/Program.cs index cca32654..b8131092 100644 --- a/build/Program.cs +++ b/src/cs/Build/Program.cs @@ -14,7 +14,7 @@ public static class Program { return new CakeHost() .UseContext() - .UseWorkingDirectory("../") + .UseWorkingDirectory("../../../") .Run(args); } } @@ -29,6 +29,5 @@ public sealed class DefaultTask : FrostingTask {} [TaskName("Deploy")] [IsDependentOn(typeof(DeployToGitHubTask))] -[IsDependentOn(typeof(DeployToMyGetTask))] [IsDependentOn(typeof(DeployToNuGetTask))] public sealed class DeployTask : FrostingTask {} diff --git a/build/RestoreTask.cs b/src/cs/Build/RestoreTask.cs similarity index 100% rename from build/RestoreTask.cs rename to src/cs/Build/RestoreTask.cs diff --git a/build/TestTask.cs b/src/cs/Build/TestTask.cs similarity index 100% rename from build/TestTask.cs rename to src/cs/Build/TestTask.cs diff --git a/src/cs/MonoGame.Extended.Collisions/CollisionComponent.cs b/src/cs/MonoGame.Extended.Collisions/CollisionComponent.cs index d615220e..51467d9d 100644 --- a/src/cs/MonoGame.Extended.Collisions/CollisionComponent.cs +++ b/src/cs/MonoGame.Extended.Collisions/CollisionComponent.cs @@ -181,44 +181,22 @@ namespace MonoGame.Extended.Collisions /// The distance vector from the edge of b to a's Position private static Vector2 CalculatePenetrationVector(IShapeF a, IShapeF b) { - switch (a) - { - case RectangleF rectA when b is RectangleF rectB: - return PenetrationVector(rectA, rectB); - case CircleF circA when b is CircleF circB: - return PenetrationVector(circA, circB); - case CircleF circA when b is RectangleF rectB: - return PenetrationVector(circA, rectB); - case RectangleF rectA when b is CircleF circB: - return PenetrationVector(rectA, circB); - } + return a switch + { + CircleF circleA when b is CircleF circleB => PenetrationVector(circleA, circleB), + CircleF circleA when b is RectangleF rectangleB => PenetrationVector(circleA, rectangleB), + CircleF circleA when b is OrientedRectangle orientedRectangleB => PenetrationVector(circleA, orientedRectangleB), - throw new NotSupportedException("Shapes must be either a CircleF or RectangleF"); - } + RectangleF rectangleA when b is CircleF circleB => PenetrationVector(rectangleA, circleB), + RectangleF rectangleA when b is RectangleF rectangleB => PenetrationVector(rectangleA, rectangleB), + RectangleF rectangleA when b is OrientedRectangle orientedRectangleB => PenetrationVector(rectangleA, orientedRectangleB), - private static Vector2 PenetrationVector(RectangleF rect1, RectangleF rect2) - { - var intersectingRectangle = RectangleF.Intersection(rect1, rect2); - Debug.Assert(!intersectingRectangle.IsEmpty, - "Violation of: !intersect.IsEmpty; Rectangles must intersect to calculate a penetration vector."); + OrientedRectangle orientedRectangleA when b is CircleF circleB => PenetrationVector(orientedRectangleA, circleB), + OrientedRectangle orientedRectangleA when b is RectangleF rectangleB => PenetrationVector(orientedRectangleA, rectangleB), + OrientedRectangle orientedRectangleA when b is OrientedRectangle orientedRectangleB => PenetrationVector(orientedRectangleA, orientedRectangleB), - Vector2 penetration; - if (intersectingRectangle.Width < intersectingRectangle.Height) - { - var d = rect1.Center.X < rect2.Center.X - ? intersectingRectangle.Width - : -intersectingRectangle.Width; - penetration = new Vector2(d, 0); - } - else - { - var d = rect1.Center.Y < rect2.Center.Y - ? intersectingRectangle.Height - : -intersectingRectangle.Height; - penetration = new Vector2(0, d); - } - - return penetration; + _ => throw new ArgumentOutOfRangeException(nameof(a)) + }; } private static Vector2 PenetrationVector(CircleF circ1, CircleF circ2) @@ -293,11 +271,71 @@ namespace MonoGame.Extended.Collisions } } + private static Vector2 PenetrationVector(CircleF circleA, OrientedRectangle orientedRectangleB) + { + var rotation = Matrix2.CreateRotationZ(orientedRectangleB.Orientation.Rotation); + var circleCenterInRectangleSpace = rotation.Transform(circleA.Center - orientedRectangleB.Center); + var circleInRectangleSpace = new CircleF(circleCenterInRectangleSpace, circleA.Radius); + var boundingRectangle = new BoundingRectangle(new Point2(), orientedRectangleB.Radii); + + var penetrationVector = PenetrationVector(circleInRectangleSpace, boundingRectangle); + var inverseRotation = Matrix2.CreateRotationZ(-orientedRectangleB.Orientation.Rotation); + var transformedPenetration = inverseRotation.Transform(penetrationVector); + + return transformedPenetration; + } + private static Vector2 PenetrationVector(RectangleF rect, CircleF circ) { return -PenetrationVector(circ, rect); } + private static Vector2 PenetrationVector(RectangleF rect1, RectangleF rect2) + { + var intersectingRectangle = RectangleF.Intersection(rect1, rect2); + Debug.Assert(!intersectingRectangle.IsEmpty, + "Violation of: !intersect.IsEmpty; Rectangles must intersect to calculate a penetration vector."); + + Vector2 penetration; + if (intersectingRectangle.Width < intersectingRectangle.Height) + { + var d = rect1.Center.X < rect2.Center.X + ? intersectingRectangle.Width + : -intersectingRectangle.Width; + penetration = new Vector2(d, 0); + } + else + { + var d = rect1.Center.Y < rect2.Center.Y + ? intersectingRectangle.Height + : -intersectingRectangle.Height; + penetration = new Vector2(0, d); + } + + return penetration; + } + + private static Vector2 PenetrationVector(RectangleF rectangleA, OrientedRectangle orientedRectangleB) + { + return PenetrationVector((OrientedRectangle)rectangleA, orientedRectangleB); + } + + private static Vector2 PenetrationVector(OrientedRectangle orientedRectangleA, CircleF circleB) + { + return -PenetrationVector(circleB, orientedRectangleA); + } + + private static Vector2 PenetrationVector(OrientedRectangle orientedRectangleA, RectangleF rectangleB) + { + return -PenetrationVector(rectangleB, orientedRectangleA); + } + + private static Vector2 PenetrationVector(OrientedRectangle orientedRectangleA, OrientedRectangle orientedRectangleB) + { + return OrientedRectangle.Intersects(orientedRectangleA, orientedRectangleB) + .MinimumTranslationVector; + } + #endregion } } diff --git a/src/cs/MonoGame.Extended.Content.Pipeline/Newtonsoft.Json.dll b/src/cs/MonoGame.Extended.Content.Pipeline/Newtonsoft.Json.dll old mode 100644 new mode 100755 index b501fb6a..1ffeabe6 Binary files a/src/cs/MonoGame.Extended.Content.Pipeline/Newtonsoft.Json.dll and b/src/cs/MonoGame.Extended.Content.Pipeline/Newtonsoft.Json.dll differ diff --git a/src/cs/MonoGame.Extended.Input/KeyboardStateExtended.cs b/src/cs/MonoGame.Extended.Input/KeyboardStateExtended.cs index 01464893..ee186771 100644 --- a/src/cs/MonoGame.Extended.Input/KeyboardStateExtended.cs +++ b/src/cs/MonoGame.Extended.Input/KeyboardStateExtended.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using Microsoft.Xna.Framework.Input; namespace MonoGame.Extended.Input @@ -22,9 +23,38 @@ namespace MonoGame.Extended.Input public bool IsKeyDown(Keys key) => _currentKeyboardState.IsKeyDown(key); public bool IsKeyUp(Keys key) => _currentKeyboardState.IsKeyUp(key); public Keys[] GetPressedKeys() => _currentKeyboardState.GetPressedKeys(); - + public void GetPressedKeys(Keys[] keys) => _currentKeyboardState.GetPressedKeys(keys); + + /// + /// Gets whether the given key was down on the previous state, but is now up. + /// + /// The key to check. + /// true if the key was released this state-change, otherwise false. + [Obsolete($"Deprecated in favor of {nameof(IsKeyReleased)}")] public bool WasKeyJustDown(Keys key) => _previousKeyboardState.IsKeyDown(key) && _currentKeyboardState.IsKeyUp(key); + + /// + /// Gets whether the given key was up on the previous state, but is now down. + /// + /// The key to check. + /// true if the key was pressed this state-change, otherwise false. + [Obsolete($"Deprecated in favor of {nameof(IsKeyPressed)}")] public bool WasKeyJustUp(Keys key) => _previousKeyboardState.IsKeyUp(key) && _currentKeyboardState.IsKeyDown(key); - public bool WasAnyKeyJustDown() => _previousKeyboardState.GetPressedKeys().Any(); + + /// + /// Gets whether the given key was down on the previous state, but is now up. + /// + /// The key to check. + /// true if the key was released this state-change, otherwise false. + public readonly bool IsKeyReleased(Keys key) => _previousKeyboardState.IsKeyDown(key) && _currentKeyboardState.IsKeyUp(key); + + /// + /// Gets whether the given key was up on the previous state, but is now down. + /// + /// The key to check. + /// true if the key was pressed this state-change, otherwise false. + public readonly bool IsKeyPressed(Keys key) => _previousKeyboardState.IsKeyUp(key) && _currentKeyboardState.IsKeyDown(key); + + public bool WasAnyKeyJustDown() => _previousKeyboardState.GetPressedKeyCount() > 0; } -} \ No newline at end of file +} diff --git a/src/cs/MonoGame.Extended.Input/MouseStateExtended.cs b/src/cs/MonoGame.Extended.Input/MouseStateExtended.cs index d9379d83..0ec6943f 100644 --- a/src/cs/MonoGame.Extended.Input/MouseStateExtended.cs +++ b/src/cs/MonoGame.Extended.Input/MouseStateExtended.cs @@ -63,6 +63,13 @@ namespace MonoGame.Extended.Input return false; } + /// + /// Get the just-down state for the mouse on this state-change: true if the mouse button has just been pressed. + /// + /// + /// Deprecated because of inconsistency with + /// The just-down state for the mouse on this state-change. + [Obsolete($"Deprecated in favor of {nameof(IsButtonPressed)}")] public bool WasButtonJustDown(MouseButton button) { // ReSharper disable once SwitchStatementMissingSomeCases @@ -78,6 +85,13 @@ namespace MonoGame.Extended.Input return false; } + /// + /// Get the just-up state for the mouse on this state-change: true if the mouse button has just been released. + /// + /// + /// Deprecated because of inconsistency with + /// The just-up state for the mouse on this state-change. + [Obsolete($"Deprecated in favor of {nameof(IsButtonReleased)}")] public bool WasButtonJustUp(MouseButton button) { // ReSharper disable once SwitchStatementMissingSomeCases @@ -93,9 +107,43 @@ namespace MonoGame.Extended.Input return false; } - private bool IsPressed(Func button) => button(_currentMouseState) == ButtonState.Pressed; - private bool IsReleased(Func button) => button(_currentMouseState) == ButtonState.Released; - private bool WasJustPressed(Func button) => button(_previousMouseState) == ButtonState.Released && button(_currentMouseState) == ButtonState.Pressed; - private bool WasJustReleased(Func button) => button(_previousMouseState) == ButtonState.Pressed && button(_currentMouseState) == ButtonState.Released; + /// + /// Get the pressed state of a mouse button, for this state-change. + /// + /// The button to check. + /// true if the given mouse button was pressed this state-change, otherwise false + public readonly bool IsButtonPressed(MouseButton button) => button switch + { + MouseButton.Left => WasJustPressed(m => m.LeftButton), + MouseButton.Middle => WasJustPressed(m => m.MiddleButton), + MouseButton.Right => WasJustPressed(m => m.RightButton), + MouseButton.XButton1 => WasJustPressed(m => m.XButton1), + MouseButton.XButton2 => WasJustPressed(m => m.XButton2), + _ => false, + }; + + /// + /// Get the released state of a mouse button, for this state-change. + /// + /// The button to check. + /// true if the given mouse button was released this state-change, otherwise false + public readonly bool IsButtonReleased(MouseButton button) => button switch + { + MouseButton.Left => WasJustReleased(m => m.LeftButton), + MouseButton.Middle => WasJustReleased(m => m.MiddleButton), + MouseButton.Right => WasJustReleased(m => m.RightButton), + MouseButton.XButton1 => WasJustReleased(m => m.XButton1), + MouseButton.XButton2 => WasJustReleased(m => m.XButton2), + _ => false, + }; + + private readonly bool IsPressed(Func button) + => button(_currentMouseState) == ButtonState.Pressed; + private readonly bool IsReleased(Func button) + => button(_currentMouseState) == ButtonState.Released; + private readonly bool WasJustPressed(Func button) + => button(_previousMouseState) == ButtonState.Released && button(_currentMouseState) == ButtonState.Pressed; + private readonly bool WasJustReleased(Func button) + => button(_previousMouseState) == ButtonState.Pressed && button(_currentMouseState) == ButtonState.Released; } -} \ No newline at end of file +} diff --git a/src/cs/MonoGame.Extended/Math/CircleF.cs b/src/cs/MonoGame.Extended/Math/CircleF.cs index d5b464af..70f93074 100644 --- a/src/cs/MonoGame.Extended/Math/CircleF.cs +++ b/src/cs/MonoGame.Extended/Math/CircleF.cs @@ -425,13 +425,13 @@ namespace MonoGame.Extended } /// - /// Performs an implicit conversion from a to a . + /// Performs an explicit conversion from a to a . /// /// The circle. /// /// The resulting . /// - public static implicit operator Rectangle(CircleF circle) + public static explicit operator Rectangle(CircleF circle) { var diameter = (int) circle.Diameter; return new Rectangle((int) (circle.Center.X - circle.Radius), (int) (circle.Center.Y - circle.Radius), @@ -446,17 +446,17 @@ namespace MonoGame.Extended /// public Rectangle ToRectangle() { - return this; + return (Rectangle)this; } /// - /// Performs an implicit conversion from a to a . + /// Performs an explicit conversion from a to a . /// /// The rectangle. /// /// The resulting . /// - public static implicit operator CircleF(Rectangle rectangle) + public static explicit operator CircleF(Rectangle rectangle) { var halfWidth = rectangle.Width / 2; var halfHeight = rectangle.Height / 2; @@ -465,13 +465,13 @@ namespace MonoGame.Extended } /// - /// Performs an implicit conversion from a to a . + /// Performs an explicit conversion from a to a . /// /// The circle. /// /// The resulting . /// - public static implicit operator RectangleF(CircleF circle) + public static explicit operator RectangleF(CircleF circle) { var diameter = circle.Diameter; return new RectangleF(circle.Center.X - circle.Radius, circle.Center.Y - circle.Radius, diameter, diameter); @@ -485,17 +485,17 @@ namespace MonoGame.Extended /// public RectangleF ToRectangleF() { - return this; + return (RectangleF)this; } /// - /// Performs an implicit conversion from a to a . + /// Performs an explicit conversion from a to a . /// /// The rectangle. /// /// The resulting . /// - public static implicit operator CircleF(RectangleF rectangle) + public static explicit operator CircleF(RectangleF rectangle) { var halfWidth = rectangle.Width * 0.5f; var halfHeight = rectangle.Height * 0.5f; diff --git a/src/cs/MonoGame.Extended/Math/OrientedRectangle.cs b/src/cs/MonoGame.Extended/Math/OrientedRectangle.cs new file mode 100644 index 00000000..1439e9f7 --- /dev/null +++ b/src/cs/MonoGame.Extended/Math/OrientedRectangle.cs @@ -0,0 +1,302 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Microsoft.Xna.Framework; +using SharpDX.Direct3D; + +namespace MonoGame.Extended +{ + // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 4.4; Bounding Volumes - Oriented Bounding Boxes (OBBs), pg 101. + + /// + /// An oriented bounding rectangle is a rectangular block, much like a bounding rectangle + /// but with an arbitrary orientation . + /// + /// + [DebuggerDisplay($"{{{nameof(DebugDisplayString)},nq}}")] + public struct OrientedRectangle : IEquatable, IShapeF + { + /// + /// The centre position of this . + /// + public Point2 Center; + + /// + /// The distance from the point along both axes to any point on the boundary of this + /// . + /// + /// + public Vector2 Radii; + + /// + /// The rotation matrix of the bounding rectangle . + /// + public Matrix2 Orientation; + + /// + /// Initializes a new instance of the structure from the specified centre + /// and the radii . + /// + /// The centre . + /// The radii . + /// The orientation . + public OrientedRectangle(Point2 center, Size2 radii, Matrix2 orientation) + { + Center = center; + Radii = radii; + Orientation = orientation; + } + + /// + /// Gets a list of points defining the corner points of the oriented rectangle. + /// + public IReadOnlyList Points + { + get + { + var topLeft = -Radii; + var bottomLeft = -new Vector2(Radii.X, -Radii.Y); + var topRight = (Vector2)new Point2(Radii.X, -Radii.Y); + var bottomRight = Radii; + + return new List + { + Vector2.Transform(topRight, Orientation) + Center, + Vector2.Transform(topLeft, Orientation) + Center, + Vector2.Transform(bottomLeft, Orientation) + Center, + Vector2.Transform(bottomRight, Orientation) + Center + }; + } + } + + public Point2 Position + { + get => Vector2.Transform(-Radii, Orientation) + Center; + set => throw new NotImplementedException(); + } + + public RectangleF BoundingRectangle => (RectangleF)this; + + /// + /// Computes the from the specified + /// transformed by . + /// + /// The to transform. + /// The transformation. + /// A new . + public static OrientedRectangle Transform(OrientedRectangle rectangle, ref Matrix2 transformMatrix) + { + Transform(ref rectangle, ref transformMatrix, out var result); + return result; + } + + private static void Transform(ref OrientedRectangle rectangle, ref Matrix2 transformMatrix, out OrientedRectangle result) + { + PrimitivesHelper.TransformOrientedRectangle( + ref rectangle.Center, + ref rectangle.Orientation, + ref transformMatrix); + result = new OrientedRectangle(); + result.Center = rectangle.Center; + result.Radii = rectangle.Radii; + result.Orientation = rectangle.Orientation; + } + + /// + /// Compares to two structures. The result specifies whether the + /// the values of the , and are + /// equal. + /// + /// The left . + /// The right . + /// true if left and right argument are equal; otherwise, false. + public static bool operator ==(OrientedRectangle left, OrientedRectangle right) + { + return left.Equals(right); + } + + /// + /// Compares to two structures. The result specifies whether the + /// the values of the , or are + /// unequal. + /// + /// The left . + /// The right . + /// true if left and right argument are unequal; otherwise, false. + public static bool operator !=(OrientedRectangle left, OrientedRectangle right) + { + return !left.Equals(right); + } + + /// + /// Determines whether two instances of are equal. + /// + /// The other . + /// true if the specified is equal + /// to the current ; otherwise, false. + public bool Equals(OrientedRectangle other) + { + return Center.Equals(other.Center) && Radii.Equals(other.Radii) && Orientation.Equals(other.Orientation); + } + + /// + /// Determines whether two instances of are equal. + /// + /// The to compare to. + /// true if the specified is equal + /// to the current ; otherwise, false. + public override bool Equals(object obj) + { + return obj is OrientedRectangle other && Equals(other); + } + + /// + /// Returns a hash code for this object instance. + /// + /// + public override int GetHashCode() + { + return HashCode.Combine(Center, Radii, Orientation); + } + + /// + /// Performs an implicit conversion from a to . + /// + /// The rectangle to convert. + /// The resulting . + public static explicit operator OrientedRectangle(RectangleF rectangle) + { + var radii = new Size2(rectangle.Width * 0.5f, rectangle.Height * 0.5f); + var centre = new Point2(rectangle.X + radii.Width, rectangle.Y + radii.Height); + + return new OrientedRectangle(centre, radii, Matrix2.Identity); + } + + public static explicit operator RectangleF(OrientedRectangle orientedRectangle) + { + var topLeft = -orientedRectangle.Radii; + var rectangle = new RectangleF(topLeft, orientedRectangle.Radii * 2); + var orientation = orientedRectangle.Orientation * Matrix2.CreateTranslation(orientedRectangle.Center); + return RectangleF.Transform(rectangle, ref orientation); + } + + /// + /// See: + /// https://www.flipcode.com/archives/2D_OBB_Intersection.shtml + /// https://dyn4j.org/2010/01/sat + /// + /// + /// + /// + /// + public static (bool Intersects, Vector2 MinimumTranslationVector) Intersects( + OrientedRectangle rectangle, OrientedRectangle other) + { + var corners = rectangle.Points; + var otherCorners = other.Points; + + var allAxis = new[] + { + corners[1] - corners[0], + corners[3] - corners[0], + otherCorners[1] - otherCorners[0], + otherCorners[3] - otherCorners[0], + }; + var normalizedAxis = new[] + { + allAxis[0], + allAxis[1], + allAxis[2], + allAxis[3] + }; + var overlap = 0f; + var minimumTranslationVector = Vector2.Zero; + + // Make the length of each axis 1/edge length, so we know any + // dot product must be less than 1 to fall within the edge. + for (var a = 0; a < normalizedAxis.Length; a++) + { + normalizedAxis[a] /= normalizedAxis[a].LengthSquared(); + } + + for (var a = 0; a < normalizedAxis.Length; a++) + { + var axisProjectedOnto = normalizedAxis[a]; + var originalAxis = allAxis[a]; + + var p1 = Project(corners, axisProjectedOnto); + var p2 = Project(otherCorners, axisProjectedOnto); + + if (!IsOverlapping(p1, p2)) + { + // There was no intersection along this dimension; + // the boxes cannot possibly overlap. + return (false, Vector2.Zero); + } + + var o = GetOverlap(p1, p2); + if (o < overlap || overlap == 0f) + { + overlap = o; + minimumTranslationVector = originalAxis * overlap; + if (p1.Min > p2.Min) + { + minimumTranslationVector = -minimumTranslationVector; + } + } + } + + // There was no dimension along which there is no intersection. + // Therefore, the boxes overlap. + return (true, minimumTranslationVector); + + (float Min, float Max) Project(IReadOnlyList vertices, Vector2 axis) + { + var t = vertices[0].Dot(axis); + + // Find the extent of box 2 on axis a + var min = t; + var max = t; + + for (var c = 1; c < 4; c++) + { + t = vertices[c].Dot(axis); + + if (t < min) + { + min = t; + } + else if (t > max) + { + max = t; + } + } + + return (min, max); + } + + bool IsOverlapping((float Min, float Max) p1, (float Min, float Max) p2) + { + return p1.Min <= p2.Max && p1.Max >= p2.Min; + } + + float GetOverlap((float Min, float Max) p1, (float Min, float Max) p2) + { + return Math.Min(p1.Max, p2.Max) - Math.Max(p1.Min, p2.Min); + } + } + + /// + /// Returns a that represents this . + /// + /// + /// A that represents this . + /// + public override string ToString() + { + return $"Centre: {Center}, Radii: {Radii}, Orientation: {Orientation}"; + } + + internal string DebugDisplayString => ToString(); + } +} diff --git a/src/cs/MonoGame.Extended/Math/PrimitivesHelper.cs b/src/cs/MonoGame.Extended/Math/PrimitivesHelper.cs index ef151bcb..ed082708 100644 --- a/src/cs/MonoGame.Extended/Math/PrimitivesHelper.cs +++ b/src/cs/MonoGame.Extended/Math/PrimitivesHelper.cs @@ -71,6 +71,21 @@ namespace MonoGame.Extended halfExtents.Y = xRadius * Math.Abs(transformMatrix.M21) + yRadius * Math.Abs(transformMatrix.M22); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void TransformOrientedRectangle( + ref Point2 center, + ref Matrix2 orientation, + ref Matrix2 transformMatrix) + { + // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 4.4; Oriented Bounding Boxes (OBBs), pg 101-105. + + center = transformMatrix.Transform(center); + orientation *= transformMatrix; + // Reset the translation since orientation is only about rotation + orientation.M31 = 0; + orientation.M32 = 0; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static float SquaredDistanceToPointFromRectangle(Point2 minimum, Point2 maximum, Point2 point) { diff --git a/src/cs/MonoGame.Extended/Math/RectangleF.cs b/src/cs/MonoGame.Extended/Math/RectangleF.cs index 7cd467e5..1a1d6859 100644 --- a/src/cs/MonoGame.Extended/Math/RectangleF.cs +++ b/src/cs/MonoGame.Extended/Math/RectangleF.cs @@ -664,7 +664,7 @@ namespace MonoGame.Extended } /// - /// Performs an implicit conversion from a to a . + /// Performs an explicit conversion from a to a . /// /// The rectangle. /// diff --git a/src/cs/MonoGame.Extended/Math/ShapeF.cs b/src/cs/MonoGame.Extended/Math/ShapeF.cs index bf8d61a3..7478f759 100644 --- a/src/cs/MonoGame.Extended/Math/ShapeF.cs +++ b/src/cs/MonoGame.Extended/Math/ShapeF.cs @@ -1,4 +1,7 @@ -namespace MonoGame.Extended +using System; +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended { /// /// Base class for shapes. @@ -27,31 +30,28 @@ /// /// Check if two shapes intersect. /// - /// The first shape. - /// The second shape. + /// The first shape. + /// The second shape. /// True if the two shapes intersect. - public static bool Intersects(this IShapeF shapeA, IShapeF shapeB) + public static bool Intersects(this IShapeF a, IShapeF b) { - var intersects = false; + return a switch + { + CircleF circleA when b is CircleF circleB => circleA.Intersects(circleB), + CircleF circleA when b is RectangleF rectangleB => circleA.Intersects(rectangleB), + CircleF circleA when b is OrientedRectangle orientedRectangleB => Intersects(circleA, orientedRectangleB), - if (shapeA is RectangleF rectangleA && shapeB is RectangleF rectangleB) - { - intersects = rectangleA.Intersects(rectangleB); - } - else if (shapeA is CircleF circleA && shapeB is CircleF circleB) - { - intersects = circleA.Intersects(circleB); - } - else if (shapeA is RectangleF rect1 && shapeB is CircleF circ1) - { - return Intersects(circ1, rect1); - } - else if (shapeA is CircleF circ2 && shapeB is RectangleF rect2) - { - return Intersects(circ2, rect2); - } + RectangleF rectangleA when b is CircleF circleB => Intersects(circleB, rectangleA), + RectangleF rectangleA when b is RectangleF rectangleB => rectangleA.Intersects(rectangleB), + RectangleF rectangleA when b is OrientedRectangle orientedRectangleB => Intersects(rectangleA, orientedRectangleB).Intersects, - return intersects; + OrientedRectangle orientedRectangleA when b is CircleF circleB => Intersects(circleB, orientedRectangleA), + OrientedRectangle orientedRectangleA when b is RectangleF rectangleB => Intersects(rectangleB, orientedRectangleA).Intersects, + OrientedRectangle orientedRectangleA when b is OrientedRectangle orientedRectangleB + => OrientedRectangle.Intersects(orientedRectangleA, orientedRectangleB).Intersects, + + _ => throw new ArgumentOutOfRangeException(nameof(a)) + }; } /// @@ -65,5 +65,31 @@ var closestPoint = rectangle.ClosestPointTo(circle.Center); return circle.Contains(closestPoint); } + + /// + /// Checks whether a and intersects. + /// + /// to use in intersection test. + /// to use in intersection test. + /// True if the circle and oriented bounded rectangle intersects, otherwise false. + public static bool Intersects(CircleF circle, OrientedRectangle orientedRectangle) + { + var rotation = Matrix2.CreateRotationZ(orientedRectangle.Orientation.Rotation); + var circleCenterInRectangleSpace = rotation.Transform(circle.Center - orientedRectangle.Center); + var circleInRectangleSpace = new CircleF(circleCenterInRectangleSpace, circle.Radius); + var boundingRectangle = new BoundingRectangle(new Point2(), orientedRectangle.Radii); + return circleInRectangleSpace.Intersects(boundingRectangle); + } + + /// + /// Checks if a and intersects. + /// + /// + /// + /// True if objects are intersecting, otherwise false. + public static (bool Intersects, Vector2 MinimumTranslationVector) Intersects(RectangleF rectangleF, OrientedRectangle orientedRectangle) + { + return OrientedRectangle.Intersects(orientedRectangle, (OrientedRectangle)rectangleF); + } } } diff --git a/src/cs/Tests/MonoGame.Extended.Collisions.Tests/MonoGame.Extended.Collisions.Tests.csproj b/src/cs/Tests/MonoGame.Extended.Collisions.Tests/MonoGame.Extended.Collisions.Tests.csproj index 729c105c..a032112e 100644 --- a/src/cs/Tests/MonoGame.Extended.Collisions.Tests/MonoGame.Extended.Collisions.Tests.csproj +++ b/src/cs/Tests/MonoGame.Extended.Collisions.Tests/MonoGame.Extended.Collisions.Tests.csproj @@ -8,9 +8,9 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/cs/Tests/MonoGame.Extended.Content.Pipeline.Tests.Tiled/MonoGame.Extended.Content.Pipeline.Tests.Tiled.csproj b/src/cs/Tests/MonoGame.Extended.Content.Pipeline.Tests.Tiled/MonoGame.Extended.Content.Pipeline.Tests.Tiled.csproj index 06a67b14..63b7d40c 100644 --- a/src/cs/Tests/MonoGame.Extended.Content.Pipeline.Tests.Tiled/MonoGame.Extended.Content.Pipeline.Tests.Tiled.csproj +++ b/src/cs/Tests/MonoGame.Extended.Content.Pipeline.Tests.Tiled/MonoGame.Extended.Content.Pipeline.Tests.Tiled.csproj @@ -40,9 +40,9 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/cs/Tests/MonoGame.Extended.Content.Pipeline.Tests/MonoGame.Extended.Content.Pipeline.Tests.csproj b/src/cs/Tests/MonoGame.Extended.Content.Pipeline.Tests/MonoGame.Extended.Content.Pipeline.Tests.csproj index 2a3f9cdc..7b503377 100644 --- a/src/cs/Tests/MonoGame.Extended.Content.Pipeline.Tests/MonoGame.Extended.Content.Pipeline.Tests.csproj +++ b/src/cs/Tests/MonoGame.Extended.Content.Pipeline.Tests/MonoGame.Extended.Content.Pipeline.Tests.csproj @@ -8,9 +8,9 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/cs/Tests/MonoGame.Extended.Entities.Tests/MonoGame.Extended.Entities.Tests.csproj b/src/cs/Tests/MonoGame.Extended.Entities.Tests/MonoGame.Extended.Entities.Tests.csproj index 73d4479f..ec4254e8 100644 --- a/src/cs/Tests/MonoGame.Extended.Entities.Tests/MonoGame.Extended.Entities.Tests.csproj +++ b/src/cs/Tests/MonoGame.Extended.Entities.Tests/MonoGame.Extended.Entities.Tests.csproj @@ -7,9 +7,9 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/cs/Tests/MonoGame.Extended.Gui.Tests/MonoGame.Extended.Gui.Tests.csproj b/src/cs/Tests/MonoGame.Extended.Gui.Tests/MonoGame.Extended.Gui.Tests.csproj index 2dca7dd8..b6a995be 100644 --- a/src/cs/Tests/MonoGame.Extended.Gui.Tests/MonoGame.Extended.Gui.Tests.csproj +++ b/src/cs/Tests/MonoGame.Extended.Gui.Tests/MonoGame.Extended.Gui.Tests.csproj @@ -7,9 +7,9 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/cs/Tests/MonoGame.Extended.Tests/CollectionAssert.cs b/src/cs/Tests/MonoGame.Extended.Tests/CollectionAssert.cs new file mode 100644 index 00000000..15c105bd --- /dev/null +++ b/src/cs/Tests/MonoGame.Extended.Tests/CollectionAssert.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using Xunit; + +namespace MonoGame.Extended.Tests; + +public static class CollectionAssert +{ + public static void Equal(IReadOnlyList expected, IReadOnlyList actual) + { + Assert.True(expected.Count == actual.Count, "The number of items in the collections does not match."); + + Assert.All(actual, x => Assert.Contains(x, expected)); + } +} diff --git a/src/cs/Tests/MonoGame.Extended.Tests/MonoGame.Extended.Tests.csproj b/src/cs/Tests/MonoGame.Extended.Tests/MonoGame.Extended.Tests.csproj index 66ec6d54..18f78f1c 100644 --- a/src/cs/Tests/MonoGame.Extended.Tests/MonoGame.Extended.Tests.csproj +++ b/src/cs/Tests/MonoGame.Extended.Tests/MonoGame.Extended.Tests.csproj @@ -7,9 +7,9 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/cs/Tests/MonoGame.Extended.Tests/Primitives/OrientedRectangleTests.cs b/src/cs/Tests/MonoGame.Extended.Tests/Primitives/OrientedRectangleTests.cs new file mode 100644 index 00000000..e3b40c8c --- /dev/null +++ b/src/cs/Tests/MonoGame.Extended.Tests/Primitives/OrientedRectangleTests.cs @@ -0,0 +1,234 @@ +using System.Collections.Generic; +using Microsoft.Xna.Framework; +using Xunit; +using Vector2 = Microsoft.Xna.Framework.Vector2; + +namespace MonoGame.Extended.Tests.Primitives; + +public class OrientedRectangleTests +{ + [Fact] + public void Initializes_oriented_rectangle() + { + var rectangle = new OrientedRectangle(new Point2(1, 2), new Size2(3, 4), new Matrix2(5, 6, 7, 8, 9, 10)); + + Assert.Equal(new Point2(1, 2), rectangle.Center); + Assert.Equal(new Vector2(3, 4), rectangle.Radii); + Assert.Equal(new Matrix2(5, 6, 7, 8, 9, 10), rectangle.Orientation); + CollectionAssert.Equal( + new List + { + new(-3, -2), + new(-33, -38), + new(23, 26), + new(53, 62) + }, + rectangle.Points); + } + + public static readonly IEnumerable _equalsComparisons = new[] + { + new object[] + { + "empty compared with empty is true", + new OrientedRectangle(Point2.Zero, Size2.Empty, Matrix2.Identity), + new OrientedRectangle(Point2.Zero, Size2.Empty, Matrix2.Identity) + }, + new object[] + { + "initialized compared with initialized true", + new OrientedRectangle(new Point2(1, 2), new Size2(3, 4), new Matrix2(5, 6, 7, 8, 9, 10)), + new OrientedRectangle(new Point2(1, 2), new Size2(3, 4), new Matrix2(5, 6, 7, 8, 9, 10)) + } + }; + + [Theory] + [MemberData(nameof(_equalsComparisons))] +#pragma warning disable xUnit1026 + public void Equals_comparison(string name, OrientedRectangle first, OrientedRectangle second) +#pragma warning restore xUnit1026 + { + Assert.True(first == second); + Assert.False(first != second); + } + + public class Transform + { + [Fact] + public void Center_point_is_not_translated() + { + var rectangle = new OrientedRectangle(new Point2(1, 2), new Size2(), Matrix2.Identity); + var transform = Matrix2.Identity; + + var result = OrientedRectangle.Transform(rectangle, ref transform); + + Assert.Equal(new Point2(1, 2), result.Center); + } + + [Fact] + public void Center_point_is_translated() + { + var rectangle = new OrientedRectangle(new Point2(0, 0), new Size2(), new Matrix2()); + var transform = Matrix2.CreateTranslation(1, 2); + + var result = OrientedRectangle.Transform(rectangle, ref transform); + + Assert.Equal(new Point2(1, 2), result.Center); + } + + [Fact] + public void Radii_is_not_changed_by_identity_transform() + { + var rectangle = new OrientedRectangle(new Point2(), new Size2(10, 20), new Matrix2()); + var transform = Matrix2.Identity; + + var result = OrientedRectangle.Transform(rectangle, ref transform); + + Assert.Equal(new Vector2(10, 20), result.Radii); + } + + [Fact] + public void Radii_is_not_changed_by_translation() + { + var rectangle = new OrientedRectangle(new Point2(1, 2), new Size2(10, 20), new Matrix2()); + var transform = Matrix2.CreateTranslation(1, 2); + + var result = OrientedRectangle.Transform(rectangle, ref transform); + + Assert.Equal(new Vector2(10, 20), result.Radii); + } + + [Fact] + public void Orientation_is_rotated_45_degrees_left() + { + var rectangle = new OrientedRectangle(new Point2(), new Size2(), Matrix2.Identity); + var transform = Matrix2.CreateRotationZ(MathHelper.PiOver4); + + var result = OrientedRectangle.Transform(rectangle, ref transform); + + Assert.Equal(new Point2(), result.Center); + Assert.Equal(new Vector2(), result.Radii); + Assert.Equal(Matrix2.CreateRotationZ(MathHelper.PiOver4), result.Orientation); + } + + [Fact] + public void Orientation_is_rotated_to_45_degrees_from_180() + { + var rectangle = new OrientedRectangle(new Point2(), new Size2(), Matrix2.CreateRotationZ(MathHelper.Pi)); + var transform = Matrix2.CreateRotationZ(-3 * MathHelper.PiOver4); + var expectedOrientation = Matrix2.CreateRotationZ(MathHelper.PiOver4); + + var result = OrientedRectangle.Transform(rectangle, ref transform); + + Assert.Equal(new Point2(), result.Center); + Assert.Equal(new Vector2(), result.Radii); + Assert.Equal(expectedOrientation.M11, result.Orientation.M11, 6); + Assert.Equal(expectedOrientation.M12, result.Orientation.M12, 6); + Assert.Equal(expectedOrientation.M21, result.Orientation.M21, 6); + Assert.Equal(expectedOrientation.M22, result.Orientation.M22, 6); + Assert.Equal(expectedOrientation.M31, result.Orientation.M31, 6); + Assert.Equal(expectedOrientation.M32, result.Orientation.M32, 6); + } + + [Fact] + public void Points_are_same_as_center() + { + var rectangle = new OrientedRectangle(new Point2(1, 2), new Size2(), Matrix2.Identity); + var transform = Matrix2.Identity; + + var result = OrientedRectangle.Transform(rectangle, ref transform); + + CollectionAssert.Equal( + new List + { + new(1, 2), + new(1, 2), + new(1, 2), + new(1, 2) + }, + result.Points); + } + + [Fact] + public void Points_are_translated() + { + var rectangle = new OrientedRectangle(new Point2(0, 0), new Size2(2, 4), Matrix2.Identity); + var transform = Matrix2.CreateTranslation(10, 20); + + var result = OrientedRectangle.Transform(rectangle, ref transform); + + CollectionAssert.Equal( + new List + { + new(8, 16), + new(8, 24), + new(12, 24), + new(12, 16) + }, + result.Points); + } + + [Fact] + public void Applies_rotation_and_translation() + { + /* Rectangle with center point p, aligned in coordinate system with origin 0. + * + * : + * : + * +-+ + * | | + * |p| + * | | + * ...............0-+............ + * : + * : + * : + * + * Rotate around center point p, 90 degrees around origin 0. + * + * : + * : + * +---+ + * | p | + * ...........+---0.............. + * : + * : + * : + * + * Then translate rectangle by x=10 and y=20. + * : + * : +---+ + * : | p | + * y=21 - - - - - - - -> +---+ + * . + * : + * ...............0.............. + * : + * : + * : + */ + var rectangle = new OrientedRectangle(new Point2(1, 2), new Size2(2, 4), Matrix2.Identity); + var transform = + Matrix2.CreateRotationZ(MathHelper.PiOver2) + * + Matrix2.CreateTranslation(10, 20); + + var result = OrientedRectangle.Transform(rectangle, ref transform); + + Assert.Equal(8, result.Center.X, 6); + Assert.Equal(21, result.Center.Y, 6); + Assert.Equal(2, result.Radii.X, 6); + Assert.Equal(4, result.Radii.Y, 6); + Assert.Equal(Matrix2.CreateRotationZ(MathHelper.PiOver2), result.Orientation); + CollectionAssert.Equal( + new List + { + new(4, 23), + new(4, 19), + new(12, 19), + new(12, 23) + }, + result.Points); + } + } +} diff --git a/src/cs/Tests/MonoGame.Extended.Tests/Primitives/ShapeTests.cs b/src/cs/Tests/MonoGame.Extended.Tests/Primitives/ShapeTests.cs index bb5e318b..37b2fc4c 100644 --- a/src/cs/Tests/MonoGame.Extended.Tests/Primitives/ShapeTests.cs +++ b/src/cs/Tests/MonoGame.Extended.Tests/Primitives/ShapeTests.cs @@ -1,8 +1,11 @@ -using Xunit; +using Microsoft.Xna.Framework; +using Xunit; -namespace MonoGame.Extended.Tests.Primitives +namespace MonoGame.Extended.Tests.Primitives; + +public class ShapeTests { - public class ShapeTests + public class CircleFTests { [Fact] public void CircCircIntersectionSameCircleTest() @@ -30,7 +33,10 @@ namespace MonoGame.Extended.Tests.Primitives Assert.False(shape1.Intersects(shape2)); } + } + public class RectangleFTests + { [Fact] public void RectRectSameRectTest() { @@ -68,7 +74,6 @@ namespace MonoGame.Extended.Tests.Primitives Assert.True(shape2.Intersects(shape1)); } - [Fact] public void RectCircOnEdgeTest() { @@ -79,4 +84,97 @@ namespace MonoGame.Extended.Tests.Primitives Assert.True(shape2.Intersects(shape1)); } } -} \ No newline at end of file + + public class OrientedRectangleTests + { + [Fact] + public void Axis_aligned_rectangle_intersects_circle() + { + /* + * : + * : + * +*+ + * ...........* *......... + * +*+ + * : + * : + */ + IShapeF rectangle = new OrientedRectangle(Point2.Zero, new Size2(1, 1), Matrix2.Identity); + var circle = new CircleF(Point2.Zero, 1); + + Assert.True(rectangle.Intersects(circle)); + } + + [Fact] + public void Axis_aligned_rectangle_does_not_intersect_circle_in_top_left() + { + /* + * * : + * * *: + * *+-+ + * ...........| |......... + * +-+ + * : + * : + */ + IShapeF rectangle = new OrientedRectangle(Point2.Zero, new Size2(1, 1), Matrix2.Identity); + var circle = new CircleF(new Point2(-2, 1), .99f); + + Assert.False(rectangle.Intersects(circle)); + } + + [Fact] + public void Rectangle_rotated_45_degrees_does_not_intersect_circle_in_bottom_right() + { + /* + * : + * : + * +-. + * .........../ / ........ + * +./* * + * * * + * :* * + */ + IShapeF rectangle = new OrientedRectangle(new Point2(-1, 1), new Size2(1.42f, 1.42f), Matrix2.CreateRotationZ(-MathHelper.PiOver4)); + var circle = new CircleF(new Point2(1, -1), 1.4f); + + Assert.False(rectangle.Intersects(circle)); + } + + [Fact] + public void Axis_aligned_rectangle_does_not_intersect_rectangle() + { + /* + * : + * : + * +-+ + * ..........| |**....... + * +-+ * + * :** + * : + */ + IShapeF rectangle = new OrientedRectangle(new Point2(-1, 0), new Size2(1, 1), Matrix2.Identity); + var rect = new RectangleF(new Point2(0.001f, 0), new Size2(2, 2)); + + Assert.False(rectangle.Intersects(rect)); + } + + [Fact] + public void Axis_aligned_rectangle_intersects_rectangle() + { + /* + * : + * : + * +-+ + * ..........| |**....... + * +-+ * + * :** + * : + */ + IShapeF rectangle = new OrientedRectangle(new Point2(-1, 0), new Size2(1, 1), Matrix2.Identity); + var rect = new RectangleF(new Point2(0, 0), new Size2(2, 2)); + + Assert.True(rectangle.Intersects(rect)); + } + } +} diff --git a/src/cs/Tests/MonoGame.Extended.Tiled.Tests/MonoGame.Extended.Tiled.Tests.csproj b/src/cs/Tests/MonoGame.Extended.Tiled.Tests/MonoGame.Extended.Tiled.Tests.csproj index a6fc12ae..675552ab 100644 --- a/src/cs/Tests/MonoGame.Extended.Tiled.Tests/MonoGame.Extended.Tiled.Tests.csproj +++ b/src/cs/Tests/MonoGame.Extended.Tiled.Tests/MonoGame.Extended.Tiled.Tests.csproj @@ -7,9 +7,9 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/MonoGame.Extended.Tweening.Tests/ColorHandler.cs b/src/cs/Tests/MonoGame.Extended.Tweening.Tests/ColorHandler.cs similarity index 100% rename from MonoGame.Extended.Tweening.Tests/ColorHandler.cs rename to src/cs/Tests/MonoGame.Extended.Tweening.Tests/ColorHandler.cs diff --git a/MonoGame.Extended.Tweening.Tests/MonoGame.Extended.Tweening.Tests.csproj b/src/cs/Tests/MonoGame.Extended.Tweening.Tests/MonoGame.Extended.Tweening.Tests.csproj similarity index 83% rename from MonoGame.Extended.Tweening.Tests/MonoGame.Extended.Tweening.Tests.csproj rename to src/cs/Tests/MonoGame.Extended.Tweening.Tests/MonoGame.Extended.Tweening.Tests.csproj index cfe82093..467d4bc6 100644 --- a/MonoGame.Extended.Tweening.Tests/MonoGame.Extended.Tweening.Tests.csproj +++ b/src/cs/Tests/MonoGame.Extended.Tweening.Tests/MonoGame.Extended.Tweening.Tests.csproj @@ -10,9 +10,9 @@ - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -23,7 +23,7 @@ - + diff --git a/MonoGame.Extended.Tweening.Tests/TweenerTests.cs b/src/cs/Tests/MonoGame.Extended.Tweening.Tests/TweenerTests.cs similarity index 100% rename from MonoGame.Extended.Tweening.Tests/TweenerTests.cs rename to src/cs/Tests/MonoGame.Extended.Tweening.Tests/TweenerTests.cs