diff --git a/runelite-api/pom.xml b/runelite-api/pom.xml
index bc24d72ca6..8f2c250c66 100644
--- a/runelite-api/pom.xml
+++ b/runelite-api/pom.xml
@@ -57,5 +57,11 @@
4.12
test
+
+ org.slf4j
+ slf4j-simple
+ 1.7.12
+ test
+
diff --git a/runelite-api/src/main/java/net/runelite/api/Model.java b/runelite-api/src/main/java/net/runelite/api/Model.java
index c287b2afd1..d229a073f7 100644
--- a/runelite-api/src/main/java/net/runelite/api/Model.java
+++ b/runelite-api/src/main/java/net/runelite/api/Model.java
@@ -103,4 +103,5 @@ public interface Model extends Renderable
int getExtremeZ();
int getXYZMag();
+ boolean isClickable();
}
diff --git a/runelite-api/src/main/java/net/runelite/api/Perspective.java b/runelite-api/src/main/java/net/runelite/api/Perspective.java
index e414e89c0d..a248f9e0bb 100644
--- a/runelite-api/src/main/java/net/runelite/api/Perspective.java
+++ b/runelite-api/src/main/java/net/runelite/api/Perspective.java
@@ -27,20 +27,19 @@ package net.runelite.api;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.Polygon;
-import java.awt.Rectangle;
-import java.awt.geom.Area;
+import java.awt.Shape;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.List;
-import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import static net.runelite.api.Constants.TILE_FLAG_BRIDGE;
import net.runelite.api.coords.LocalPoint;
+import net.runelite.api.geometry.RectangleUnion;
+import net.runelite.api.geometry.Shapes;
+import net.runelite.api.geometry.SimplePolygon;
import net.runelite.api.model.Jarvis;
-import net.runelite.api.model.Triangle;
-import net.runelite.api.model.Vertex;
import net.runelite.api.widgets.Widget;
import net.runelite.api.widgets.WidgetInfo;
@@ -562,244 +561,178 @@ public class Perspective
* Get the on-screen clickable area of {@code model} as though it's for the
* object on the tile at ({@code localX}, {@code localY}) and rotated to
* angle {@code orientation}.
- *
- * @param client the game client
- * @param model the model to calculate a clickbox for
+ * @param client the game client
+ * @param model the model to calculate a clickbox for
* @param orientation the orientation of the model (0-2048, where 0 is north)
- * @param point the coordinate of the tile
+ * @param point the coordinate of the tile
* @return the clickable area of the model
*/
- public static @Nullable Area getClickbox(@Nonnull Client client, Model model, int orientation, @Nonnull LocalPoint point)
+ @Nullable
+ public static Shape getClickbox(@Nonnull Client client, Model model, int orientation, LocalPoint point)
{
if (model == null)
{
return null;
}
- List triangles = model.getTriangles().stream()
- .map(triangle -> triangle.rotate(orientation))
- .collect(Collectors.toList());
+ int x = point.getX();
+ int y = point.getY();
+ int z = getTileHeight(client, point, client.getPlane());
- List vertices = model.getVertices().stream()
- .map(v -> v.rotate(orientation))
- .collect(Collectors.toList());
+ SimplePolygon bounds = calculateAABB(client, model, orientation, x, y, z);
- Area clickBox = get2DGeometry(client, triangles, point);
- Area visibleAABB = getAABB(client, vertices, point);
-
- if (visibleAABB == null)
+ if (bounds == null)
{
return null;
}
- clickBox.intersect(visibleAABB);
- return clickBox;
- }
-
- /**
- * Determine if a given point is off-screen.
- *
- * @param client
- * @param point
- * @return
- */
- private static boolean isOffscreen(@Nonnull Client client, @Nonnull Point point)
- {
- return (point.getX() < 0 || point.getX() >= client.getViewportWidth())
- && (point.getY() < 0 || point.getY() >= client.getViewportHeight());
- }
-
- private static @Nonnull Area get2DGeometry(
- @Nonnull Client client,
- @Nonnull List triangles,
- @Nonnull LocalPoint point
- )
- {
- int radius = 5;
- Area geometry = new Area();
-
- final int tileHeight = getTileHeight(client, point, client.getPlane());
-
- for (Triangle triangle : triangles)
+ if (model.isClickable())
{
- Vertex _a = triangle.getA();
- Point a = localToCanvas(client,
- point.getX() - _a.getX(),
- point.getY() - _a.getZ(),
- tileHeight + _a.getY());
- if (a == null)
- {
- continue;
- }
-
- Vertex _b = triangle.getB();
- Point b = localToCanvas(client,
- point.getX() - _b.getX(),
- point.getY() - _b.getZ(),
- tileHeight + _b.getY());
- if (b == null)
- {
- continue;
- }
-
- Vertex _c = triangle.getC();
- Point c = localToCanvas(client,
- point.getX() - _c.getX(),
- point.getY() - _c.getZ(),
- tileHeight + _c.getY());
- if (c == null)
- {
- continue;
- }
-
- if (isOffscreen(client, a) && isOffscreen(client, b) && isOffscreen(client, c))
- {
- continue;
- }
-
- int minX = Math.min(Math.min(a.getX(), b.getX()), c.getX());
- int minY = Math.min(Math.min(a.getY(), b.getY()), c.getY());
-
- // For some reason, this calculation is always 4 pixels short of the actual in-client one
- int maxX = Math.max(Math.max(a.getX(), b.getX()), c.getX()) + 4;
- int maxY = Math.max(Math.max(a.getY(), b.getY()), c.getY()) + 4;
-
- Rectangle clickableRect = new Rectangle(
- minX - radius, minY - radius,
- maxX - minX + radius, maxY - minY + radius
- );
-
- if (geometry.contains(clickableRect))
- {
- continue;
- }
-
- geometry.add(new Area(clickableRect));
+ return bounds;
}
- return geometry;
- }
-
- private static Area getAABB(
- @Nonnull Client client,
- @Nonnull List vertices,
- @Nonnull LocalPoint point
- )
- {
- int maxX = 0;
- int minX = 0;
- int maxY = 0;
- int minY = 0;
- int maxZ = 0;
- int minZ = 0;
-
- for (Vertex vertex : vertices)
- {
- int x = vertex.getX();
- int y = vertex.getY();
- int z = vertex.getZ();
-
- if (x > maxX)
- {
- maxX = x;
- }
- if (x < minX)
- {
- minX = x;
- }
-
- if (y > maxY)
- {
- maxY = y;
- }
- if (y < minY)
- {
- minY = y;
- }
-
- if (z > maxZ)
- {
- maxZ = z;
- }
- if (z < minZ)
- {
- minZ = z;
- }
- }
-
- int centerX = (minX + maxX) / 2;
- int centerY = (minY + maxY) / 2;
- int centerZ = (minZ + maxZ) / 2;
-
- int extremeX = (maxX - minX + 1) / 2;
- int extremeY = (maxY - minY + 1) / 2;
- int extremeZ = (maxZ - minZ + 1) / 2;
-
- if (extremeX < 32)
- {
- extremeX = 32;
- }
-
- if (extremeZ < 32)
- {
- extremeZ = 32;
- }
-
- int x1 = point.getX() - (centerX - extremeX);
- int y1 = centerY - extremeY;
- int z1 = point.getY() - (centerZ - extremeZ);
-
- int x2 = point.getX() - (centerX + extremeX);
- int y2 = centerY + extremeY;
- int z2 = point.getY() - (centerZ + extremeZ);
-
- final int tileHeight = getTileHeight(client, point, client.getPlane());
-
- Point p1 = localToCanvas(client, x1, z1, tileHeight + y1);
- Point p2 = localToCanvas(client, x1, z2, tileHeight + y1);
- Point p3 = localToCanvas(client, x2, z2, tileHeight + y1);
-
- Point p4 = localToCanvas(client, x2, z1, tileHeight + y1);
- Point p5 = localToCanvas(client, x1, z1, tileHeight + y2);
- Point p6 = localToCanvas(client, x1, z2, tileHeight + y2);
- Point p7 = localToCanvas(client, x2, z2, tileHeight + y2);
- Point p8 = localToCanvas(client, x2, z1, tileHeight + y2);
-
- List points = new ArrayList<>(8);
- points.add(p1);
- points.add(p2);
- points.add(p3);
- points.add(p4);
- points.add(p5);
- points.add(p6);
- points.add(p7);
- points.add(p8);
-
- try
- {
- points = Jarvis.convexHull(points);
- }
- catch (NullPointerException e)
- {
- // No non-null screen points for this AABB e.g. for an way off-screen model
- return null;
- }
-
- if (points == null)
+ Shapes bounds2d = calculate2DBounds(client, model, orientation, x, y, z);
+ if (bounds2d == null)
{
return null;
}
- Polygon hull = new Polygon();
- for (Point p : points)
+ for (SimplePolygon poly : bounds2d.getShapes())
{
- if (p != null)
- {
- hull.addPoint(p.getX(), p.getY());
- }
+ poly.intersectWithConvex(bounds);
}
- return new Area(hull);
+ return bounds2d;
+ }
+
+ private static SimplePolygon calculateAABB(Client client, Model m, int jauOrient, int x, int y, int z)
+ {
+ int ex = m.getExtremeX();
+ if (ex == -1)
+ {
+ // dynamic models don't get stored when they render where this normally happens
+ m.calculateBoundsCylinder();
+ m.calculateExtreme(0);
+ ex = m.getExtremeX();
+ }
+
+ int x1 = m.getCenterX();
+ int y1 = m.getCenterZ();
+ int z1 = m.getCenterY();
+
+ int ey = m.getExtremeZ();
+ int ez = m.getExtremeY();
+
+ int x2 = x1 + ex;
+ int y2 = y1 + ey;
+ int z2 = z1 + ez;
+
+ x1 -= ex;
+ y1 -= ey;
+ z1 -= ez;
+
+ int[] xa = new int[]{
+ x1, x2, x1, x2,
+ x1, x2, x1, x2
+ };
+ int[] ya = new int[]{
+ y1, y1, y2, y2,
+ y1, y1, y2, y2
+ };
+ int[] za = new int[]{
+ z1, z1, z1, z1,
+ z2, z2, z2, z2
+ };
+
+ int[] x2d = new int[8];
+ int[] y2d = new int[8];
+
+ modelToCanvas(client, 8, x, y, z, jauOrient, xa, ya, za, x2d, y2d);
+
+ return Jarvis.convexHull(x2d, y2d);
+ }
+
+ private static Shapes calculate2DBounds(Client client, Model m, int jauOrient, int x, int y, int z)
+ {
+ int[] x2d = new int[m.getVerticesCount()];
+ int[] y2d = new int[m.getVerticesCount()];
+
+ Perspective.modelToCanvas(client,
+ m.getVerticesCount(),
+ x, y, z,
+ jauOrient,
+ m.getVerticesX(), m.getVerticesZ(), m.getVerticesY(),
+ x2d, y2d);
+
+ final int radius = 5;
+
+ int[][] tris = new int[][]{
+ m.getTrianglesX(),
+ m.getTrianglesY(),
+ m.getTrianglesZ()
+ };
+
+ int vpX1 = client.getViewportXOffset();
+ int vpY1 = client.getViewportXOffset();
+ int vpX2 = vpX1 + client.getViewportWidth();
+ int vpY2 = vpY1 + client.getViewportHeight();
+
+ List rects = new ArrayList<>(m.getTrianglesCount());
+
+ nextTri:
+ for (int tri = 0; tri < m.getTrianglesCount(); tri++)
+ {
+ int
+ minX = Integer.MAX_VALUE,
+ minY = Integer.MAX_VALUE,
+ maxX = Integer.MIN_VALUE,
+ maxY = Integer.MIN_VALUE;
+
+ for (int[] vertex : tris)
+ {
+ final int idx = vertex[tri];
+ final int xs = x2d[idx];
+ final int ys = y2d[idx];
+
+ if (xs == Integer.MIN_VALUE || ys == Integer.MIN_VALUE)
+ {
+ continue nextTri;
+ }
+
+ if (xs < minX)
+ {
+ minX = xs;
+ }
+ if (xs > maxX)
+ {
+ maxX = xs;
+ }
+ if (ys < minY)
+ {
+ minY = ys;
+ }
+ if (ys > maxY)
+ {
+ maxY = ys;
+ }
+ }
+
+ minX -= radius;
+ minY -= radius;
+ maxX += radius;
+ maxY += radius;
+
+ if (vpX1 > maxX || vpX2 < minX || vpY1 > maxY || vpY2 < minY)
+ {
+ continue;
+ }
+
+ RectangleUnion.Rectangle r = new RectangleUnion.Rectangle(minX, minY, maxX, maxY);
+
+ rects.add(r);
+ }
+
+ return RectangleUnion.union(rects);
}
/**
diff --git a/runelite-api/src/main/java/net/runelite/api/geometry/RectangleUnion.java b/runelite-api/src/main/java/net/runelite/api/geometry/RectangleUnion.java
new file mode 100644
index 0000000000..e71f80c6b1
--- /dev/null
+++ b/runelite-api/src/main/java/net/runelite/api/geometry/RectangleUnion.java
@@ -0,0 +1,415 @@
+/*
+ * Copyright (c) 2019 Abex
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package net.runelite.api.geometry;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import javax.annotation.Nullable;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.RequiredArgsConstructor;
+import lombok.ToString;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+public class RectangleUnion
+{
+ private RectangleUnion()
+ {
+ }
+
+ @RequiredArgsConstructor
+ @Getter
+ @ToString
+ public static class Rectangle
+ {
+ private final int x1, y1, x2, y2;
+ }
+
+ /**
+ * Returns a polygon representing the union of all of the passed rectangles.
+ * the passed List will be modified
+ */
+ @Nullable
+ public static Shapes union(List lefts)
+ {
+ // https://stackoverflow.com/a/35362615/2977136
+ if (lefts.size() == 0)
+ {
+ return null;
+ }
+
+ boolean trace = log.isTraceEnabled();
+
+ // Sort all of the rectangles so they are ordered by their left edge
+ lefts.sort(Comparator.comparing(Rectangle::getX1));
+
+ // Again, but for the right edge
+ // this should be relatively fast if the rectangles are similar sizes because timsort deals with partially
+ // presorted data well
+ List rights = new ArrayList<>(lefts);
+ rights.sort(Comparator.comparing(Rectangle::getX2));
+
+ // ranges of our scan line with how many rectangles it is occluding
+ Segments segments = new Segments();
+ Shapes out = new Shapes<>(new ArrayList<>());
+ ChangingState cs = new ChangingState(out);
+
+ // Walk a beam left to right, colliding with any vertical edges of rectangles
+ for (int l = 0, r = 0; ; )
+ {
+ Rectangle lr = null, rr = null;
+ if (l < lefts.size())
+ {
+ lr = lefts.get(l);
+ }
+ if (r < rights.size())
+ {
+ rr = rights.get(r);
+ }
+ if (lr == null && rr == null)
+ {
+ break;
+ }
+
+ // get the next edge, preferring + edges
+ Rectangle rect;
+ boolean remove = lr == null || (rr != null && rr.x2 < lr.x1);
+ if (remove)
+ {
+ cs.delta = -1;
+ cs.x = rr.x2;
+ r++;
+ rect = rr;
+ }
+ else
+ {
+ cs.delta = 1;
+ cs.x = lr.x1;
+ l++;
+ rect = lr;
+ }
+ if (trace)
+ {
+ log.trace("{}{}", remove ? "-" : "+", rect);
+ }
+
+ int y1 = rect.y1;
+ int y2 = rect.y2;
+
+ // Find or create the y1 edge
+ Segment n = segments.findLE(y1);
+ if (n == null)
+ {
+ n = segments.insertAfter(null, y1);
+ }
+ if (n.y != y1)
+ {
+ n = segments.insertAfter(n, y1);
+ n.value = n.previous.value;
+ }
+
+ for (; ; )
+ {
+ // create the y2 edge if the next edge is past
+ if (n.next == null || n.next.y > y2)
+ {
+ segments.insertAfter(n, y2);
+ }
+ cs.touch(n);
+ n = n.next;
+ if (n.y == y2)
+ {
+ cs.finish(n);
+
+ if (trace)
+ {
+ for (Segment s = segments.first; s != null; s = s.next)
+ {
+ String chunk = "";
+ if (s.chunk != null)
+ {
+ chunk = (s.left ? ">" : "[") + System.identityHashCode(s.chunk) + (s.left ? "]" : "<");
+ }
+ log.trace("{} = {} {}", s.y, s.value, chunk);
+ }
+ log.trace("");
+ }
+ break;
+ }
+ }
+ }
+
+ assert segments.allZero();
+
+ return out;
+ }
+
+ @RequiredArgsConstructor
+ private static class ChangingState
+ {
+ final Shapes out;
+
+ int x;
+ int delta;
+
+ Segment first;
+
+ void touch(Segment s)
+ {
+ int oldValue = s.value;
+ s.value += delta;
+ if (oldValue <= 0 ^ s.value <= 0)
+ {
+ if (first == null)
+ {
+ first = s;
+ }
+ }
+ else
+ {
+ finish(s);
+ }
+ }
+
+ void finish(Segment s)
+ {
+ if (first == null)
+ {
+ return;
+ }
+
+ if (first.chunk != null && s.chunk != null)
+ {
+ push(first);
+ push(s);
+
+ if (first.chunk == s.chunk)
+ {
+ Chunk c = first.chunk;
+ first.chunk = null;
+ s.chunk = null;
+ c.left = null;
+ c.right = null;
+ out.getShapes().add(c);
+ }
+ else
+ {
+ Chunk leftChunk, rightChunk;
+ if (!s.left)
+ {
+ leftChunk = s.chunk;
+ rightChunk = first.chunk;
+ }
+ else
+ {
+ leftChunk = first.chunk;
+ rightChunk = s.chunk;
+ }
+
+ log.trace("Joining {} onto {}", System.identityHashCode(rightChunk), System.identityHashCode(leftChunk));
+ if (first.left == s.left)
+ {
+ log.trace("reverse");
+ if (first.left)
+ {
+ leftChunk.reverse();
+ }
+ else
+ {
+ rightChunk.reverse();
+ }
+ }
+ log.trace("{} {}", first.y, s.y);
+ rightChunk.appendTo(leftChunk);
+
+ first.chunk = null;
+ s.chunk = null;
+ leftChunk.right.chunk = null;
+ rightChunk.left.chunk = null;
+ leftChunk.right = rightChunk.right;
+ leftChunk.left.chunk = leftChunk;
+ leftChunk.right.chunk = leftChunk;
+ }
+ }
+ else if (first.chunk == null && s.chunk == null)
+ {
+ first.chunk = new Chunk();
+ first.chunk.right = first;
+ first.left = false;
+ s.chunk = first.chunk;
+ first.chunk.left = s;
+ s.left = true;
+
+ push(first);
+ push(s);
+ }
+ else if (first.chunk == null)
+ {
+ push(s);
+ move(first, s);
+ push(first);
+ }
+ else
+ {
+ push(first);
+ move(s, first);
+ push(s);
+ }
+
+ first = null;
+ }
+
+ private void move(Segment dst, Segment src)
+ {
+ dst.chunk = src.chunk;
+ dst.left = src.left;
+ src.chunk = null;
+ if (dst.left)
+ {
+ assert dst.chunk.left == src;
+ dst.chunk.left = dst;
+ }
+ else
+ {
+ assert dst.chunk.right == src;
+ dst.chunk.right = dst;
+ }
+ }
+
+ private void push(Segment s)
+ {
+ if (s.left)
+ {
+ s.chunk.pushLeft(x, s.y);
+ assert s.chunk.left == s;
+ }
+ else
+ {
+ s.chunk.pushRight(x, s.y);
+ assert s.chunk.right == s;
+ }
+ }
+ }
+
+ @NoArgsConstructor
+ private static class Segment
+ {
+ Segment next, previous;
+
+ Chunk chunk;
+ boolean left;
+ int y;
+ int value;
+ }
+
+ @NoArgsConstructor
+ private static class Segments
+ {
+ Segment first;
+
+ Segment findLE(int y)
+ {
+ Segment s = first;
+ if (s == null || s.y > y)
+ {
+ return null;
+ }
+ for (; ; )
+ {
+ if (s.y == y)
+ {
+ return s;
+ }
+
+ Segment n = s.next;
+ if (n == null || n.y > y)
+ {
+ return s;
+ }
+
+ s = n;
+ }
+ }
+
+ Segment insertAfter(Segment before, int y)
+ {
+ Segment n = new Segment();
+ n.y = y;
+ if (before != null)
+ {
+ if (before.next != null)
+ {
+ n.next = before.next;
+ n.next.previous = n;
+ }
+ n.value = before.value;
+ before.next = n;
+ n.previous = before;
+ }
+ else
+ {
+ if (first != null)
+ {
+ n.next = first;
+ first.previous = n;
+ }
+ first = n;
+ }
+ return n;
+ }
+
+ boolean allZero()
+ {
+ for (Segment s = first; s != null; s = s.next)
+ {
+ if (s.value != 0 || s.chunk != null)
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+
+ private static class Chunk extends SimplePolygon
+ {
+ Segment left, right;
+
+ @Override
+ public void reverse()
+ {
+ super.reverse();
+ assert right.left == false;
+ assert left.left == true;
+ Segment tr = left;
+ left = right;
+ right = tr;
+ right.left = false;
+ left.left = true;
+ }
+ }
+}
diff --git a/runelite-api/src/test/java/net/runelite/api/geometry/RectangleUnionTest.java b/runelite-api/src/test/java/net/runelite/api/geometry/RectangleUnionTest.java
new file mode 100644
index 0000000000..f8d714180b
--- /dev/null
+++ b/runelite-api/src/test/java/net/runelite/api/geometry/RectangleUnionTest.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (c) 2019 Abex
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package net.runelite.api.geometry;
+
+import java.awt.Color;
+import java.awt.Graphics2D;
+import java.awt.Shape;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+import javax.imageio.ImageIO;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.Assert;
+
+@Slf4j
+public class RectangleUnionTest
+{
+ private static final int ITERATIONS = 100;
+ private static final int WIDTH = 1000;
+ private static final int MAX_RECTS = 50;
+
+ // @Test
+ public void test() throws IOException
+ {
+ for (int count = 1; count < MAX_RECTS; count++)
+ {
+ for (int r = 0; r < ITERATIONS; r++)
+ {
+ Random rand = new Random(count << 16 | r);
+ String id = count + "rects_iteration" + r;
+ log.info(id);
+ BufferedImage wanted = new BufferedImage(WIDTH, WIDTH, BufferedImage.TYPE_BYTE_BINARY);
+ BufferedImage got = new BufferedImage(WIDTH, WIDTH, BufferedImage.TYPE_BYTE_BINARY);
+
+ Graphics2D wg = wanted.createGraphics();
+ wg.setColor(Color.WHITE);
+ Graphics2D gg = got.createGraphics();
+ gg.setColor(Color.WHITE);
+
+ List rects = new ArrayList<>(count);
+
+ for (int i = 0; i < count; i++)
+ {
+ int x1, y1, x2, y2;
+
+ do
+ {
+ x1 = rand.nextInt(WIDTH);
+ x2 = rand.nextInt(WIDTH);
+ }
+ while (x1 >= x2);
+
+ do
+ {
+ y1 = rand.nextInt(WIDTH);
+ y2 = rand.nextInt(WIDTH);
+ }
+ while (y1 >= y2);
+
+ RectangleUnion.Rectangle rect = new RectangleUnion.Rectangle(x1, y1, x2, y2);
+ log.trace("{}", rect);
+ rects.add(rect);
+
+ wg.fillRect(x1, y1, x2 - x1, y2 - y1);
+ }
+
+ Shape union = RectangleUnion.union(rects);
+
+ gg.fill(union);
+
+ loop:
+ for (int x = 0; x < WIDTH; x++)
+ {
+ for (int y = 0; y < WIDTH; y++)
+ {
+ if (wanted.getRGB(x, y) != got.getRGB(x, y))
+ {
+ File tmp = new File(System.getProperty("java.io.tmpdir"));
+ ImageIO.write(wanted, "png", new File(tmp, id + "_wanted.png"));
+ ImageIO.write(got, "png", new File(tmp, id + "_got.png"));
+
+ Assert.fail(id);
+ break loop;
+ }
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file