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