Merge branch 'tiled_animation_flip_fix' of https://github.com/tigurx/MonoGame.Extended into tiled_animation_flip_fix

This commit is contained in:
jimmy b
2024-05-18 13:17:47 -05:00
40 changed files with 939 additions and 132 deletions
+1 -1
View File
@@ -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
+3
View File
@@ -12,3 +12,6 @@
# cake build output
artifacts
# Visual Studio
.vs/*
+22 -22
View File
@@ -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}
+1 -2
View File
@@ -14,7 +14,7 @@ public static class Program
{
return new CakeHost()
.UseContext<BuildContext>()
.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 {}
@@ -181,44 +181,22 @@ namespace MonoGame.Extended.Collisions
/// <returns>The distance vector from the edge of b to a's Position</returns>
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
}
}
Binary file not shown.
@@ -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);
/// <summary>
/// Gets whether the given key was down on the previous state, but is now up.
/// </summary>
/// <param name="key">The key to check.</param>
/// <returns>true if the key was released this state-change, otherwise false.</returns>
[Obsolete($"Deprecated in favor of {nameof(IsKeyReleased)}")]
public bool WasKeyJustDown(Keys key) => _previousKeyboardState.IsKeyDown(key) && _currentKeyboardState.IsKeyUp(key);
/// <summary>
/// Gets whether the given key was up on the previous state, but is now down.
/// </summary>
/// <param name="key">The key to check.</param>
/// <returns>true if the key was pressed this state-change, otherwise false.</returns>
[Obsolete($"Deprecated in favor of {nameof(IsKeyPressed)}")]
public bool WasKeyJustUp(Keys key) => _previousKeyboardState.IsKeyUp(key) && _currentKeyboardState.IsKeyDown(key);
public bool WasAnyKeyJustDown() => _previousKeyboardState.GetPressedKeys().Any();
/// <summary>
/// Gets whether the given key was down on the previous state, but is now up.
/// </summary>
/// <param name="key">The key to check.</param>
/// <returns>true if the key was released this state-change, otherwise false.</returns>
public readonly bool IsKeyReleased(Keys key) => _previousKeyboardState.IsKeyDown(key) && _currentKeyboardState.IsKeyUp(key);
/// <summary>
/// Gets whether the given key was up on the previous state, but is now down.
/// </summary>
/// <param name="key">The key to check.</param>
/// <returns>true if the key was pressed this state-change, otherwise false.</returns>
public readonly bool IsKeyPressed(Keys key) => _previousKeyboardState.IsKeyUp(key) && _currentKeyboardState.IsKeyDown(key);
public bool WasAnyKeyJustDown() => _previousKeyboardState.GetPressedKeyCount() > 0;
}
}
}
@@ -63,6 +63,13 @@ namespace MonoGame.Extended.Input
return false;
}
/// <summary>
/// Get the just-down state for the mouse on this state-change: true if the mouse button has just been pressed.
/// </summary>
/// <param name="button"></param>
/// <remarks>Deprecated because of inconsistency with <see cref="KeyboardStateExtended"/></remarks>
/// <returns>The just-down state for the mouse on this state-change.</returns>
[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;
}
/// <summary>
/// Get the just-up state for the mouse on this state-change: true if the mouse button has just been released.
/// </summary>
/// <param name="button"></param>
/// <remarks>Deprecated because of inconsistency with <see cref="KeyboardStateExtended"/></remarks>
/// <returns>The just-up state for the mouse on this state-change.</returns>
[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<MouseState, ButtonState> button) => button(_currentMouseState) == ButtonState.Pressed;
private bool IsReleased(Func<MouseState, ButtonState> button) => button(_currentMouseState) == ButtonState.Released;
private bool WasJustPressed(Func<MouseState, ButtonState> button) => button(_previousMouseState) == ButtonState.Released && button(_currentMouseState) == ButtonState.Pressed;
private bool WasJustReleased(Func<MouseState, ButtonState> button) => button(_previousMouseState) == ButtonState.Pressed && button(_currentMouseState) == ButtonState.Released;
/// <summary>
/// Get the pressed state of a mouse button, for this state-change.
/// </summary>
/// <param name="button">The button to check.</param>
/// <returns>true if the given mouse button was pressed this state-change, otherwise false</returns>
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,
};
/// <summary>
/// Get the released state of a mouse button, for this state-change.
/// </summary>
/// <param name="button">The button to check.</param>
/// <returns>true if the given mouse button was released this state-change, otherwise false</returns>
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<MouseState, ButtonState> button)
=> button(_currentMouseState) == ButtonState.Pressed;
private readonly bool IsReleased(Func<MouseState, ButtonState> button)
=> button(_currentMouseState) == ButtonState.Released;
private readonly bool WasJustPressed(Func<MouseState, ButtonState> button)
=> button(_previousMouseState) == ButtonState.Released && button(_currentMouseState) == ButtonState.Pressed;
private readonly bool WasJustReleased(Func<MouseState, ButtonState> button)
=> button(_previousMouseState) == ButtonState.Pressed && button(_currentMouseState) == ButtonState.Released;
}
}
}
+10 -10
View File
@@ -425,13 +425,13 @@ namespace MonoGame.Extended
}
/// <summary>
/// Performs an implicit conversion from a <see cref="CircleF" /> to a <see cref="Rectangle" />.
/// Performs an explicit conversion from a <see cref="CircleF" /> to a <see cref="Rectangle" />.
/// </summary>
/// <param name="circle">The circle.</param>
/// <returns>
/// The resulting <see cref="Rectangle" />.
/// </returns>
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
/// </returns>
public Rectangle ToRectangle()
{
return this;
return (Rectangle)this;
}
/// <summary>
/// Performs an implicit conversion from a <see cref="Rectangle" /> to a <see cref="CircleF" />.
/// Performs an explicit conversion from a <see cref="Rectangle" /> to a <see cref="CircleF" />.
/// </summary>
/// <param name="rectangle">The rectangle.</param>
/// <returns>
/// The resulting <see cref="CircleF" />.
/// </returns>
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
}
/// <summary>
/// Performs an implicit conversion from a <see cref="CircleF" /> to a <see cref="RectangleF" />.
/// Performs an explicit conversion from a <see cref="CircleF" /> to a <see cref="RectangleF" />.
/// </summary>
/// <param name="circle">The circle.</param>
/// <returns>
/// The resulting <see cref="RectangleF" />.
/// </returns>
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
/// </returns>
public RectangleF ToRectangleF()
{
return this;
return (RectangleF)this;
}
/// <summary>
/// Performs an implicit conversion from a <see cref="RectangleF" /> to a <see cref="CircleF" />.
/// Performs an explicit conversion from a <see cref="RectangleF" /> to a <see cref="CircleF" />.
/// </summary>
/// <param name="rectangle">The rectangle.</param>
/// <returns>
/// The resulting <see cref="CircleF" />.
/// </returns>
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;
@@ -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.
/// <summary>
/// An oriented bounding rectangle is a rectangular block, much like a bounding rectangle
/// <see cref="BoundingRectangle" /> but with an arbitrary orientation <see cref="Vector2" />.
/// </summary>
/// <seealso cref="IEquatable{T}" />
[DebuggerDisplay($"{{{nameof(DebugDisplayString)},nq}}")]
public struct OrientedRectangle : IEquatable<OrientedRectangle>, IShapeF
{
/// <summary>
/// The centre position of this <see cref="OrientedRectangle" />.
/// </summary>
public Point2 Center;
/// <summary>
/// The distance from the <see cref="Center" /> point along both axes to any point on the boundary of this
/// <see cref="OrientedRectangle" />.
/// </summary>
///
public Vector2 Radii;
/// <summary>
/// The rotation matrix <see cref="Matrix2" /> of the bounding rectangle <see cref="OrientedRectangle" />.
/// </summary>
public Matrix2 Orientation;
/// <summary>
/// Initializes a new instance of the <see cref="BoundingRectangle" /> structure from the specified centre
/// <see cref="Point2" /> and the radii <see cref="Size2" />.
/// </summary>
/// <param name="center">The centre <see cref="Point2" />.</param>
/// <param name="radii">The radii <see cref="Vector2" />.</param>
/// <param name="orientation">The orientation <see cref="Matrix2" />.</param>
public OrientedRectangle(Point2 center, Size2 radii, Matrix2 orientation)
{
Center = center;
Radii = radii;
Orientation = orientation;
}
/// <summary>
/// Gets a list of points defining the corner points of the oriented rectangle.
/// </summary>
public IReadOnlyList<Vector2> 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>
{
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;
/// <summary>
/// Computes the <see cref="OrientedRectangle"/> from the specified <paramref name="rectangle"/>
/// transformed by <paramref name="transformMatrix"/>.
/// </summary>
/// <param name="rectangle">The <see cref="OrientedRectangle"/> to transform.</param>
/// <param name="transformMatrix">The <see cref="Matrix2"/> transformation.</param>
/// <returns>A new <see cref="OrientedRectangle"/>.</returns>
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;
}
/// <summary>
/// Compares to two <see cref="OrientedRectangle"/> structures. The result specifies whether the
/// the values of the <see cref="Center"/>, <see cref="Radii"/> and <see cref="Orientation"/> are
/// equal.
/// </summary>
/// <param name="left">The left <see cref="OrientedRectangle" />.</param>
/// <param name="right">The right <see cref="OrientedRectangle" />.</param>
/// <returns><c>true</c> if left and right argument are equal; otherwise, <c>false</c>.</returns>
public static bool operator ==(OrientedRectangle left, OrientedRectangle right)
{
return left.Equals(right);
}
/// <summary>
/// Compares to two <see cref="OrientedRectangle"/> structures. The result specifies whether the
/// the values of the <see cref="Center"/>, <see cref="Radii"/> or <see cref="Orientation"/> are
/// unequal.
/// </summary>
/// <param name="left">The left <see cref="OrientedRectangle" />.</param>
/// <param name="right">The right <see cref="OrientedRectangle" />.</param>
/// <returns><c>true</c> if left and right argument are unequal; otherwise, <c>false</c>.</returns>
public static bool operator !=(OrientedRectangle left, OrientedRectangle right)
{
return !left.Equals(right);
}
/// <summary>
/// Determines whether two instances of <see cref="OrientedRectangle"/> are equal.
/// </summary>
/// <param name="other">The other <see cref="OrientedRectangle"/>.</param>
/// <returns><c>true</c> if the specified <see cref="OrientedRectangle"/> is equal
/// to the current <see cref="OrientedRectangle"/>; otherwise, <c>false</c>.</returns>
public bool Equals(OrientedRectangle other)
{
return Center.Equals(other.Center) && Radii.Equals(other.Radii) && Orientation.Equals(other.Orientation);
}
/// <summary>
/// Determines whether two instances of <see cref="OrientedRectangle"/> are equal.
/// </summary>
/// <param name="obj">The <see cref="OrientedRectangle"/> to compare to.</param>
/// <returns><c>true</c> if the specified <see cref="OrientedRectangle"/> is equal
/// to the current <see cref="OrientedRectangle"/>; otherwise, <c>false</c>.</returns>
public override bool Equals(object obj)
{
return obj is OrientedRectangle other && Equals(other);
}
/// <summary>
/// Returns a hash code for this object instance.
/// </summary>
/// <returns></returns>
public override int GetHashCode()
{
return HashCode.Combine(Center, Radii, Orientation);
}
/// <summary>
/// Performs an implicit conversion from a <see cref="RectangleF" /> to <see cref="OrientedRectangle" />.
/// </summary>
/// <param name="rectangle">The rectangle to convert.</param>
/// <returns>The resulting <see cref="OrientedRectangle" />.</returns>
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);
}
/// <summary>
/// See:
/// https://www.flipcode.com/archives/2D_OBB_Intersection.shtml
/// https://dyn4j.org/2010/01/sat
/// </summary>
/// <param name="rectangle"></param>
/// <param name="other"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
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<Vector2> 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);
}
}
/// <summary>
/// Returns a <see cref="string" /> that represents this <see cref="OrientedRectangle" />.
/// </summary>
/// <returns>
/// A <see cref="string" /> that represents this <see cref="OrientedRectangle" />.
/// </returns>
public override string ToString()
{
return $"Centre: {Center}, Radii: {Radii}, Orientation: {Orientation}";
}
internal string DebugDisplayString => ToString();
}
}
@@ -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)
{
+1 -1
View File
@@ -664,7 +664,7 @@ namespace MonoGame.Extended
}
/// <summary>
/// Performs an implicit conversion from a <see cref="Rectangle" /> to a <see cref="RectangleF" />.
/// Performs an explicit conversion from a <see cref="Rectangle" /> to a <see cref="RectangleF" />.
/// </summary>
/// <param name="rectangle">The rectangle.</param>
/// <returns>
+48 -22
View File
@@ -1,4 +1,7 @@
namespace MonoGame.Extended
using System;
using Microsoft.Xna.Framework;
namespace MonoGame.Extended
{
/// <summary>
/// Base class for shapes.
@@ -27,31 +30,28 @@
/// <summary>
/// Check if two shapes intersect.
/// </summary>
/// <param name="shapeA">The first shape.</param>
/// <param name="shapeB">The second shape.</param>
/// <param name="a">The first shape.</param>
/// <param name="b">The second shape.</param>
/// <returns>True if the two shapes intersect.</returns>
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))
};
}
/// <summary>
@@ -65,5 +65,31 @@
var closestPoint = rectangle.ClosestPointTo(circle.Center);
return circle.Contains(closestPoint);
}
/// <summary>
/// Checks whether a <see cref="CircleF"/> and <see cref="OrientedRectangle"/> intersects.
/// </summary>
/// <param name="circle"><see cref="CircleF"/>to use in intersection test.</param>
/// <param name="orientedRectangle"><see cref="OrientedRectangle"/>to use in intersection test.</param>
/// <returns>True if the circle and oriented bounded rectangle intersects, otherwise false.</returns>
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);
}
/// <summary>
/// Checks if a <see cref="RectangleF"/> and <see cref="OrientedRectangle"/> intersects.
/// </summary>
/// <param name="rectangleF"></param>
/// <param name="orientedRectangle"></param>
/// <returns>True if objects are intersecting, otherwise false.</returns>
public static (bool Intersects, Vector2 MinimumTranslationVector) Intersects(RectangleF rectangleF, OrientedRectangle orientedRectangle)
{
return OrientedRectangle.Intersects(orientedRectangle, (OrientedRectangle)rectangleF);
}
}
}
@@ -8,9 +8,9 @@
<ItemGroup>
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.1.303" />
<PackageReference Include="NSubstitute" Version="4.2.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.2">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="xunit" Version="2.6.6" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.6">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
@@ -40,9 +40,9 @@
<PackageReference Include="MonoGame.Framework.Content.Pipeline" Version="3.8.1.303" />
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.1.303" />
<PackageReference Include="NSubstitute" Version="4.2.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.2">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="xunit" Version="2.6.6" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.6">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
@@ -8,9 +8,9 @@
<PackageReference Include="MonoGame.Framework.Content.Pipeline" Version="3.8.1.303" />
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.1.303" />
<PackageReference Include="NSubstitute" Version="4.2.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.2">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="xunit" Version="2.6.6" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.6">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
@@ -7,9 +7,9 @@
<ItemGroup>
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.1.303" />
<PackageReference Include="NSubstitute" Version="4.2.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.2">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="xunit" Version="2.6.6" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.6">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
@@ -7,9 +7,9 @@
<ItemGroup>
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.1.303" />
<PackageReference Include="NSubstitute" Version="4.2.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.2">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="xunit" Version="2.6.6" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.6">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
@@ -0,0 +1,14 @@
using System.Collections.Generic;
using Xunit;
namespace MonoGame.Extended.Tests;
public static class CollectionAssert
{
public static void Equal<T>(IReadOnlyList<T> expected, IReadOnlyList<T> 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));
}
}
@@ -7,9 +7,9 @@
<ItemGroup>
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.1.303" />
<PackageReference Include="NSubstitute" Version="4.2.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.2">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="xunit" Version="2.6.6" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.6">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
@@ -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<Vector2>
{
new(-3, -2),
new(-33, -38),
new(23, 26),
new(53, 62)
},
rectangle.Points);
}
public static readonly IEnumerable<object[]> _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<Vector2>
{
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<Vector2>
{
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<Vector2>
{
new(4, 23),
new(4, 19),
new(12, 19),
new(12, 23)
},
result.Points);
}
}
}
@@ -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));
}
}
}
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));
}
}
}
@@ -7,9 +7,9 @@
<ItemGroup>
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.1.303" />
<PackageReference Include="NSubstitute" Version="4.2.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.2">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="xunit" Version="2.6.6" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.6">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
@@ -10,9 +10,9 @@
<ItemGroup>
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.1.303" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0"/>
<PackageReference Include="xunit" Version="2.4.1"/>
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="xunit" Version="2.6.6" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.6">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
@@ -23,7 +23,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\src\cs\MonoGame.Extended.Tweening\MonoGame.Extended.Tweening.csproj" />
<ProjectReference Include="..\..\MonoGame.Extended.Tweening\MonoGame.Extended.Tweening.csproj" />
</ItemGroup>
</Project>