// Copyright (c) Craftwork Games. All rights reserved. // Licensed under the MIT license. // See LICENSE file in the project root for full license information. using System; using System.Diagnostics; using System.Runtime.Serialization; using Microsoft.Xna.Framework; namespace MonoGame.Extended { /// /// Represents an axis-aligned bounding box in 2D space, defined by minimum and maximum corner points. /// [DataContract] [DebuggerDisplay("{DebugDisplayString,nq}")] public struct BoundingBox2D : IEquatable { /// /// The number of corners in this bounding box. /// public const int CornerCount = 4; #region Public Fields /// /// The minimum corner position with the smallest X and Y coordinates. /// [DataMember] public Vector2 Min; /// /// The maximum corner position with the largest X and Y coordinates. /// [DataMember] public Vector2 Max; #endregion #region Public Properties /// /// Gets the center position of this bounding box in 2D space. /// public readonly Vector2 Center => (Min + Max) * 0.5f; /// /// Gets the size of this bounding box as width and height. /// public readonly Vector2 Size => Max - Min; /// /// Gets the half extents of this bounding box, representing the distance from the center to each edge. /// public readonly Vector2 HalfExtents => (Max - Min) * 0.5f; /// /// Gets the width of this bounding box. /// public readonly float Width => Max.X - Min.X; /// /// Gets the height of this bounding box. /// public readonly float Height => Max.Y - Min.Y; /// /// Gets the area enclosed by this bounding box. /// public readonly float Area => Width * Height; #endregion #region Internal Properties internal string DebugDisplayString { get { return string.Concat( "Min( ", Min.ToString(), " ) \r\n", "Max( ", Max.ToString(), " )" ); } } #endregion #region Public Constructors /// /// Creates a new with the specified minimum and maximum corners. /// /// The minimum corner, containing the smallest X and Y coordinate values. /// The maximum corner, containing the largest X and Y coordinate values. public BoundingBox2D(Vector2 min, Vector2 max) { Min = min; Max = max; } #endregion #region Public Methods /// /// Creates a new from the specified minimum and maximum corners. /// /// The minimum corner, containing the smallest X and Y coordinate values. /// The maximum corner, containing the largest X and Y coordinate values. /// /// A new with the specified corners. /// public static BoundingBox2D CreateFromMinMax(Vector2 min, Vector2 max) { return new BoundingBox2D(min, max); } /// /// Creates a new from a center position and half extents. /// /// The center position of the bounding box in 2D space. /// /// The half extents representing the distance from the center to each edge (half-width and half-height). /// /// /// A new centered at the specified position with the given extents. /// public static BoundingBox2D CreateFromCenterAndExtents(Vector2 center, Vector2 halfExtents) { return new BoundingBox2D(center - halfExtents, center + halfExtents); } /// /// Creates a new from a minimum corner position and size. /// /// The minimum corner position in 2D space. /// The size as width and height. /// /// A new with the specified position and size. /// public static BoundingBox2D CreateFromPositionAndSize(Vector2 position, Vector2 size) { return new BoundingBox2D(position, position + size); } /// /// Creates a new that encloses all specified points. /// /// The array of points to enclose within the bounding box. /// /// A new with minimum and maximum corners positioned to contain all the specified points. /// /// /// Thrown when is . /// /// /// Thrown when is empty. /// public static BoundingBox2D CreateFromPoints(Vector2[] points) { if (points == null) { throw new ArgumentNullException(nameof(points)); } if (points.Length == 0) { throw new ArgumentException("Cannot create bounding box from empty point array.", nameof(points)); } Vector2 min = new Vector2(float.MaxValue, float.MaxValue); Vector2 max = new Vector2(float.MinValue, float.MinValue); for (int i = 0; i < points.Length; i++) { if (points[i].X < min.X) min.X = points[i].X; if (points[i].Y < min.Y) min.Y = points[i].Y; if (points[i].X > max.X) max.X = points[i].X; if (points[i].Y > max.Y) max.Y = points[i].Y; } return new BoundingBox2D(min, max); } /// /// Creates a new that encloses two bounding boxes. /// /// The first bounding box to enclose. /// The second bounding box to enclose. /// /// A new that completely contains both input bounding boxes. /// public static BoundingBox2D CreateMerged(BoundingBox2D original, BoundingBox2D additional) { BoundingBox2D result; result.Min.X = MathF.Min(original.Min.X, additional.Min.X); result.Min.Y = MathF.Min(original.Min.Y, additional.Min.Y); result.Max.X = MathF.Max(original.Max.X, additional.Max.X); result.Max.Y = MathF.Max(original.Max.Y, additional.Max.Y); return result; } /// /// Gets an array containing the four corner positions of this bounding box. /// /// /// An array of 4 corner positions in counter-clockwise order starting from the minimum corner: /// bottom-left, bottom-right, top-right, top-left. /// public readonly Vector2[] GetCorners() { return new Vector2[CornerCount] { new Vector2(Min.X, Min.Y), new Vector2(Max.X, Min.Y), new Vector2(Max.X, Max.Y), new Vector2(Min.X, Max.Y) }; } /// /// Writes the four corner positions of this bounding box into an existing array. /// /// /// The array to write corner positions into. Must have at least 4 elements. /// Corners are written in counter-clockwise order starting from the minimum corner. /// /// /// Thrown when is . /// /// /// Thrown when has fewer than 4 elements. /// public readonly void GetCorners(Vector2[] corners) { if (corners == null) throw new ArgumentNullException(nameof(corners)); if (corners.Length < CornerCount) throw new ArgumentException($"Array must have at least {CornerCount} elements", nameof(corners)); corners[0] = new Vector2(Min.X, Min.Y); corners[1] = new Vector2(Max.X, Min.Y); corners[2] = new Vector2(Max.X, Max.Y); corners[3] = new Vector2(Min.X, Max.Y); } /// /// Applies a matrix transformation to this bounding box and creates a new axis-aligned bounding box that encloses the result. /// /// The transformation matrix to apply. /// /// A new axis-aligned that completely contains all corners of this bounding box after transformation. /// /// /// When a transformation includes rotation or skew, the transformed corners of the original box may no longer /// be axis-aligned. This method computes a new axis-aligned bounding box that encloses all transformed corners, /// which may be larger than the original if rotation is applied. /// public readonly BoundingBox2D Transform(Matrix matrix) { Vector2 transformedCenter = Vector2.Transform(Center, matrix); // For each axis, sum the absolute values of the transformed half-extents. // This gives us the new box that bounds the rotated original Vector2 newHalfExtents; newHalfExtents.X = MathF.Abs(matrix.M11) * HalfExtents.X + MathF.Abs(matrix.M12) * HalfExtents.Y; newHalfExtents.Y = MathF.Abs(matrix.M21) * HalfExtents.X + MathF.Abs(matrix.M22) * HalfExtents.Y; return CreateFromCenterAndExtents(transformedCenter, newHalfExtents); } /// /// Creates a new by translating this bounding box by the specified offset. /// /// The offset to translate the bounding box by in 2D space. /// /// A new at the translated position with the same size. /// public readonly BoundingBox2D Translate(Vector2 translation) { return new BoundingBox2D(Min + translation, Max + translation); } /// /// Tests whether a point lies inside this bounding box or on its boundary. /// /// The point to test in 2D space. /// /// if the point is inside or on the boundary; /// otherwise, if the point is outside. /// public readonly ContainmentType Contains(Vector2 point) { return Collision2D.ContainsAabbPoint(point, Min, Max); } /// /// Tests whether this bounding box contains, intersects, or is separate from another bounding box. /// /// The other bounding box to test against. /// /// if the other bounding box is completely inside this one; /// if they partially overlap; /// or if they do not touch. /// public readonly ContainmentType Contains(BoundingBox2D other) { return Collision2D.ContainsAabbAabb(Min, Max, other.Min, other.Max); } /// /// Tests whether this bounding box contains, intersects, or is separate from a circle. /// /// The circle to test against. /// /// if the circle is completely inside this bounding box; /// if they partially overlap; /// or if they do not touch. /// public readonly ContainmentType Contains(BoundingCircle2D circle) { return Collision2D.ContainsAabbCircle(Min, Max, circle.Center, circle.Radius); } /// /// Tests whether this bounding box contains, intersects, or is separate from an oriented bounding box. /// /// The oriented bounding box to test against. /// /// if the oriented bounding box is completely inside this bounding box; /// if they partially overlap; /// or if they do not touch. /// public readonly ContainmentType Contains(OrientedBoundingBox2D obb) { return Collision2D.ContainsAabbObb(Min, Max, obb.Center, obb.AxisX, obb.AxisY, obb.HalfExtents); } /// /// Tests whether this bounding box contains, intersects, or is separate from a capsule. /// /// The capsule to test against. /// /// if the capsule is completely inside this bounding box; /// if they partially overlap; /// or if they do not touch. /// public readonly ContainmentType Contains(BoundingCapsule2D capsule) { return Collision2D.ContainsAabbCapsule(Min, Max, capsule.PointA, capsule.PointB, capsule.Radius); } /// /// Tests whether this bounding box contains, intersects, or is separate from a polygon. /// /// The polygon to test against. /// /// if the polygon is completely inside this bounding box; /// if they partially overlap; /// or if they do not touch. /// public readonly ContainmentType Contains(BoundingPolygon2D polygon) { return Collision2D.ContainsAabbConvexPolygon(Min, Max, polygon.Vertices, polygon.Normals); } /// /// Tests whether this bounding box intersects with another bounding box. /// /// The other bounding box to test against. /// /// if the bounding boxes overlap or touch; otherwise, . /// public readonly bool Intersects(BoundingBox2D other) { return Collision2D.IntersectsAabbAabb(Min, Max, other.Min, other.Max); } /// /// Tests whether this bounding box intersects with a circle. /// /// The circle to test against. /// /// if the bounding box and circle overlap or touch; otherwise, . /// public readonly bool Intersects(BoundingCircle2D circle) { return Collision2D.IntersectsCircleAabb(circle.Center, circle.Radius, Min, Max); } /// /// Tests whether this bounding box intersects with a capsule. /// /// The capsule to test against. /// /// if the bounding box and capsule overlap or touch; otherwise, . /// public readonly bool Intersects(BoundingCapsule2D capsule) { return Collision2D.IntersectsAabbCapsule(Min, Max, capsule.PointA, capsule.PointB, capsule.Radius); } /// /// Tests whether this bounding box intersects with an oriented bounding box. /// /// The oriented bounding box to test against. /// /// if the boxes overlap or touch; otherwise, . /// public readonly bool Intersects(OrientedBoundingBox2D obb) { return Collision2D.IntersectsAabbObb(Center, HalfExtents, obb.Center, obb.AxisX, obb.AxisY, obb.HalfExtents); } /// /// Tests whether this bounding box intersects with a polygon. /// /// The polygon to test against. /// /// if the box and polygon overlap or touch; otherwise, . /// public readonly bool Intersects(BoundingPolygon2D polygon) { return Collision2D.IntersectsAabbConvexPolygon(Center, HalfExtents, polygon.Vertices, polygon.Normals); } /// /// Deconstructs this bounding box into its component values. /// /// /// When this method returns, contains the minimum corner, containing the smallest X and Y coordinate values. /// /// /// When this method returns, contains the maximum corner, containing the largest X and Y coordinate values. /// public readonly void Deconstruct(out Vector2 min, out Vector2 max) { min = Min; max = Max; } /// public readonly bool Equals(BoundingBox2D other) { return Min.Equals(other.Min) && Max.Equals(other.Max); } /// public override readonly bool Equals(object obj) { return obj is BoundingBox2D other && Equals(other); } /// public override readonly int GetHashCode() { return Min.GetHashCode() ^ Max.GetHashCode(); } /// public override readonly string ToString() { return $"{{Min:{Min} Max:{Max}}}"; } /// public static bool operator ==(BoundingBox2D left, BoundingBox2D right) { return left.Equals(right); } /// public static bool operator !=(BoundingBox2D left, BoundingBox2D right) { return !left.Equals(right); } #endregion } }