Merge pull request #9848 from abextm/faster-click-hull-external

Optimize getClickbox and getConvexHull
This commit is contained in:
Adam
2019-10-02 14:24:52 -04:00
committed by GitHub
25 changed files with 1604 additions and 437 deletions

View File

@@ -57,5 +57,11 @@
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.12</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -26,6 +26,7 @@ package net.runelite.api;
import java.awt.Graphics2D;
import java.awt.Polygon;
import java.awt.Shape;
import java.awt.image.BufferedImage;
import javax.annotation.Nullable;
import net.runelite.api.annotations.VisibleForDevtools;
@@ -218,7 +219,7 @@ public interface Actor extends Renderable
* @return the convex hull
* @see net.runelite.api.model.Jarvis
*/
Polygon getConvexHull();
Shape getConvexHull();
/**
* Gets the world area that the actor occupies.

View File

@@ -24,7 +24,7 @@
*/
package net.runelite.api;
import java.awt.Polygon;
import java.awt.Shape;
/**
* Represents a decorative object, such as an object on a wall.
@@ -37,8 +37,8 @@ public interface DecorativeObject extends TileObject
* @return the convex hull
* @see net.runelite.api.model.Jarvis
*/
Polygon getConvexHull();
Polygon getConvexHull2();
Shape getConvexHull();
Shape getConvexHull2();
Renderable getRenderable();
Renderable getRenderable2();

View File

@@ -24,7 +24,7 @@
*/
package net.runelite.api;
import java.awt.Polygon;
import java.awt.Shape;
import net.runelite.api.coords.Angle;
/**
@@ -54,12 +54,12 @@ public interface GameObject extends TileObject
Point getSceneMaxLocation();
/**
* Gets the convex hull of the actors model.
* Gets the convex hull of the object's model.
*
* @return the convex hull
* @see net.runelite.api.model.Jarvis
*/
Polygon getConvexHull();
Shape getConvexHull();
/**
* Gets the orientation of the object.

View File

@@ -24,29 +24,11 @@
*/
package net.runelite.api;
import java.util.List;
import net.runelite.api.model.Triangle;
import net.runelite.api.model.Vertex;
/**
* Represents the model of an object.
*/
public interface Model extends Renderable
{
/**
* Gets a list of all vertices of the model.
*
* @return the vertices
*/
List<Vertex> getVertices();
/**
* Gets a list of all triangles of the model.
*
* @return the triangle
*/
List<Triangle> getTriangles();
int getVerticesCount();
int[] getVerticesX();
@@ -103,4 +85,5 @@ public interface Model extends Renderable
int getExtremeZ();
int getXYZMag();
boolean isClickable();
}

View File

@@ -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;
@@ -148,7 +147,74 @@ public class Perspective
}
return null;
}
/**
* Translates a model's vertices into 2d space
*/
public static void modelToCanvas(Client client, int end, int x3dCenter, int y3dCenter, int z3dCenter, int rotate, int[] x3d, int[] y3d, int[] z3d, int[] x2d, int[] y2d)
{
final int
cameraPitch = client.getCameraPitch(),
cameraYaw = client.getCameraYaw(),
pitchSin = SINE[cameraPitch],
pitchCos = COSINE[cameraPitch],
yawSin = SINE[cameraYaw],
yawCos = COSINE[cameraYaw],
rotateSin = SINE[rotate],
rotateCos = COSINE[rotate],
cx = x3dCenter - client.getCameraX(),
cy = y3dCenter - client.getCameraY(),
cz = z3dCenter - client.getCameraZ(),
viewportXMiddle = client.getViewportWidth() / 2,
viewportYMiddle = client.getViewportHeight() / 2,
viewportXOffset = client.getViewportXOffset(),
viewportYOffset = client.getViewportYOffset(),
zoom3d = client.getScale();
for (int i = 0; i < end; i++)
{
int x = x3d[i];
int y = y3d[i];
int z = z3d[i];
if (rotate != 0)
{
int x0 = x;
x = x0 * rotateCos + y * rotateSin >> 16;
y = y * rotateCos - x0 * rotateSin >> 16;
}
x += cx;
y += cy;
z += cz;
final int
x1 = x * yawCos + y * yawSin >> 16,
y1 = y * yawCos - x * yawSin >> 16,
y2 = z * pitchCos - y1 * pitchSin >> 16,
z1 = y1 * pitchCos + z * pitchSin >> 16;
int viewX, viewY;
if (z1 < 50)
{
viewX = Integer.MIN_VALUE;
viewY = Integer.MIN_VALUE;
}
else
{
viewX = (viewportXMiddle + x1 * zoom3d / z1) + viewportXOffset;
viewY = (viewportYMiddle + y2 * zoom3d / z1) + viewportYOffset;
}
x2d[i] = viewX;
y2d[i] = viewY;
}
}
/**
@@ -495,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<Triangle> 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<Vertex> 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<Triangle> 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<Vertex> 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<Point> 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<SimplePolygon> 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<SimplePolygon> 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<RectangleUnion.Rectangle> 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);
}
/**

View File

@@ -26,7 +26,7 @@ package net.runelite.api;
import java.awt.Graphics2D;
import java.awt.Polygon;
import java.awt.geom.Area;
import java.awt.Shape;
import javax.annotation.Nullable;
import net.runelite.api.coords.LocalPoint;
import net.runelite.api.coords.WorldPoint;
@@ -132,5 +132,5 @@ public interface TileObject
* @return the clickable area
*/
@Nullable
Area getClickbox();
Shape getClickbox();
}

View File

@@ -24,7 +24,7 @@
*/
package net.runelite.api;
import java.awt.Polygon;
import java.awt.Shape;
/**
* Represents one or two walls on a tile
@@ -63,8 +63,8 @@ public interface WallObject extends TileObject
* @return the convex hull
* @see net.runelite.api.model.Jarvis
*/
Polygon getConvexHull();
Polygon getConvexHull2();
Shape getConvexHull();
Shape getConvexHull2();
Renderable getRenderable1();
Renderable getRenderable2();

View File

@@ -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<SimplePolygon> union(List<Rectangle> 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<Rectangle> 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<SimplePolygon> 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<SimplePolygon> 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;
}
}
}

View File

@@ -0,0 +1,212 @@
/*
* 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.Rectangle;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public class Shapes<T extends Shape> implements Shape
{
public Shapes(T ...shape)
{
this(Arrays.asList(shape));
}
@Getter
private final List<T> shapes;
@Override
public Rectangle getBounds()
{
int
minX = Integer.MAX_VALUE,
minY = Integer.MAX_VALUE,
maxX = Integer.MIN_VALUE,
maxY = Integer.MIN_VALUE;
for (Shape shape : shapes)
{
Rectangle bounds = shape.getBounds();
minX = Math.min(bounds.x, minX);
minY = Math.min(bounds.y, minY);
maxX = Math.max(bounds.x + bounds.width, maxX);
maxY = Math.max(bounds.y + bounds.height, maxY);
}
return new Rectangle(minX, minY, maxX - minX, maxY - minY);
}
@Override
public Rectangle2D getBounds2D()
{
double
minX = Double.MAX_VALUE,
minY = Double.MAX_VALUE,
maxX = Double.MIN_VALUE,
maxY = Double.MIN_VALUE;
for (Shape shape : shapes)
{
Rectangle2D bounds = shape.getBounds2D();
minX = Math.min(bounds.getX(), minX);
minY = Math.min(bounds.getY(), minY);
maxX = Math.max(bounds.getMaxX(), maxX);
maxY = Math.max(bounds.getMaxY(), maxY);
}
return new Rectangle2D.Double(minX, minY, maxX - minX, maxY - minY);
}
@Override
public boolean contains(double x, double y)
{
return shapes.stream().anyMatch(s -> s.contains(x, y));
}
@Override
public boolean contains(Point2D p)
{
return shapes.stream().anyMatch(s -> s.contains(p));
}
@Override
public boolean intersects(double x, double y, double w, double h)
{
return shapes.stream().anyMatch(s -> s.intersects(x, y, w, h));
}
@Override
public boolean intersects(Rectangle2D r)
{
return shapes.stream().anyMatch(s -> s.intersects(r));
}
@Override
public boolean contains(double x, double y, double w, double h)
{
return shapes.stream().anyMatch(s -> s.contains(x, y, w, h));
}
@Override
public boolean contains(Rectangle2D r)
{
return shapes.stream().anyMatch(s -> s.contains(r));
}
@Override
public PathIterator getPathIterator(AffineTransform at)
{
return new ShapeIterator(shapes.stream()
.map(s -> s.getPathIterator(at))
.iterator());
}
@Override
public PathIterator getPathIterator(AffineTransform at, double flatness)
{
return new ShapeIterator(shapes.stream()
.map(s -> s.getPathIterator(at, flatness))
.iterator());
}
private static class ShapeIterator implements PathIterator
{
private final Iterator<PathIterator> iter;
private PathIterator current = null;
private final int windingRule;
ShapeIterator(Iterator<PathIterator> iter)
{
this.iter = iter;
if (iter.hasNext())
{
current = iter.next();
windingRule = current.getWindingRule();
checkDone();
}
else
{
windingRule = 0;
}
}
@Override
public int getWindingRule()
{
return windingRule;
}
@Override
public boolean isDone()
{
return current == null;
}
@Override
public void next()
{
current.next();
checkDone();
}
private void checkDone()
{
for (; current != null && current.isDone(); )
{
if (iter.hasNext())
{
current = iter.next();
assert windingRule == current.getWindingRule();
}
else
{
current = null;
}
}
}
@Override
public int currentSegment(float[] coords)
{
return current.currentSegment(coords);
}
@Override
public int currentSegment(double[] coords)
{
return current.currentSegment(coords);
}
}
}

View File

@@ -0,0 +1,496 @@
/*
* 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.Rectangle;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import net.runelite.api.Point;
/**
* A simple list of vertices that can be append or prepended to
*/
@AllArgsConstructor
@Getter
@Setter
public class SimplePolygon implements Shape
{
private static final int GROW = 16;
protected int[] x, y;
protected int left, right;
public SimplePolygon()
{
this(new int[32], new int[32], 16, 15);
}
public SimplePolygon(int[] x, int[] y, int length)
{
this(x, y, 0, length - 1);
}
public void pushLeft(int xCoord, int yCoord)
{
left--;
if (left < 0)
{
expandLeft(GROW);
}
x[left] = xCoord;
y[left] = yCoord;
}
public void popLeft()
{
left++;
}
protected void expandLeft(int grow)
{
int[] nx = new int[x.length + grow];
System.arraycopy(x, 0, nx, grow, x.length);
x = nx;
int[] ny = new int[nx.length];
System.arraycopy(y, 0, ny, grow, y.length);
y = ny;
left += grow;
right += grow;
}
public void pushRight(int xCoord, int yCoord)
{
right++;
if (right >= x.length)
{
expandRight(GROW);
}
x[right] = xCoord;
y[right] = yCoord;
}
public void popRight()
{
right--;
}
protected void expandRight(int grow)
{
int[] nx = new int[x.length + grow];
System.arraycopy(x, 0, nx, 0, x.length);
x = nx;
int[] ny = new int[nx.length];
System.arraycopy(y, 0, ny, 0, y.length);
y = ny;
}
public int getX(int index)
{
return x[left + index];
}
public int getY(int index)
{
return y[left + index];
}
public int size()
{
return right - left + 1;
}
public List<Point> toRuneLitePointList()
{
List<Point> out = new ArrayList<>(size());
for (int i = left; i <= right; i++)
{
out.add(new Point(x[i], y[i]));
}
return out;
}
public void copyTo(int[] xDest, int[] yDest, int offset)
{
System.arraycopy(x, left, xDest, offset, size());
System.arraycopy(y, left, yDest, offset, size());
}
public void appendTo(SimplePolygon other)
{
int size = size();
if (size <= 0)
{
return;
}
other.expandRight(size);
copyTo(other.x, other.y, other.right + 1);
other.right += size;
}
public void reverse()
{
int half = size() / 2;
for (int i = 0; i < half; i++)
{
int li = left + i;
int ri = right - i;
int tx = x[li];
int ty = y[li];
x[li] = x[ri];
y[li] = y[ri];
x[ri] = tx;
y[ri] = ty;
}
}
/**
* Clips the polygon with the passed convex polygon
*/
public void intersectWithConvex(SimplePolygon convex)
{
// Sutherland-Hodgman
int[] tx = new int[size()];
int[] ty = new int[tx.length];
int cx1 = convex.x[convex.right];
int cy1 = convex.y[convex.right];
for (int ci = convex.left; ci <= convex.right; ci++)
{
if (size() < 3)
{
return;
}
int tRight = this.right;
int tLeft = this.left;
int[] tmpX = x;
int[] tmpY = y;
this.x = tx;
this.y = ty;
this.left = 0;
this.right = -1;
tx = tmpX;
ty = tmpY;
int cx2 = convex.x[ci];
int cy2 = convex.y[ci];
int tx1 = tx[tRight];
int ty1 = ty[tRight];
for (int ti = tLeft; ti <= tRight; ti++)
{
int tx2 = tx[ti];
int ty2 = ty[ti];
int p1 = (cx2 - cx1) * (ty1 - cy1) - (cy2 - cy1) * (tx1 - cx1);
int p2 = (cx2 - cx1) * (ty2 - cy1) - (cy2 - cy1) * (tx2 - cx1);
if (p1 < 0 && p2 < 0)
{
pushRight(tx2, ty2);
}
else if (p1 >= 0 != p2 >= 0)
{
long nota = cx1 * cy2 - cy1 * cx2;
long clue = tx1 * ty2 - ty1 * tx2;
long div = ((cx1 - cx2) * (ty1 - ty2) - (cy1 - cy2) * (tx1 - tx2));
pushRight((int) ((nota * (tx1 - tx2) - (cx1 - cx2) * clue) / div),
(int) ((nota * (ty1 - ty2) - (cy1 - cy2) * clue) / div));
if (p1 >= 0)
{
pushRight(tx2, ty2);
}
}
tx1 = tx2;
ty1 = ty2;
}
cx1 = cx2;
cy1 = cy2;
}
}
@Override
public Rectangle getBounds()
{
int
minX = Integer.MAX_VALUE,
minY = Integer.MAX_VALUE,
maxX = Integer.MIN_VALUE,
maxY = Integer.MIN_VALUE;
for (int i = left; i <= right; i++)
{
final int xs = x[i];
final int ys = y[i];
if (xs < minX)
{
minX = xs;
}
if (xs > maxX)
{
maxX = xs;
}
if (ys < minY)
{
minY = ys;
}
if (ys > maxY)
{
maxY = ys;
}
}
return new Rectangle(minX, minY, maxX - minX, maxY - minY);
}
@Override
public Rectangle2D getBounds2D()
{
Rectangle b = getBounds();
return new Rectangle2D.Float(b.x, b.y, b.width, b.height);
}
@Override
public boolean contains(double cx, double cy)
{
if (size() < 3)
{
return false;
}
return (crossings(cx, cy, false) & 1) != 0;
}
private int crossings(double cx, double cy, boolean swap)
{
int collisions = 0;
int[] x = this.x;
int[] y = this.y;
if (swap)
{
y = this.x;
x = this.y;
}
for (int x0 = x[right], y0 = y[right], x1, y1, i = left; i <= right; i++, x0 = x1, y0 = y1)
{
x1 = x[i];
y1 = y[i];
if (y0 == y1)
{
continue;
}
double dy0 = y0, dy1 = y1;
if (cy <= dy0 == cy <= dy1)
{
continue;
}
double dx0 = x0, dx1 = x1;
boolean left = cx < dx0;
if (left == cx < dx1)
{
if (!left)
{
collisions++;
}
continue;
}
if ((dx1 - dx0) * (cy - dy0) - (cx - dx0) * (dy1 - dy0) > 0 == dy0 > dy1)
{
collisions++;
}
}
return collisions;
}
@Override
public boolean contains(Point2D p)
{
return contains(p.getX(), p.getY());
}
@Override
public boolean intersects(double x0, double y0, double w, double h)
{
// this is horribly inefficient, but I don't think it will be called anywhere
double x1 = x0 + w;
double y1 = y0 + h;
return crossings(x0, y0, false) != crossings(x1, y0, false) // top
|| crossings(x0, y1, false) != crossings(x1, y1, false) // bottom
|| crossings(x0, y0, true) != crossings(x0, y1, true) // left
|| crossings(x1, y0, true) != crossings(x1, y1, true); // right
}
@Override
public boolean intersects(Rectangle2D r)
{
return intersects(r.getX(), r.getY(), r.getWidth(), r.getHeight());
}
@Override
public boolean contains(double x, double y, double w, double h)
{
if (!getBounds().contains(x, y, w, h))
{
return false;
}
return !intersects(x, y, w, h);
}
@Override
public boolean contains(Rectangle2D r)
{
return contains(r.getX(), r.getY(), r.getWidth(), r.getHeight());
}
@Override
public PathIterator getPathIterator(AffineTransform at)
{
if (at == null)
{
return new SimpleIterator();
}
return new TransformIterator(at);
}
@Override
public PathIterator getPathIterator(AffineTransform at, double flatness)
{
return getPathIterator(at);
}
private class SimpleIterator implements PathIterator
{
private int i = -1;
@Override
public int getWindingRule()
{
return WIND_EVEN_ODD;
}
@Override
public boolean isDone()
{
return size() == 0 || i > right;
}
@Override
public void next()
{
if (i == -1)
{
i = left;
}
else
{
i++;
}
}
@Override
public int currentSegment(float[] coords)
{
if (i == -1)
{
coords[0] = x[right];
coords[1] = y[right];
return SEG_MOVETO;
}
coords[0] = x[i];
coords[1] = y[i];
return SEG_LINETO;
}
@Override
public int currentSegment(double[] coords)
{
if (i == -1)
{
coords[0] = x[right];
coords[1] = y[right];
return SEG_MOVETO;
}
coords[0] = x[i];
coords[1] = y[i];
return SEG_LINETO;
}
}
private class TransformIterator extends SimpleIterator
{
private final AffineTransform transform;
TransformIterator(AffineTransform transform)
{
this.transform = transform;
}
@Override
public int currentSegment(float[] coords)
{
int v = super.currentSegment(coords);
transform.transform(coords, 0, coords, 0, 2);
return v;
}
@Override
public int currentSegment(double[] coords)
{
int v = super.currentSegment(coords);
transform.transform(coords, 0, coords, 0, 2);
return v;
}
}
}

View File

@@ -24,9 +24,9 @@
*/
package net.runelite.api.model;
import java.util.ArrayList;
import java.util.List;
import net.runelite.api.Point;
import net.runelite.api.geometry.SimplePolygon;
/**
* Provides utility methods for computing the convex hull of a list of
@@ -41,92 +41,147 @@ public class Jarvis
/**
* Computes and returns the convex hull of the passed points.
* <p>
* The size of the list must be at least 4, otherwise this method will
* The size of the list must be at least 3, otherwise this method will
* return null.
*
* @param points list of points
* @return list containing the points part of the convex hull
*/
@Deprecated
public static List<Point> convexHull(List<Point> points)
{
if (points.size() < 3)
int[] xs = new int[points.size()];
int[] ys = new int[xs.length];
for (int i = 0; i < xs.length; i++)
{
Point p = points.get(i);
xs[i] = p.getX();
ys[i] = p.getY();
}
SimplePolygon poly = convexHull(xs, ys);
if (poly == null)
{
return null;
}
List<Point> ch = new ArrayList<>();
return poly.toRuneLitePointList();
}
/**
* Computes and returns the convex hull of the passed points.
* <p>
* The size of the list must be at least 3, otherwise this method will
* return null.
*
* @return a shape the points part of the convex hull
*/
public static SimplePolygon convexHull(int[] xs, int[] ys)
{
int length = xs.length;
// remove any invalid entries
{
int i = 0, offset = 0;
for (; i < length; i++)
{
if (xs[i] == Integer.MIN_VALUE)
{
offset++;
i++;
break;
}
}
for (; i < length; i++)
{
if (xs[i] == Integer.MIN_VALUE)
{
offset++;
continue;
}
xs[i - offset] = xs[i];
ys[i - offset] = ys[i];
}
length -= offset;
}
if (length < 3)
{
return null;
}
// find the left most point
Point left = findLeftMost(points);
int left = findLeftMost(xs, ys, length);
// current point we are on
Point current = left;
int current = left;
SimplePolygon out = new SimplePolygon(new int[16], new int[16], 0);
do
{
ch.add(current);
assert ch.size() <= points.size() : "hull has more points than graph";
if (ch.size() > points.size())
int cx = xs[current];
int cy = ys[current];
out.pushRight(cx, cy);
if (out.size() > length)
{
// Just to make sure we never somehow get stuck in this loop
return null;
}
// the next point - all points are to the right of the
// line between current and next
Point next = null;
int next = 0;
int nx = xs[next];
int ny = ys[next];
for (Point p : points)
for (int i = 1; i < length; i++)
{
if (next == null)
long cp = crossProduct(cx, cy, xs[i], ys[i], nx, ny);
if (cp > 0 || (cp == 0 && square(cx - xs[i]) + square(cy - ys[i]) > square(cx - nx) + square(cy - ny)))
{
next = p;
continue;
next = i;
nx = xs[next];
ny = ys[next];
}
long cp = crossProduct(current, p, next);
if (cp > 0 || (cp == 0 && current.distanceTo(p) > current.distanceTo(next)))
{
next = p;
}
}
// Points can be null if they are behind or very close to the camera.
if (next == null)
{
return null;
}
current = next;
}
while (current != left);
return ch;
return out;
}
private static Point findLeftMost(List<Point> points)
private static int square(int x)
{
Point left = null;
return x * x;
}
for (Point p : points)
private static int findLeftMost(int[] xs, int[] ys, int length)
{
int idx = 0;
int x = xs[idx];
int y = ys[idx];
for (int i = 1; i < length; i++)
{
if (left == null || p.getX() < left.getX())
int ix = xs[i];
if (ix < x || ix == x && ys[i] < y)
{
left = p;
}
else if (p.getX() == left.getX() && p.getY() < left.getY())
{
left = p;
idx = i;
x = xs[idx];
y = ys[idx];
}
}
return left;
return idx;
}
private static long crossProduct(Point p, Point q, Point r)
private static long crossProduct(int px, int py, int qx, int qy, int rx, int ry)
{
long val = (long)(q.getY() - p.getY()) * (r.getX() - q.getX())
- (long)(q.getX() - p.getX()) * (r.getY() - q.getY());
long val = (long) (qy - py) * (rx - qx)
- (long) (qx - px) * (ry - qy);
return val;
}
}

View File

@@ -1,54 +0,0 @@
/*
* Copyright (c) 2017, Adam <Adam@sigterm.info>
* 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 lombok.Value;
/**
* Represents 3 vertices as a three-dimensional Triangle.
*/
@Value
public class Triangle
{
private final Vertex a;
private final Vertex b;
private final Vertex c;
/**
* Rotates the triangle by the given orientation.
*
* @param orientation passed orientation
* @return new instance
*/
public Triangle rotate(int orientation)
{
return new Triangle(
a.rotate(orientation),
b.rotate(orientation),
c.rotate(orientation)
);
}
}

View File

@@ -1,65 +0,0 @@
/*
* Copyright (c) 2017, Adam <Adam@sigterm.info>
* 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 lombok.Value;
import net.runelite.api.Perspective;
/**
* Represents a point in a three-dimensional space.
*/
@Value
public class Vertex
{
private final int x;
private final int y;
private final int z;
/**
* Rotates the triangle by the given orientation.
*
* @param orientation passed orientation
* @return new instance
*/
public Vertex rotate(int orientation)
{
// models are orientated north (1024) and there are 2048 angles total
orientation = (orientation + 1024) % 2048;
if (orientation == 0)
{
return this;
}
int sin = Perspective.SINE[orientation];
int cos = Perspective.COSINE[orientation];
return new Vertex(
x * cos + z * sin >> 16,
y,
z * cos - x * sin >> 16
);
}
}

View File

@@ -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<RectangleUnion.Rectangle> 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;
}
}
}
}
}
}
}