runelite-api: Optimize getClickbox
- Use the pre-calculated center/extreme xyz fields for the aabb
- use modelToCanvas and reduce indirection
- Use a specialized union that only does axis-aligned rectangles
instead of the Area class
- Use a specialized intersection that only does convex polygons, again
to avoid Area
This commit is contained in:
@@ -57,5 +57,11 @@
|
|||||||
<version>4.12</version>
|
<version>4.12</version>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.slf4j</groupId>
|
||||||
|
<artifactId>slf4j-simple</artifactId>
|
||||||
|
<version>1.7.12</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
@@ -103,4 +103,5 @@ public interface Model extends Renderable
|
|||||||
int getExtremeZ();
|
int getExtremeZ();
|
||||||
|
|
||||||
int getXYZMag();
|
int getXYZMag();
|
||||||
|
boolean isClickable();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,20 +27,19 @@ package net.runelite.api;
|
|||||||
import java.awt.FontMetrics;
|
import java.awt.FontMetrics;
|
||||||
import java.awt.Graphics2D;
|
import java.awt.Graphics2D;
|
||||||
import java.awt.Polygon;
|
import java.awt.Polygon;
|
||||||
import java.awt.Rectangle;
|
import java.awt.Shape;
|
||||||
import java.awt.geom.Area;
|
|
||||||
import java.awt.geom.Rectangle2D;
|
import java.awt.geom.Rectangle2D;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import static net.runelite.api.Constants.TILE_FLAG_BRIDGE;
|
import static net.runelite.api.Constants.TILE_FLAG_BRIDGE;
|
||||||
import net.runelite.api.coords.LocalPoint;
|
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.Jarvis;
|
||||||
import net.runelite.api.model.Triangle;
|
|
||||||
import net.runelite.api.model.Vertex;
|
|
||||||
import net.runelite.api.widgets.Widget;
|
import net.runelite.api.widgets.Widget;
|
||||||
import net.runelite.api.widgets.WidgetInfo;
|
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
|
* 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
|
* object on the tile at ({@code localX}, {@code localY}) and rotated to
|
||||||
* angle {@code orientation}.
|
* angle {@code orientation}.
|
||||||
*
|
* @param client the game client
|
||||||
* @param client the game client
|
* @param model the model to calculate a clickbox for
|
||||||
* @param model the model to calculate a clickbox for
|
|
||||||
* @param orientation the orientation of the model (0-2048, where 0 is north)
|
* @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
|
* @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)
|
if (model == null)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Triangle> triangles = model.getTriangles().stream()
|
int x = point.getX();
|
||||||
.map(triangle -> triangle.rotate(orientation))
|
int y = point.getY();
|
||||||
.collect(Collectors.toList());
|
int z = getTileHeight(client, point, client.getPlane());
|
||||||
|
|
||||||
List<Vertex> vertices = model.getVertices().stream()
|
SimplePolygon bounds = calculateAABB(client, model, orientation, x, y, z);
|
||||||
.map(v -> v.rotate(orientation))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
Area clickBox = get2DGeometry(client, triangles, point);
|
if (bounds == null)
|
||||||
Area visibleAABB = getAABB(client, vertices, point);
|
|
||||||
|
|
||||||
if (visibleAABB == null)
|
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
clickBox.intersect(visibleAABB);
|
if (model.isClickable())
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
Vertex _a = triangle.getA();
|
return bounds;
|
||||||
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 geometry;
|
Shapes<SimplePolygon> bounds2d = calculate2DBounds(client, model, orientation, x, y, z);
|
||||||
}
|
if (bounds2d == null)
|
||||||
|
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Polygon hull = new Polygon();
|
for (SimplePolygon poly : bounds2d.getShapes())
|
||||||
for (Point p : points)
|
|
||||||
{
|
{
|
||||||
if (p != null)
|
poly.intersectWithConvex(bounds);
|
||||||
{
|
|
||||||
hull.addPoint(p.getX(), p.getY());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user