diff --git a/runelite-api/src/main/java/net/runelite/api/GameObject.java b/runelite-api/src/main/java/net/runelite/api/GameObject.java index 12c9fef56b..2c83eb9b4c 100644 --- a/runelite-api/src/main/java/net/runelite/api/GameObject.java +++ b/runelite-api/src/main/java/net/runelite/api/GameObject.java @@ -24,6 +24,12 @@ */ package net.runelite.api; +import java.awt.Polygon; +import java.util.ArrayList; +import java.util.List; +import net.runelite.api.model.Jarvis; +import net.runelite.api.model.Vertex; + /** * * @author Adam @@ -55,4 +61,82 @@ public class GameObject extends TileObject { return gameObject.getY(); } + + public Renderable getRenderable() + { + return Renderable.of(gameObject.getRenderable()); + } + + public Polygon getConvexHull() + { + Renderable renderable = getRenderable(); + if (renderable == null) + { + return null; + } + + Model model; + + if (renderable instanceof Model) + { + model = (Model) renderable; + } + else + { + model = renderable.getModel(); + } + + if (model == null) + { + return null; + } + + int localX = gameObject.getX(); + int localY = gameObject.getY(); + + // models are orientated north (1024) and there are 2048 angles total + int orientation = (gameObject.getOrientation() + 1024) % 2048; + + List verticies = model.getVertices(); + + if (orientation != 0) + { + // rotate verticies + for (int i = 0; i < verticies.size(); ++i) + { + Vertex v = verticies.get(i); + verticies.set(i, v.rotate(orientation)); + } + } + + List points = new ArrayList<>(); + + for (Vertex v : verticies) + { + // Compute canvas location of vertex + Point p = Perspective.worldToCanvas(client, + localX - v.getX(), + localY - v.getZ(), + -v.getY()); + if (p != null) + { + points.add(p); + } + } + + // Run Jarvis march algorithm + points = Jarvis.convexHull(points); + if (points == null) + { + return null; + } + + // Convert to a polygon + Polygon p = new Polygon(); + for (Point point : points) + { + p.addPoint(point.getX(), point.getY()); + } + return p; + } } 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 f588db588b..a707707878 100644 --- a/runelite-api/src/main/java/net/runelite/api/Model.java +++ b/runelite-api/src/main/java/net/runelite/api/Model.java @@ -29,12 +29,13 @@ import java.util.List; import net.runelite.api.model.Triangle; import net.runelite.api.model.Vertex; -public class Model +public class Model extends Renderable { private final net.runelite.rs.api.Model model; public Model(net.runelite.rs.api.Model model) { + super(model); this.model = model; } diff --git a/runelite-api/src/main/java/net/runelite/api/Node.java b/runelite-api/src/main/java/net/runelite/api/Node.java index 3cc6bad0ae..630769f0d3 100644 --- a/runelite-api/src/main/java/net/runelite/api/Node.java +++ b/runelite-api/src/main/java/net/runelite/api/Node.java @@ -68,7 +68,7 @@ public class Node if (node instanceof net.runelite.rs.api.Renderable) { - return new Renderable((net.runelite.rs.api.Renderable) node); + return Renderable.of((net.runelite.rs.api.Renderable) node); } if (node instanceof net.runelite.rs.api.WidgetNode) diff --git a/runelite-api/src/main/java/net/runelite/api/Player.java b/runelite-api/src/main/java/net/runelite/api/Player.java index fd5422e59c..b9764e8037 100644 --- a/runelite-api/src/main/java/net/runelite/api/Player.java +++ b/runelite-api/src/main/java/net/runelite/api/Player.java @@ -128,24 +128,12 @@ public class Player extends Actor Vertex c = triangle.getC(); Triangle rotatedTriangle = new Triangle( - rotate(a, orientation), - rotate(b, orientation), - rotate(c, orientation) + a.rotate(orientation), + b.rotate(orientation), + c.rotate(orientation) ); rotatedTriangles.add(rotatedTriangle); } return rotatedTriangles; } - - private Vertex rotate(Vertex vertex, int orientation) - { - int sin = Perspective.SINE[orientation]; - int cos = Perspective.COSINE[orientation]; - - return new Vertex( - vertex.getX() * cos + vertex.getZ() * sin >> 16, - vertex.getY(), - vertex.getZ() * cos - vertex.getX() * sin >> 16 - ); - } } diff --git a/runelite-api/src/main/java/net/runelite/api/Renderable.java b/runelite-api/src/main/java/net/runelite/api/Renderable.java index 6ce81eaa5c..fcb48045a8 100644 --- a/runelite-api/src/main/java/net/runelite/api/Renderable.java +++ b/runelite-api/src/main/java/net/runelite/api/Renderable.java @@ -36,11 +36,22 @@ public class Renderable extends Node public Model getModel() { - return new Model(renderable.getModel()); + net.runelite.rs.api.Model model = renderable.getModel(); + return model != null ? new Model(model) : null; } public static Renderable of(net.runelite.rs.api.Renderable renderable) { - return (Renderable) Node.of(renderable); + if (renderable == null) + { + return null; + } + + if (renderable instanceof net.runelite.rs.api.Model) + { + return new Model((net.runelite.rs.api.Model) renderable); + } + + return new Renderable(renderable); } } diff --git a/runelite-api/src/main/java/net/runelite/api/model/Jarvis.java b/runelite-api/src/main/java/net/runelite/api/model/Jarvis.java new file mode 100644 index 0000000000..04ac76e20f --- /dev/null +++ b/runelite-api/src/main/java/net/runelite/api/model/Jarvis.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2017, Adam + * 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.model; + +import java.util.ArrayList; +import java.util.List; +import net.runelite.api.Point; + +/** + * Implementation of the Jarvis march algorithm + * https://en.wikipedia.org/wiki/Gift_wrapping_algorithm + * @author adam + */ +public class Jarvis +{ + /** + * compute the convex hull of a given set of points + * + * @param points + * @return + */ + public static List convexHull(List points) + { + if (points.size() < 3) + { + return null; + } + + List ch = new ArrayList<>(); + + // find the left most point + Point left = findLeftMost(points); + + // current point we are on + Point current = left; + + do + { + ch.add(current); + + // the next point - all points are to the right of the + // line between current and next + Point next = null; + + for (Point p : points) + { + if (next == null) + { + next = p; + continue; + } + + int cp = crossProduct(current, p, next); + if (cp > 0 || (cp == 0 && current.distanceTo(p) > current.distanceTo(next))) + { + next = p; + } + } + + assert next != null; + + assert ch.size() <= points.size() : "hull has more points than graph"; + current = next; + } + while (current != left); + + return ch; + } + + private static Point findLeftMost(List points) + { + Point left = null; + + for (Point p : points) + { + if (left == null || p.getX() < left.getX()) + { + left = p; + } + else if (p.getX() == left.getX() && p.getY() < left.getY()) + { + left = p; + } + } + + return left; + } + + private static int crossProduct(Point p, Point q, Point r) + { + int val = (q.getY() - p.getY()) * (r.getX() - q.getX()) + - (q.getX() - p.getX()) * (r.getY() - q.getY()); + return val; + } +} diff --git a/runelite-api/src/main/java/net/runelite/api/model/Triangle.java b/runelite-api/src/main/java/net/runelite/api/model/Triangle.java index b05c201c06..161bf01b2e 100644 --- a/runelite-api/src/main/java/net/runelite/api/model/Triangle.java +++ b/runelite-api/src/main/java/net/runelite/api/model/Triangle.java @@ -24,6 +24,8 @@ */ package net.runelite.api.model; +import java.util.Objects; + public class Triangle { private final Vertex a; @@ -43,6 +45,43 @@ public class Triangle return "Triangle{" + "a=" + a + ", b=" + b + ", c=" + c + '}'; } + @Override + public int hashCode() + { + int hash = 7; + hash = 13 * hash + Objects.hashCode(this.a); + hash = 13 * hash + Objects.hashCode(this.b); + hash = 13 * hash + Objects.hashCode(this.c); + return hash; + } + + @Override + public boolean equals(Object obj) + { + if (obj == null) + { + return false; + } + if (getClass() != obj.getClass()) + { + return false; + } + final Triangle other = (Triangle) obj; + if (!Objects.equals(this.a, other.a)) + { + return false; + } + if (!Objects.equals(this.b, other.b)) + { + return false; + } + if (!Objects.equals(this.c, other.c)) + { + return false; + } + return true; + } + public Vertex getA() { return a; diff --git a/runelite-api/src/main/java/net/runelite/api/model/Vertex.java b/runelite-api/src/main/java/net/runelite/api/model/Vertex.java index 8f7210e2e9..70b9c6adff 100644 --- a/runelite-api/src/main/java/net/runelite/api/model/Vertex.java +++ b/runelite-api/src/main/java/net/runelite/api/model/Vertex.java @@ -24,6 +24,8 @@ */ package net.runelite.api.model; +import net.runelite.api.Perspective; + public class Vertex { private final int x; @@ -43,6 +45,43 @@ public class Vertex return "Vertex{" + "x=" + x + ", y=" + y + ", z=" + z + '}'; } + @Override + public int hashCode() + { + int hash = 7; + hash = 67 * hash + this.x; + hash = 67 * hash + this.y; + hash = 67 * hash + this.z; + return hash; + } + + @Override + public boolean equals(Object obj) + { + if (obj == null) + { + return false; + } + if (getClass() != obj.getClass()) + { + return false; + } + final Vertex other = (Vertex) obj; + if (this.x != other.x) + { + return false; + } + if (this.y != other.y) + { + return false; + } + if (this.z != other.z) + { + return false; + } + return true; + } + public int getX() { return x; @@ -57,4 +96,21 @@ public class Vertex { return z; } + + /** + * Rotate the vertex by the given orientation + * @param orientation + * @return the newly rotated vertex + */ + public Vertex rotate(int orientation) + { + int sin = Perspective.SINE[orientation]; + int cos = Perspective.COSINE[orientation]; + + return new Vertex( + x * cos + z * sin >> 16, + y, + z * cos - x * sin >> 16 + ); + } } diff --git a/runelite-api/src/test/java/net/runelite/api/model/JarvisTest.java b/runelite-api/src/test/java/net/runelite/api/model/JarvisTest.java new file mode 100644 index 0000000000..1a6f3f1aaa --- /dev/null +++ b/runelite-api/src/test/java/net/runelite/api/model/JarvisTest.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2017, Adam + * 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.model; + +import java.util.Arrays; +import java.util.List; +import net.runelite.api.Point; +import org.junit.Assert; +import org.junit.Test; + +public class JarvisTest +{ + @Test + public void test() + { + Point[] points = + { + new Point(0, 3), + new Point(1, 1), + new Point(2, 2), + new Point(4, 4), + new Point(0, 0), + new Point(1, 2), + new Point(3, 1), + new Point(3, 3) + }; + + List result = Jarvis.convexHull(Arrays.asList(points)); + Assert.assertEquals(4, result.size()); + Assert.assertEquals(new Point(0, 0), result.get(0)); + Assert.assertEquals(new Point(0, 3), result.get(1)); + Assert.assertEquals(new Point(4, 4), result.get(2)); + Assert.assertEquals(new Point(3, 1), result.get(3)); + } + + @Test + public void test2() + { + Point[] points = + { + new Point(0, 3), + new Point(4, 2), + new Point(3, 5), + new Point(5, 3), + new Point(3, 0), + new Point(1, 1), + new Point(1, 2), + new Point(2, 2) + }; + + List result = Jarvis.convexHull(Arrays.asList(points)); + Assert.assertEquals(5, result.size()); + Assert.assertEquals(new Point(0, 3), result.get(0)); + Assert.assertEquals(new Point(3, 5), result.get(1)); + Assert.assertEquals(new Point(5, 3), result.get(2)); + Assert.assertEquals(new Point(3, 0), result.get(3)); + Assert.assertEquals(new Point(1, 1), result.get(4)); + } + + @Test + public void testCollinear() + { + Point[] points = new Point[] + { + new Point(604, 76), + new Point(609, 81), + new Point(606, 78), + new Point(602, 74), + new Point(610, 74), + new Point(609, 77), + new Point(602, 72), + new Point(606, 77), + new Point(611, 77), + new Point(607, 72), + new Point(616, 74), + new Point(611, 70), + new Point(611, 71), + new Point(610, 73), + new Point(614, 82), + new Point(615, 79), + new Point(615, 77), + new Point(615, 76), + new Point(634, 71), + new Point(604, 76) + }; + Jarvis.convexHull(Arrays.asList(points)); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/devtools/DevToolsOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/devtools/DevToolsOverlay.java index d56c13de0d..33cf660d8d 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/devtools/DevToolsOverlay.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/devtools/DevToolsOverlay.java @@ -252,6 +252,14 @@ public class DevToolsOverlay extends Overlay { OverlayUtil.renderTileOverlay(graphics, gameObject, "ID: " + gameObject.getId(), GREEN); } + + // Draw a polygon around the convex hull + // of the model vertices + Polygon p = gameObject.getConvexHull(); + if (p != null) + { + graphics.drawPolygon(p); + } } } } diff --git a/runescape-api/src/main/java/net/runelite/rs/api/Model.java b/runescape-api/src/main/java/net/runelite/rs/api/Model.java index 0e391eaa05..1a719ec116 100644 --- a/runescape-api/src/main/java/net/runelite/rs/api/Model.java +++ b/runescape-api/src/main/java/net/runelite/rs/api/Model.java @@ -27,7 +27,7 @@ package net.runelite.rs.api; import net.runelite.mapping.Import; -public interface Model +public interface Model extends Renderable { @Import("verticesX") int[] getVerticesX();