mirror of
https://github.com/AlexMacocian/MonoGame.Extended.git
synced 2026-07-15 15:09:29 +00:00
Merge branch 'tiled_animation_flip_fix' of https://github.com/tigurx/MonoGame.Extended into tiled_animation_flip_fix
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -12,3 +12,6 @@
|
||||
|
||||
# cake build output
|
||||
artifacts
|
||||
|
||||
# Visual Studio
|
||||
.vs/*
|
||||
|
||||
+22
-22
@@ -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}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Regular → Executable
BIN
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -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>
|
||||
|
||||
+3
-3
@@ -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>
|
||||
|
||||
+3
-3
@@ -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>
|
||||
|
||||
+3
-3
@@ -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>
|
||||
|
||||
+4
-4
@@ -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>
|
||||
Reference in New Issue
Block a user