This is a rectangular box defined by minimum and maximum corners + * that can be used to represent a collection of blocks. + * + *
While similar to Bukkit's {@link org.bukkit.util.BoundingBox BoundingBox},
+ * this implementation is much more focused on performance and does not use as
+ * many input sanitization operations.
+ *
+ * @author Jikoo
+ */
+public class BoundingBox implements Cloneable
+{
+
+ /**
+ * Construct a new bounding box containing all of the given blocks.
+ *
+ * @param blocks a collection of blocks to construct a bounding box around
+ * @return the bounding box
+ */
+ public static BoundingBox ofBlocks(Collection Note that center coordinates are world coordinates
+ * while all of the other coordinates are block coordinates.
+ *
+ * @return the center of the X axis
+ */
+ public double getCenterX()
+ {
+ return this.minX + (this.getLength() / 2D);
+ }
+
+ /**
+ * Gets the center of the bounding box on the Y axis.
+ *
+ * Note that center coordinates are world coordinates
+ * while all of the other coordinates are block coordinates.
+ *
+ * @return the center of the X axis
+ */
+ public double getCenterY()
+ {
+ return this.minY + (this.getHeight() / 2D);
+ }
+
+ /**
+ * Gets the center of the bounding box on the Z axis.
+ *
+ * Note that center coordinates are world coordinates
+ * while all of the other coordinates are block coordinates.
+ *
+ * @return the center of the X axis
+ */
+ public double getCenterZ()
+ {
+ return this.minZ + (this.getWidth() / 2D);
+ }
+
+ /**
+ * Gets the center of the bounding box as a vector.
+ *
+ * Note that center coordinates are world coordinates
+ * while all of the other coordinates are block coordinates.
+ *
+ * @return the center of the X axis
+ */
+ public Vector getCenter()
+ {
+ return new Vector(this.getCenterX(), this.getCenterY(), this.getCenterZ());
+ }
+
+ /**
+ * Gets the area of the base of the bounding box.
+ *
+ * The base is the lowest plane defined by the X and Z axis.
+ *
+ * @return the area of the base of the bounding box
+ */
+ public int getArea()
+ {
+ return this.getLength() * this.getWidth();
+ }
+
+ /**
+ * Gets the volume of the bounding box.
+ *
+ * @return the volume of the bounding box
+ */
+ public int getVolume()
+ {
+ return this.getArea() * getHeight();
+ }
+
+ /**
+ * Copies the dimensions and location of another bounding box.
+ *
+ * @param other the bounding box to copy
+ */
+ public void copy(BoundingBox other)
+ {
+ this.minX = other.minX;
+ this.minY = other.minY;
+ this.minZ = other.minZ;
+ this.maxX = other.maxX;
+ this.maxY = other.maxY;
+ this.maxZ = other.maxZ;
+ }
+
+ /**
+ * Changes the size the bounding box in the direction specified by the Minecraft blockface.
+ *
+ * If the specified directional magnitude is negative, the box is contracted instead.
+ *
+ * When contracting, the box does not care if the contraction would cause a negative side length.
+ * In these cases, the lowest point is redefined by the new location of the maximum corner instead.
+ *
+ * @param direction the direction to change size in
+ * @param magnitude the magnitude of the resizing
+ */
+ public void resize(BlockFace direction, int magnitude)
+ {
+ if (magnitude == 0 || direction == BlockFace.SELF) return;
+
+ Vector vector = direction.getDirection().multiply(magnitude);
+
+ // Force normalized rounding - prevents issues with non-cardinal directions.
+ int modX = NumberConversions.round(vector.getX());
+ int modY = NumberConversions.round(vector.getY());
+ int modZ = NumberConversions.round(vector.getZ());
+
+ if (modX == 0 && modY == 0 && modZ == 0) return;
+
+ // Modify correct point.
+ if (direction.getModX() > 0)
+ this.maxX += modX;
+ else
+ this.minX += modX;
+ if (direction.getModY() > 0)
+ this.maxY += modY;
+ else
+ this.minY += modY;
+ if (direction.getModZ() > 0)
+ this.maxZ += modZ;
+ else
+ this.minZ += modZ;
+
+ // If box is contracting, re-verify points in case corners have swapped.
+ if (magnitude < 0)
+ verify(this.minX, this.minY, this.minZ, this.maxX, this.maxY, this.maxZ);
+ }
+
+ /**
+ * Moves the bounding box in the direction specified by the Minecraft BlockFace and magnitude.
+ *
+ * Note that a negative direction will move in the opposite direction
+ * to the extent that the following example returns true:
+ *
+ * public boolean testBoxMove(BoundingBox box, BlockFace face, int magnitude)
+ * {
+ * BoundingBox box2 = box.clone();
+ * box.move(face, magnitude);
+ * box2.move(face.getOpposite(), -magnitude);
+ * return box.equals(box2);
+ * }
+ *
+ *
+ * @param direction the direction to move in
+ * @param magnitude the magnitude of the move
+ */
+ public void move(BlockFace direction, int magnitude)
+ {
+ if (magnitude == 0 || direction == BlockFace.SELF) return;
+
+ Vector vector = direction.getDirection().multiply(magnitude);
+
+ int blockX = NumberConversions.round(vector.getX());
+ this.minX += blockX;
+ this.maxX += blockX;
+ int blockY = NumberConversions.round(vector.getY());
+ this.minY += blockY;
+ this.maxY += blockY;
+ int blockZ = NumberConversions.round(vector.getZ());
+ this.minZ += blockZ;
+ this.maxZ += blockZ;
+ }
+
+ /**
+ * Expands the bounding box to contain the position specified.
+ *
+ * @param x the X coordinate to include
+ * @param y the Y coordinate to include
+ * @param z the Z coordinate to include
+ */
+ public void union(int x, int y, int z)
+ {
+ this.minX = Math.min(x, this.minX);
+ this.maxX = Math.max(x, this.maxX);
+ this.minY = Math.min(y, this.minY);
+ this.maxY = Math.max(y, this.maxY);
+ this.minZ = Math.min(z, this.minZ);
+ this.maxZ = Math.max(z, this.maxZ);
+ }
+
+ /**
+ * Expands the bounding box to contain the position specified.
+ *
+ * @param position the position to include
+ */
+ public void union(Block position)
+ {
+ this.union(position.getX(), position.getY(), position.getZ());
+ }
+
+ /**
+ * Expands the bounding box to contain the position specified.
+ *
+ * @param position the position to include
+ */
+ public void union(Vector position)
+ {
+ this.union(position.getBlockX(), position.getBlockY(), position.getBlockZ());
+ }
+
+ /**
+ * Expands the bounding box to contain the position specified.
+ *
+ * @param position the position to include
+ */
+ public void union(Location position)
+ {
+ this.union(position.getBlockX(), position.getBlockY(), position.getBlockZ());
+ }
+
+ /**
+ * Expands the bounding box to contain the bounding box specified.
+ *
+ * @param other the bounding box to include
+ */
+ public void union(BoundingBox other)
+ {
+ this.minX = Math.min(this.minX, other.minX);
+ this.maxX = Math.max(this.maxX, other.maxX);
+ this.minY = Math.min(this.minY, other.minY);
+ this.maxY = Math.max(this.maxY, other.maxY);
+ this.minZ = Math.min(this.minZ, other.minZ);
+ this.maxZ = Math.max(this.maxZ, other.maxZ);
+ }
+
+ /**
+ * Internal containment check.
+ *
+ * @param minX the minimum X value to check for containment
+ * @param minY the minimum Y value to check for containment
+ * @param minZ the minimum Z value to check for containment
+ * @param maxX the maximum X value to check for containment
+ * @param maxY the maximum X value to check for containment
+ * @param maxZ the maximum X value to check for containment
+ * @return true if the specified values are inside the bounding box
+ */
+ private boolean containsInternal(int minX, int minY, int minZ, int maxX, int maxY, int maxZ)
+ {
+ return minX >= this.minX && maxX <= this.maxX
+ && minY >= this.minY && maxY <= this.maxY
+ && minZ >= this.minZ && maxZ <= this.maxZ;
+ }
+
+ /**
+ * Checks if the bounding box contains the position specified.
+ *
+ * @param x the X coordinate of the position
+ * @param y the Y coordinate of the position
+ * @param z the Z coordinate of the position
+ * @return true if the specified position is inside the bounding box
+ */
+ public boolean contains(int x, int y, int z)
+ {
+ return containsInternal(x, y, z, x, y, z);
+ }
+
+ /**
+ * Checks if the bounding box contains the position specified.
+ *
+ * @param position the position
+ * @return true if the specified position is inside the bounding box
+ */
+ public boolean contains(Vector position)
+ {
+ return contains(position.getBlockX(), position.getBlockY(), position.getBlockZ());
+ }
+
+ /**
+ * Checks if the bounding box contains the position specified.
+ *
+ * @param position the position
+ * @return true if the specified position is inside the bounding box
+ */
+ public boolean contains(Location position)
+ {
+ return contains(position.getBlockX(), position.getBlockY(), position.getBlockZ());
+ }
+
+ /**
+ * Checks if the bounding box contains the position specified.
+ *
+ * @param position the position
+ * @return true if the specified position is inside the bounding box
+ */
+ public boolean contains(Block position)
+ {
+ return contains(position.getX(), position.getY(), position.getZ());
+ }
+
+ /**
+ * Checks if the bounding box contains another bounding box consisting of the positions specified.
+ *
+ * @param x1 the X coordinate of the first position
+ * @param y1 the Y coordinate of the first position
+ * @param z1 the Z coordinate of the first position
+ * @param x2 the X coordinate of the second position
+ * @param y2 the Y coordinate of the second position
+ * @param z2 the Z coordinate of the second position
+ * @return true if the specified positions are inside the bounding box
+ */
+ public boolean contains(int x1, int y1, int z1, int x2, int y2, int z2)
+ {
+ return contains(new BoundingBox(x1, y1, z1, x2, y2, z2));
+ }
+
+ /**
+ * Checks if the bounding box contains another bounding box.
+ *
+ * @param other the other bounding box
+ * @return true if the specified positions are inside the bounding box
+ */
+ public boolean contains(BoundingBox other)
+ {
+ return containsInternal(other.minX, other.minY, other.minZ, other.maxX, other.maxY, other.maxZ);
+ }
+
+ /**
+ * Checks if the bounding box intersects another bounding box.
+ *
+ * @param other the other bounding box
+ * @return true if the specified positions are inside the bounding box
+ */
+ public boolean intersects(BoundingBox other)
+ {
+ // For help visualizing test cases, try https://silentmatt.com/rectangle-intersection/
+ return this.minX <= other.maxX && this.maxX >= other.minX
+ && this.minY <= other.maxY && this.maxY >= other.minY
+ && this.minZ <= other.maxZ && this.maxZ >= other.minZ;
+ }
+
+ /**
+ * Gets a bounding box containing the intersection of the bounding box with another.
+ *
+ * @param other the other bounding box
+ * @return the bounding box representing overlapping area or null if the boxes do not overlap.
+ */
+ public BoundingBox intersection(BoundingBox other)
+ {
+ if (!intersects(other)) return null;
+
+ return new BoundingBox(
+ Math.max(this.minX, other.minX),
+ Math.max(this.minY, other.minY),
+ Math.max(this.minZ, other.minZ),
+ Math.min(this.maxX, other.maxX),
+ Math.min(this.maxY, other.maxY),
+ Math.min(this.maxZ, other.maxZ),
+ false);
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ BoundingBox other = (BoundingBox) o;
+ return this.minX == other.minX
+ && this.minY == other.minY
+ && this.minZ == other.minZ
+ && this.maxX == other.maxX
+ && this.maxY == other.maxY
+ && this.maxZ == other.maxZ;
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return Objects.hash(this.minX, this.minY, this.minZ, this.maxX, this.maxY, this.maxZ);
+ }
+
+ @Override
+ public String toString()
+ {
+ return "BoundingBox{" +
+ "minX=" + minX +
+ ", minY=" + minY +
+ ", minZ=" + minZ +
+ ", maxX=" + maxX +
+ ", maxY=" + maxY +
+ ", maxZ=" + maxZ +
+ '}';
+ }
+
+ @Override
+ public BoundingBox clone()
+ {
+ try
+ {
+ return (BoundingBox) super.clone();
+ }
+ catch (CloneNotSupportedException e)
+ {
+ throw new Error(e);
+ }
+ }
+
+}
diff --git a/src/test/java/me/ryanhamshire/GriefPrevention/ClaimTest.java b/src/test/java/me/ryanhamshire/GriefPrevention/ClaimTest.java
deleted file mode 100644
index 5fa3437..0000000
--- a/src/test/java/me/ryanhamshire/GriefPrevention/ClaimTest.java
+++ /dev/null
@@ -1,72 +0,0 @@
-package me.ryanhamshire.GriefPrevention;
-
-import org.bukkit.Location;
-import org.bukkit.World;
-import org.junit.jupiter.api.Test;
-import org.mockito.Mockito;
-
-import java.util.Collections;
-
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-public class ClaimTest {
-
- @Test
- public void testClaimOverlap()
- {
- World world = Mockito.mock(World.class);
-
- // One corner inside
- Claim claimA = new Claim(new Location(world, 0, 0, 0), new Location(world, 10, 0, 10), null,
- Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), false, 0L);
- Claim claimB = new Claim(new Location(world, 5, 0, 5), new Location(world, 15, 0, 15), null,
- Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), false, 0L);
- Claim claimC = new Claim(new Location(world, -5, 0, -5), new Location(world, 4, 0, 4), null,
- Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), false, 0L);
-
- assertTrue(claimA.overlaps(claimB));
- assertTrue(claimB.overlaps(claimA));
- assertTrue(claimA.overlaps(claimC));
- assertTrue(claimC.overlaps(claimA));
- assertFalse(claimB.overlaps(claimC));
- assertFalse(claimC.overlaps(claimB));
-
- // Complete containment
- claimA = new Claim(new Location(world, 0, 0, 0), new Location(world, 20, 0, 20), null,
- Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), false, 0L);
- claimB = new Claim(new Location(world, 5, 0, 5), new Location(world, 15, 0, 15), null,
- Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), false, 0L);
-
- assertTrue(claimA.overlaps(claimB));
- assertTrue(claimB.overlaps(claimA));
-
- // Central intersection
- claimA = new Claim(new Location(world, 0, 0, 5), new Location(world, 10, 0, 15), null,
- Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), false, 0L);
- claimB = new Claim(new Location(world, 5, 0, 0), new Location(world, 15, 0, 10), null,
- Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), false, 0L);
-
- assertTrue(claimA.overlaps(claimB));
- assertTrue(claimB.overlaps(claimA));
-
- // Linear North-South
- claimA = new Claim(new Location(world, 0, 0, 0), new Location(world, 10, 0, 10), null,
- Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), false, 0L);
- claimB = new Claim(new Location(world, 0, 0, 15), new Location(world, 15, 0, 25), null,
- Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), false, 0L);
-
- assertFalse(claimA.overlaps(claimB));
- assertFalse(claimB.overlaps(claimA));
-
- // Linear East-West
- claimA = new Claim(new Location(world, 0, 0, 0), new Location(world, 10, 0, 10), null,
- Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), false, 0L);
- claimB = new Claim(new Location(world, 15, 0, 0), new Location(world, 25, 0, 15), null,
- Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), false, 0L);
-
- assertFalse(claimA.overlaps(claimB));
- assertFalse(claimB.overlaps(claimA));
- }
-
-}
diff --git a/src/test/java/me/ryanhamshire/GriefPrevention/util/BoundingBoxTest.java b/src/test/java/me/ryanhamshire/GriefPrevention/util/BoundingBoxTest.java
new file mode 100644
index 0000000..008d82d
--- /dev/null
+++ b/src/test/java/me/ryanhamshire/GriefPrevention/util/BoundingBoxTest.java
@@ -0,0 +1,182 @@
+package me.ryanhamshire.GriefPrevention.util;
+
+import org.bukkit.block.BlockFace;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class BoundingBoxTest
+{
+ @Test
+ public void testVerify()
+ {
+ BoundingBox boxA = new BoundingBox(0, 0, 0, 10, 10, 10);
+ BoundingBox boxB = new BoundingBox(10, 0, 10, 0, 10, 0);
+ assertEquals(boxA, boxB);
+ }
+
+ @Test
+ public void testMeasurements()
+ {
+ BoundingBox boxA = new BoundingBox(-1, 0, 1, 5, 4, 3);
+ assertEquals(7, boxA.getLength());
+ assertEquals(5, boxA.getHeight());
+ assertEquals(3, boxA.getWidth());
+ assertEquals(7 * 3, boxA.getArea());
+ assertEquals(7 * 3 * 5, boxA.getVolume());
+ assertEquals(2.5, boxA.getCenterX());
+ assertEquals(2.5, boxA.getCenterY());
+ assertEquals(2.5, boxA.getCenterZ());
+ }
+
+ @Test
+ public void testCopy()
+ {
+ BoundingBox boxA = new BoundingBox(1, 2, 3, 4, 5, 6);
+ BoundingBox boxB = new BoundingBox(7, 8, 9, 10, 11, 12);
+ boxB.copy(boxA);
+ assertEquals(boxA.getMinX(), boxB.getMinX());
+ assertEquals(boxA.getMinY(), boxB.getMinY());
+ assertEquals(boxA.getMinZ(), boxB.getMinZ());
+ assertEquals(boxA.getMaxX(), boxB.getMaxX());
+ assertEquals(boxA.getMaxY(), boxB.getMaxY());
+ assertEquals(boxA.getMaxZ(), boxB.getMaxZ());
+ }
+
+ @Test
+ public void testResize()
+ {
+ testBlockfaceFunction(BoundingBox::resize,
+ new BoundingBox(0, 0, -10, 10, 10, 10));
+ }
+
+ private void testBlockfaceFunction(TriConsumer