Initial fork from spacecraft-tactics

This commit is contained in:
extio-de
2025-12-20 16:27:45 +01:00
commit 31b62ed2ed
105 changed files with 13037 additions and 0 deletions

View File

@@ -0,0 +1,832 @@
/* Copyright (C) 2023 Stephan Birkl - All Rights Reserved.
* THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
* PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
* OR OTHER DEALINGS IN THE SOFTWARE.
*/
package de.extio.game_engine.spatial2;
import java.awt.Rectangle;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;
import de.extio.game_engine.spatial2.model.Area2;
import de.extio.game_engine.spatial2.model.CoordD2;
import de.extio.game_engine.spatial2.model.CoordI2;
import de.extio.game_engine.spatial2.model.Edge2;
import de.extio.game_engine.spatial2.model.HasPosition2;
import de.extio.game_engine.spatial2.model.HasPositionAndDimension2;
import de.extio.game_engine.spatial2.model.ImmutableCoordD2;
import de.extio.game_engine.spatial2.model.ImmutableCoordI2;
import de.extio.game_engine.spatial2.model.MutableCoordD2;
import de.extio.game_engine.spatial2.model.MutableCoordI2;
import de.extio.game_engine.spatial2.model.RectI2;
import de.extio.game_engine.util.rng.ThreadLocalXorShift128Random;
/**
* Library for geometric problems
*/
public final class WorldUtils2 {
/**
* The Pythagorean theorem
* @param v Vector
* @return Length of vector v
*/
public static double getDistance(final CoordI2 v) {
return Math.sqrt(Math.pow(v.getX(), 2.0) + Math.pow(v.getY(), 2.0));
}
/**
* The Pythagorean theorem
* @param v Vector
* @return Length of vector v
*/
public static double getDistance(final CoordD2 v) {
return Math.sqrt(Math.pow(v.getX(), 2.0) + Math.pow(v.getY(), 2.0));
}
/**
* The Pythagorean theorem
* @param p0 Coordinate 0
* @param p1 Coordinate 1
* @return Distance
*/
public static double getDistance(final HasPosition2 p0, final HasPosition2 p1) {
return getDistance(p0.getPosition(), p1.getPosition());
}
/**
* The Pythagorean theorem
* @param c0 Coordinate 0
* @param c1 Coordinate 1
* @return Distance
*/
public static double getDistance(final CoordI2 c0, final CoordI2 c1) {
return Math.sqrt(Math.pow(c0.getX() - c1.getX(), 2.0) + Math.pow(c0.getY() - c1.getY(), 2.0));
}
/**
* The Pythagorean theorem
* @param c0 Coordinate 0
* @param c1 Coordinate 1
* @return Distance
*/
public static double getDistance(final CoordD2 c0, final CoordD2 c1) {
return Math.sqrt(Math.pow(c0.getX() - c1.getX(), 2.0) + Math.pow(c0.getY() - c1.getY(), 2.0));
}
/**
* Calculate the angle between 2 vectors
* @param v0 Vector 0
* @param v1 Vector 1
* @return Angle (radians)
*/
public static double getVectorAngle(final CoordI2 v0, final CoordI2 v1) {
return Math.atan2(v0.getX() * v1.getY() - v0.getY() * v1.getX(), v0.getX() * v1.getX() + v0.getY() * v1.getY());
}
/**
* Calculate the angle between 2 vectors
* @param v0 Vector 0
* @param v1 Vector 1
* @return Angle (radians)
*/
public static double getVectorAngle(final CoordD2 v0, final CoordD2 v1) {
// final double dotV = v0.getX() * v1.getX() + v0.getY() * v1.getY();
// final double distV = getDistance(v0) * getDistance(v1);
// if (distV == 0.0) {
// return 0.0;
// }
// return Math.acos(Math.min(dotV / distV, 1.0));
return Math.atan2(v0.getX() * v1.getY() - v0.getY() * v1.getX(), v0.getX() * v1.getX() + v0.getY() * v1.getY());
}
/**
* Calculate the direction of a vector
* @param v0 Vector 0
* @param v1 Vector 1
* @return Angle (radians)
*/
public static double getVectorAngle(final CoordI2 v) {
return Math.atan2(v.getX(), v.getY());
}
/**
* Calculate the direction of a vector
* @param v0 Vector 0
* @param v1 Vector 1
* @return Angle (radians)
*/
public static double getVectorAngle(final CoordD2 v) {
return Math.atan2(v.getX(), v.getY());
}
/**
* Calculate the dot product
* @param v0 Vector 0
* @param v1 Vector 1
* @return Dot product
*/
public static double getVectorDotProduct(final CoordD2 v0, final CoordD2 v1) {
return v0.getX() * v1.getX() + v0.getY() * v1.getY();
}
/**
* Rotates a vector
* @return Rotated vector
*/
public static CoordD2 rotateVector(final CoordD2 v, final double θ) {
return ImmutableCoordD2.create(v.getX() * Math.cos(θ) - v.getY() * Math.sin(θ), v.getX() * Math.sin(θ) + v.getY() * Math.cos(θ));
}
/**
* Checks whether coord is within bounds (e.g. composite entity)
* @param hasPosition
* @return
*/
public static boolean isInBounds(final HasPosition2 hasPosition, final CoordI2 dimension) {
return isInBounds(hasPosition.getPosition(), dimension);
}
/**
* Checks whether coord is within bounds (e.g. composite entity)
* @param coord
* @return
*/
public static boolean isInBounds(final CoordI2 coord, final CoordI2 dimension) {
if (coord == null ||
coord.getX() < 0 || coord.getX() >= dimension.getX() ||
coord.getY() < 0 || coord.getY() >= dimension.getY()) {
return false;
}
return true;
}
/**
* Signum function that returns -1 or 1.
* @param n
* @return -1 if n is negative and 1 if n is positive (including 0)
*/
public static int copySign(final int n) {
return (Integer.signum(n) >> 31) | 1;
}
/**
* Signum function that returns -1 or 1.
* @param n
* @return -1 if n is negative and 1 if n is positive (including 0)
*/
public static int copySign(final double n) {
return ((int) Math.signum(n) >> 31) | 1;
}
/**
* Intersects 2 recangles
* <!-- @deprecated Use Area2 based function intersectAreas() instead -->
* @param r0 Rectangle 0
* @param r1 Rectangle 1
* @return The intersected area if r0 and r1 overlap, else null
*/
// @Deprecated
public static RectI2 intersectRectangles(final RectI2 r0, final RectI2 r1) {
final CoordI2 size0 = ImmutableCoordI2.create(r0.getC1().getX() - r0.getC0().getX(), r0.getC1().getY() - r0.getC0().getY());
final CoordI2 size1 = ImmutableCoordI2.create(r1.getC1().getX() - r1.getC0().getX(), r1.getC1().getY() - r1.getC0().getY());
final Rectangle rect0 = new Rectangle(r0.getC0().getX(), r0.getC0().getY(), size0.getX(), size0.getY());
final Rectangle rect1 = new Rectangle(r1.getC0().getX(), r1.getC0().getY(), size1.getX(), size1.getY());
final Rectangle intersection = rect0.intersection(rect1);
if (intersection.isEmpty()) {
return null;
}
return new RectI2(
ImmutableCoordI2.create((int) intersection.getX(), (int) intersection.getY()),
ImmutableCoordI2.create((int) (intersection.getX() + intersection.getWidth()), (int) (intersection.getY() + intersection.getHeight())));
}
/**
* Intersects 2 areas
* @param r0 Area 0
* @param r1 Area 1
* @return The intersected area if r0 and r1 overlap, else null
*/
public static Area2 intersectAreas(final HasPositionAndDimension2 a0, final HasPositionAndDimension2 a1) {
final Rectangle rect0 = new Rectangle(a0.getPosition().getX(), a0.getPosition().getY(), a0.getDimension().getX(), a0.getDimension().getY());
final Rectangle rect1 = new Rectangle(a1.getPosition().getX(), a1.getPosition().getY(), a1.getDimension().getX(), a1.getDimension().getY());
final Rectangle intersection = rect0.intersection(rect1);
if (intersection.isEmpty()) {
return null;
}
return new Area2(
ImmutableCoordI2.create((int) intersection.getX(), (int) intersection.getY()),
ImmutableCoordI2.create((int) intersection.getWidth(), (int) intersection.getHeight()));
}
/**
* Checks whether 2 objects intersect
* @param obj0 HasPositionAndDimension2 object 0
* @param obj1 HasPositionAndDimension2 object 1
* @return true / false whether obj0 intersects with obj1
*/
public static boolean intersects(final HasPositionAndDimension2 obj0, final HasPositionAndDimension2 obj1) {
return intersects(obj0.getPosition(), obj0.getDimension(), obj1.getPosition(), obj1.getDimension());
}
/**
* Checks whether 2 objects intersect
* @param position0 position of object 0
* @param dimension0 dimension of object 0
* @param position1 position of object 1
* @param dimension1 dimension of object 1
* @return true / false whether obj0 intersects with obj1
*/
public static boolean intersects(final CoordI2 position0, final CoordI2 dimension0, final CoordI2 position1, final CoordI2 dimension1) {
int d0x = dimension0.getX();
int d0y = dimension0.getY();
int d1x = dimension1.getX();
int d1y = dimension1.getY();
if (d0x <= 0 || d0y <= 0 || d1x <= 0 || d1y <= 0) {
return false;
}
d0x += position0.getX();
d0y += position0.getY();
d1x += position1.getX();
d1y += position1.getY();
return ((d0x < position0.getX() || d0x > position1.getX()) &&
(d0y < position0.getY() || d0y > position1.getY()) &&
(d1x < position1.getX() || d1x > position0.getX()) &&
(d1y < position1.getY() || d1y > position0.getY()));
}
/**
* Builds an aligned Area2 from arbitrary points
* @return
*/
public static Area2 pointsToArea(final CoordI2... points) {
if (points == null || points.length == 0) {
return null;
}
final CoordI2 corner0 = MutableCoordI2.create(points[0]);
final CoordI2 corner1 = MutableCoordI2.create(points[0]).add(1, 1);
for (int i = 1; i < points.length; i++) {
corner0.setXY(Math.min(corner0.getX(), points[i].getX()), Math.min(corner0.getY(), points[i].getY()));
corner1.setXY(Math.max(corner1.getX(), points[i].getX() + 1), Math.max(corner1.getY(), points[i].getY() + 1));
}
final CoordI2 dimension = ImmutableCoordI2.create(Math.max(1, corner1.getX() - corner0.getX()), Math.max(1, corner1.getY() - corner0.getY()));
return new Area2(ImmutableCoordI2.create(corner0), dimension);
}
/**
* Calculates the point of intersection to intercept target from origin with velocity velOrigin
* @param pOrigin_ Position of origin (e.g. ship that intercepts target)
* @param velOrigin Velocity of origin
* @param pTarget_ Position of target
* @param vTarget_ Velocity vector of target
* @return Point of interception or null if interception is not possible
*/
public static CoordI2 intercept(final CoordI2 pOrigin_, final double velOrigin, final CoordI2 pTarget_, final CoordD2 vTarget_) {
final CoordD2 pOrigin = ImmutableCoordD2.create(pOrigin_);
final CoordD2 pTarget = ImmutableCoordD2.create(pTarget_);
final CoordD2 vTarget = ImmutableCoordD2.create(vTarget_);
final double a = (vTarget.getX() * vTarget.getX()) + (vTarget.getY() * vTarget.getY()) - (velOrigin * velOrigin);
final double b = 2 * ((pTarget.getX() * vTarget.getX()) + (pTarget.getY() * vTarget.getY()) - (pOrigin.getX() * vTarget.getX()) - (pOrigin.getY() * vTarget.getY()));
final double c = (pTarget.getX() * pTarget.getX()) + (pTarget.getY() * pTarget.getY()) + (pOrigin.getX() * pOrigin.getX()) + (pOrigin.getY() * pOrigin.getY()) - (2 * pOrigin.getX() * pTarget.getX()) - (2 * pOrigin.getY() * pTarget.getY());
final double t0 = (-b + Math.sqrt((b * b) - (4.0 * a * c))) / (2.0 * a);
final double t1 = (-b - Math.sqrt((b * b) - (4.0 * a * c))) / (2.0 * a);
final boolean t0Pass = !Double.isNaN(t0) && t0 > 0.0;
final boolean t1Pass = !Double.isNaN(t1) && t1 > 0.0;
double t;
if (t0Pass && t1Pass) {
t = Math.min(t0, t1);
}
else if (t0Pass) {
t = t0;
}
else if (t1Pass) {
t = t1;
}
else {
return null;
}
return ImmutableCoordI2.create(MutableCoordD2.create(vTarget).multiply(t).add(pTarget));
}
/**
* Returns the sector the point is located from source's view
* @param point
* @param source
* @return Sector (Edge2)
*/
public static Edge2 getSector(final CoordI2 point, final HasPositionAndDimension2 source) {
if (ImmutableCoordI2.zero().equals(source.getDimension())) {
throw new IllegalArgumentException("Source's dimension is zero");
}
final DistanceAndSector dists[] = new DistanceAndSector[4];
final CoordI2 targetPos = ImmutableCoordI2.create(source.getPosition());
dists[0] = new DistanceAndSector(getDistance(point, targetPos), 0b1);
dists[1] = new DistanceAndSector(getDistance(point, targetPos.add(source.getDimension().getX(), 0)), 0b10);
dists[2] = new DistanceAndSector(getDistance(point, targetPos.add(0, source.getDimension().getY())), 0b100);
dists[3] = new DistanceAndSector(getDistance(point, targetPos.add(source.getDimension().getX(), source.getDimension().getY())), 0b1000);
Arrays.sort(dists, (e0, e1) -> Double.compare(e0.dist, e1.dist));
final int sector = dists[0].sector | dists[1].sector;
if (sector == 0b11) {
return Edge2.TOP;
}
else if (sector == 0b101) {
return Edge2.LEFT;
}
else if (sector == 0b1010) {
return Edge2.RIGHT;
}
else if (sector == 0b1100) {
return Edge2.BOTTOM;
}
throw new RuntimeException("Unknown sector 0b" + Integer.toString(sector, 2));
}
/**
* Calculates the pedal of altitude c of triangle A.B,C
* @param a Point A
* @param b Point B
* @param c Point C
* @return Pedal of altitude c
*/
public static CoordD2 getPedal(final CoordI2 a, final CoordI2 b, final CoordI2 c) {
final double distA = getDistance(a, b);
if (distA == 0.0) {
return null;
}
final double distASquare = Math.pow(distA, 2.0);
final double distB = getDistance(b, c);
final double distBSquare = Math.pow(distB, 2.0);
final double distC = getDistance(c, a);
final double distCSquare = Math.pow(distC, 2.0);
final double altA = Math.sqrt((2.0 * ((distASquare * distBSquare) + (distBSquare * distCSquare) + (distCSquare * distASquare))) - (Math.pow(distA, 4.0) + Math.pow(distB, 4.0) + Math.pow(distC, 4.0))) / (2.0 * distA);
if (Double.isNaN(altA)) {
return null;
}
final double alpha = getVectorAngle(b.substract(a), c.substract(a));
final double scaleA = (Math.sqrt(distCSquare - Math.pow(altA, 2.0)) / distA) * copySign(Math.PI / 2.0 - Math.abs(alpha));
if (Double.isNaN(scaleA)) {
return null;
}
return ImmutableCoordD2.create(
(((double) b.getX() - (double) a.getX()) * scaleA) + a.getX(),
(((double) b.getY() - (double) a.getY()) * scaleA) + a.getY());
}
/**
* Get's the pedal of altitude c of triangle A.B,C or the closest point on line segment a,b
* <pre>
* a-----p2-------b p1
* | \ |
* | \ |
* | \|
* c2 c1
* </pre>
*
* Returns null if there is no pedal.
*/
public static CoordD2 getPedalOnLineSegmentAB(final CoordI2 a, final CoordI2 b, final CoordI2 c) {
final CoordD2 pedal = getPedal(a, b, c);
if (pedal == null) {
return null;
}
double x = pedal.getX();
final double minX = Math.min(a.getX(), b.getX());
if (x < minX) {
x = minX;
}
final double maxX = Math.max(a.getX(), b.getX());
if (x > maxX) {
x = maxX;
}
double y = pedal.getY();
final double minY = Math.min(a.getY(), b.getY());
if (y < minY) {
y = minY;
}
final double maxY = Math.max(a.getY(), b.getY());
if (y > maxY) {
y = maxY;
}
return ImmutableCoordD2.create(x, y);
}
/**
* Get's the closest point at area target from origin
* <pre>
* *-----*
* | |
* | x _______ origin
* | |
* *-----*
* </pre>
* @param origin
* @param target
* @return
*/
public static CoordI2 getClosestPoint(final CoordI2 origin, final HasPositionAndDimension2 target) {
double minDistance = Double.MAX_VALUE;
CoordI2 result = null;
final List<CoordI2> vertices = getVertices(target);
for (int i = 1; i < 4; i++) {
final CoordD2 pedal = getPedalOnLineSegmentAB(vertices.get(i - 1), vertices.get(i), origin);
if (pedal != null) {
final CoordI2 pedalI = pedal.toImmutableCoordI2();
final double distance = getDistance(origin, pedalI);
if (distance < minDistance) {
minDistance = distance;
result = pedalI;
}
}
}
return result;
}
/**
* Calculates waypoints to evade a collision between origin and target.
* @param pOrigin Origin
* @param velOrigin Origin velocity
* @param vOrigin Origin velocity vector, origin course not necessarily the current velocity vector
* @param pTarget Target
* @param vTarget Target velocity vector
* @param oTarget Collision offset on target (entityPos)
* @param pInterception (Optional) Point of interception. Can be null, in this case it is calculated based on the velocity of source and the velocity vector of target.
* @return Waypoints for evasion or null if there is no collision
*/
public static List<CoordI2> evade(final HasPositionAndDimension2 origin, final double velOrigin, final CoordD2 vOrigin, final HasPositionAndDimension2 target, final CoordD2 vTarget, final CoordI2 oTarget, final CoordI2 pInterception_) {
final double THRESHOLD_NOT_MOVING = 0.2;
final CoordI2 pOrigin = ImmutableCoordI2.create(origin.getPosition()).add(ImmutableCoordI2.create(origin.getDimension()).divide(2));
final CoordI2 pTarget = ImmutableCoordI2.create(target.getPosition()).add(ImmutableCoordI2.create(target.getDimension()).divide(2));
final double velTarget = getDistance(vTarget);
if (velOrigin < THRESHOLD_NOT_MOVING) {
// Corner case: Origin is not moving
final CoordD2 vOriginToTarget = ImmutableCoordD2.create(pTarget).substract(ImmutableCoordD2.create(pOrigin));
CoordD2 vEvasion = MutableCoordD2
.create(-vOriginToTarget.getY(), vOriginToTarget.getX()) // Rotate by PI/2
.toVNorm()
.multiply(getDistance(origin.getDimension()) * 2.0);
vEvasion = vEvasion.setXY(Math.ceil(vEvasion.getX()), Math.ceil(vEvasion.getY()));
return List.of(pOrigin.add(ImmutableCoordI2.create(vEvasion)));
}
// Point of interception
CoordI2 pInterception;
if (pInterception_ == null) {
pInterception = intercept(pOrigin, velOrigin, pTarget, vTarget);
if (pInterception == null) {
return null;
}
}
else {
pInterception = pInterception_;
}
// Create vertices
final CoordI2 boxDim = ImmutableCoordI2.create(
Math.max(origin.getDimension().getX(), origin.getDimension().getY()) + 4,
Math.max(origin.getDimension().getX(), origin.getDimension().getY()) + 4);
final CoordI2 targetDimHalf = target.getDimension().divide(2);
final Area2 box = new Area2(pInterception
.add(targetDimHalf.substract(oTarget))
.substract(targetDimHalf)
.substract(boxDim),
targetDimHalf
.add(boxDim)
.multiply(2));
final List<CoordI2> vertices = getVertices(box);
// Rule out one or more vertices, because ...
if (velTarget >= THRESHOLD_NOT_MOVING && Math.abs(getVectorAngle(vOrigin, vTarget)) < Math.PI / 4.0) { // Velocity vectors are similar -> collision from behind
// Remove both vertices in origin move direction
int removeVertexIdxLow = -1;
int removeVertexIdxHigh = -1;
double aLow = Math.PI;
double aHigh = 0.0;
for (int i = 0; i < vertices.size(); i++) {
final CoordI2 curVertex = vertices.get(i);
final CoordD2 direction = pInterception.substract(curVertex).toImmutableCoordD2();
final double a = Math.abs(getVectorAngle(vOrigin, direction));
if (removeVertexIdxLow == -1 || a < aLow) {
aLow = a;
removeVertexIdxLow = i;
}
if (removeVertexIdxHigh == -1 || a > aHigh) {
aHigh = a;
removeVertexIdxHigh = i;
}
}
vertices.set(removeVertexIdxLow, null);
vertices.set(removeVertexIdxHigh, null);
}
else if (velTarget < velOrigin - 0.75) { // Origin speed is higher
// Remove vertex farthest from origin move direction, but closest vertex will never be ruled out (-> primary)
int safeVertexIdx = 0;
double vLen = -1.0;
for (int i = 0; i < vertices.size(); i++) {
final CoordI2 curVertex = vertices.get(i);
final double curVLen = getDistance(pOrigin, curVertex);
if (vLen < 0.0 || curVLen < vLen) {
vLen = curVLen;
safeVertexIdx = i;
}
}
int removeVertexIdx = 0;
double aHigh = 0.0;
for (int i = 0; i < vertices.size(); i++) {
if (i == safeVertexIdx) {
continue;
}
final CoordI2 curVertex = vertices.get(i);
final double a = Math.abs(getVectorAngle(curVertex.substract(pOrigin).toImmutableCoordD2(), vOrigin));
if (a > aHigh) {
aHigh = a;
removeVertexIdx = i;
}
}
vertices.set(removeVertexIdx, null);
}
else {
// Remove vertex closest to target move direction
int removeVertexIdx = -1;
double vLen = -1.0;
for (int i = 0; i < vertices.size(); i++) {
final CoordI2 curVertex = vertices.get(i);
final CoordD2 curV = ImmutableCoordD2.create(pInterception.substract(curVertex)).add(vTarget);
final double curVLen = getDistance(curV);
if (vLen < 0.0 || curVLen < vLen) {
vLen = curVLen;
removeVertexIdx = i;
}
}
vertices.set(removeVertexIdx, null);
}
// Get closest vertex -> primary
int primaryVertexIdx = -1;
double closestDist = -1.0;
for (int i = 0; i < vertices.size(); i++) {
if (vertices.get(i) != null) {
final double curDist = getDistance(vertices.get(i), pOrigin);
if (closestDist < 0.0 || curDist < closestDist) {
closestDist = curDist;
primaryVertexIdx = i;
}
}
}
// Choose secondary vertex
final int candidate0 = (primaryVertexIdx + 1) % vertices.size();
final int candidate1 = Math.floorMod(primaryVertexIdx - 1, vertices.size());
int secondVertexIdx;
if (vertices.get(candidate0) != null && vertices.get(candidate1) != null) {
// Center vertex, choose secondary which is closer to origin velocity vector
final double a0 = getVectorAngle(pInterception.substract(pOrigin), vertices.get(candidate0).substract(pOrigin));
final double a1 = getVectorAngle(pInterception.substract(pOrigin), vertices.get(candidate1).substract(pOrigin));
if (Math.abs(a0) < Math.abs(a1)) {
secondVertexIdx = candidate0;
}
else {
secondVertexIdx = candidate1;
}
}
// Outer vertex, there is only one choice for the secondary one
else if (vertices.get(candidate0) != null) {
secondVertexIdx = candidate0;
}
else {
secondVertexIdx = candidate1;
}
// Check whether path from primary vertex to secondary intersects with origin path -> in this case skip the primary vertex except the closest vertex is in target velocity direction
// final double a0 = getVectorAngle(pInterception.substract(pOrigin), vertices.get(primaryVertexIdx).substract(pOrigin));
// final double a1 = getVectorAngle(pInterception.substract(pOrigin), vertices.get(secondVertexIdx).substract(pOrigin));
// final double intersecting = Math.signum(a0 * a1);
// if (intersecting < 0.0) {
// if (closestDist < getDistance(removedVertex, pOrigin)) {
// primaryVertexIdx = secondVertexIdx;
// secondVertexIdx = -1;
// }
// }
if (secondVertexIdx == -1 || vertices.get(secondVertexIdx) == null) {
return List.of(vertices.get(primaryVertexIdx));
}
return List.of(vertices.get(primaryVertexIdx), vertices.get(secondVertexIdx));
}
/**
* <pre>
* x-----x
* | |
* | |
* x-----x
* </pre>
* Returns the 4 vertices of a rectangle
* @param obj Rectangle
* @return 4 vertices ordered clock-wise
*/
public static List<CoordI2> getVertices(final HasPositionAndDimension2 obj) {
final List<CoordI2> result = new ArrayList<>(4);
final CoordI2 objPos = ImmutableCoordI2.create(obj.getPosition());
result.add(objPos);
result.add(objPos.add(obj.getDimension().getX(), 0));
result.add(objPos.add(obj.getDimension()));
result.add(objPos.add(0, obj.getDimension().getY()));
return result;
}
/**
* <pre>
* x-----x
* | |
* | |
* x-----x
* </pre>
* Returns the 4 sides of a rectangle
* @param obj Rectangle
* @return 4 sides ordered clock-wise
*/
public static List<Area2> getSides(final HasPositionAndDimension2 obj) {
final List<Area2> result = new ArrayList<>(4);
final CoordI2 objPos = ImmutableCoordI2.create(obj.getPosition());
result.add(new Area2(objPos, ImmutableCoordI2.create(obj.getDimension().getX(), 1)));
result.add(new Area2(objPos.add(obj.getDimension().getX(), 0), ImmutableCoordI2.create(1, obj.getDimension().getY())));
result.add(new Area2(objPos.add(1, obj.getDimension().getY()), ImmutableCoordI2.create(obj.getDimension().getX(), 1)));
result.add(new Area2(objPos.add(0, 1), ImmutableCoordI2.create(1, obj.getDimension().getY())));
return result;
}
/**
* Returns a random point in a circle
* @param center of the circle
* @param radius of the circle
* @return random point in the circle
*/
public static CoordI2 getRandomPointInCircle(final CoordI2 center, final int radius) {
return getRandomPointInCircle(center, radius, ThreadLocalXorShift128Random.current());
}
/**
* Returns a point in a circle
* @param center of the circle
* @param radius of the circle
* @param random rng
* @return random point in the circle
*/
public static CoordI2 getRandomPointInCircle(final CoordI2 center, final int radius, final Random random) {
return MutableCoordD2
.create(Math.cos(random.nextDouble() * Math.PI * 2.0 - Math.PI), Math.sin(random.nextDouble() * Math.PI * 2.0 - Math.PI))
.multiply(radius)
.toImmutableCoordI2()
.add(center);
}
/**
* Returns a random point on a circle
* @param center of the circle
* @param radius of the circle
* @return random point in the circle
*/
public static CoordI2 getRandomPointOnCircle(final CoordI2 center, final int radius) {
return getRandomPointOnCircle(center, radius, ThreadLocalXorShift128Random.current());
}
/**
* Returns a random point on a circle
* @param center of the circle
* @param radius of the circle
* @param random rng
* @return random point in the circle
*/
public static CoordI2 getRandomPointOnCircle(final CoordI2 center, final int radius, final Random random) {
final double angle = random.nextDouble() * Math.PI * 2.0 - Math.PI;
return MutableCoordD2
.create(Math.cos(angle), Math.sin(angle))
.multiply(radius)
.toImmutableCoordI2()
.add(center);
}
/**
* Returns a reproducible point on a circle
* @param center of the circle
* @param radius of the circle
* @param counter decides where the point is
* @return random point in the circle
*/
public static CoordI2 getReproduciblePointOnCircle(final CoordI2 center, final int radius, final AtomicInteger counter) {
final double angle = counter.incrementAndGet() / 3.0 % Math.PI * 2.0 - Math.PI;
return MutableCoordD2
.create(Math.cos(angle), Math.sin(angle))
.multiply(radius)
.toImmutableCoordI2()
.add(center);
}
/**
* Calculates edge required at c1 to block the path from c0 to c1. Swap both parameters to get edge required at c0 to block the path from c1 to c0.<br/>
* <br/>
* c0==&gt; |c1<br/>
* c0| &lt;==c1<br/>
*
* @param c0 Position
* @param c1 Target
* @return Edges that are required as bit mask
*/
public static int calcEdgeRequirement(final CoordI2 c0, final CoordI2 c1) {
final int dx = c0.getX() - c1.getX();
final int dy = c0.getY() - c1.getY();
int edgeReq = 0;
if (dx < 0 && dy < 0) {
edgeReq = Edge2.LEFT.getBit() | Edge2.TOP.getBit();
}
else if (dx < 0 && dy == 0) {
edgeReq = Edge2.LEFT.getBit();
}
else if (dx < 0 && dy > 0) {
edgeReq = Edge2.LEFT.getBit() | Edge2.BOTTOM.getBit();
}
else if (dx == 0 && dy < 0) {
edgeReq = Edge2.TOP.getBit();
}
else if (dx == 0 && dy > 0) {
edgeReq = Edge2.BOTTOM.getBit();
}
else if (dx > 0 && dy < 0) {
edgeReq = Edge2.RIGHT.getBit() | Edge2.TOP.getBit();
}
else if (dx > 0 && dy == 0) {
edgeReq = Edge2.RIGHT.getBit();
}
else if (dx > 0 && dy > 0) {
edgeReq = Edge2.RIGHT.getBit() | Edge2.BOTTOM.getBit();
}
return edgeReq;
}
private static class DistanceAndSector {
double dist;
int sector;
public DistanceAndSector(final double dist, final int sector) {
this.dist = dist;
this.sector = sector;
}
}
}

View File

@@ -0,0 +1,195 @@
/* Copyright (C) 2023 Stephan Birkl - All Rights Reserved.
* THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
* PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
* OR OTHER DEALINGS IN THE SOFTWARE.
*/
package de.extio.game_engine.spatial2.index;
import java.util.ArrayList;
import java.util.ConcurrentModificationException;
import java.util.Iterator;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import de.extio.game_engine.spatial2.model.CoordI2;
import de.extio.game_engine.spatial2.model.HasPosition2;
import de.extio.game_engine.spatial2.model.MutableCoordI2;
/**
* Implements a grid index that can query objects with dimension 1 very fast at the expense of memory consumption. Use QuadTree instead for very large grids or 2 dimensional objects.
*/
public final class GridIndex2D<T extends HasPosition2> implements Iterable<T>, AutoCloseable {
private final List<List<List<T>>> index = new ArrayList<>();
private long modCnt;
public GridIndex2D() {
}
public void add(final T obj) {
this.ensureBounds(obj.getPosition());
this.index.get(obj.getPosition().getX()).get(obj.getPosition().getY()).add(obj);
this.modCnt++;
}
public void addAll(final Iterable<T> objs) {
objs.forEach(this::add);
}
public void addAll(final Iterator<T> it) {
it.forEachRemaining(this::add);
}
public void remove(final T obj) {
if (!this.checkBounds(obj.getPosition())) {
return;
}
this.removeInternal(obj);
this.modCnt++;
}
public List<T> get(final HasPosition2 hasPosition2) {
final CoordI2 coord = hasPosition2.getPosition();
if (!this.checkBounds(coord)) {
return List.of();
}
return this.index.get(coord.getX()).get(coord.getY());
}
/**
* Clears but keeps internal state allocated
*/
public void clear() {
this.index.forEach(list -> list.forEach(List::clear));
this.modCnt++;
}
/**
* Clears and frees internal state
*/
@Override
public void close() {
this.index.clear();
this.modCnt = 0;
}
@Override
public Iterator<T> iterator() {
return new Iterator<>() {
int x;
int y;
int i;
long modCnt;
{
this.modCnt = GridIndex2D.this.modCnt;
}
@Override
public boolean hasNext() {
return this.x < GridIndex2D.this.index.size();
}
@Override
public T next() {
if (this.modCnt != GridIndex2D.this.modCnt) {
throw new ConcurrentModificationException();
}
final T result = GridIndex2D.this.index.get(this.x).get(this.y).get(this.i);
this.forward();
return result;
}
@Override
public void remove() {
GridIndex2D.this.removeInternal(GridIndex2D.this.index.get(this.x).get(this.y).get(this.i));
if (!this.validate()) {
this.forward();
}
}
private void forward() {
while (this.hasNext()) {
if (++this.i >= GridIndex2D.this.index.get(this.x).get(this.y).size()) {
this.i = 0;
if (++this.y >= GridIndex2D.this.index.get(this.x).size()) {
this.y = 0;
this.x++;
}
}
if (this.validate()) {
break;
}
}
}
private boolean validate() {
return this.x < GridIndex2D.this.index.size() && this.y < GridIndex2D.this.index.get(this.x).size() && this.i < GridIndex2D.this.index.get(this.x).get(this.y).size();
}
};
}
@Override
public void forEach(final Consumer<? super T> action) {
for (int x = 0; x < this.index.size(); x++) {
for (int y = 0; y < this.index.get(x).size(); y++) {
for (int i = 0; i < this.index.get(x).get(y).size(); i++) {
action.accept(this.index.get(x).get(y).get(i));
}
}
}
}
public void forEach(final BiConsumer<CoordI2, List<T>> action) {
final CoordI2 coord = MutableCoordI2.create();
for (int x = 0; x < this.index.size(); x++) {
for (int y = 0; y < this.index.get(x).size(); y++) {
if (this.index.get(x).get(y).size() > 0) {
coord.setXY(x, y);
action.accept(coord, this.index.get(x).get(y));
}
}
}
}
private void ensureBounds(final CoordI2 coord) {
int delta = this.index.size() - coord.getX();
if (delta < 1) {
for (delta = -delta; delta >= 0; delta--) {
this.index.add(new ArrayList<>());
}
}
delta = this.index.get(coord.getX()).size() - coord.getY();
if (delta < 1) {
for (delta = -delta; delta >= 0; delta--) {
this.index.get(coord.getX()).add(new ArrayList<>());
}
}
}
private boolean checkBounds(final CoordI2 coord) {
return coord.getX() >= 0 && coord.getY() >= 0 && coord.getX() < this.index.size() && coord.getY() < this.index.get(coord.getX()).size();
}
private void removeInternal(final T obj) {
this.index.get(obj.getPosition().getX()).get(obj.getPosition().getY()).remove(obj);
}
}

View File

@@ -0,0 +1,121 @@
/* Copyright (C) 2023 Stephan Birkl - All Rights Reserved.
* THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
* PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
* OR OTHER DEALINGS IN THE SOFTWARE.
*/
package de.extio.game_engine.spatial2.index;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Predicate;
import de.extio.game_engine.spatial2.WorldUtils2;
import de.extio.game_engine.spatial2.model.CoordI2;
import de.extio.game_engine.spatial2.model.ImmutableCoordI2;
import de.extio.game_engine.spatial2.model.SpatialIndex2Capable;
/**
* LinearSearchIndex2D doing linear search only. Use it only for very special use cases. Implementation is mainly intended for performance comparison.
*/
public final class LinearSearchIndex2D<T extends SpatialIndex2Capable> implements SpatialIndex2D<T> {
private static final int RESULT_DIM_START = 8;
private static final int RESULT_DIM_MAX = 96;
private List<T> objects = new ArrayList<>();
private int resultDim = RESULT_DIM_START;
@Override
public Iterator<T> iterator() {
return this.objects.iterator();
}
@Override
public void add(final T obj) {
this.objects.add(obj);
}
@Override
public void remove(final T obj) {
this.objects.remove(obj);
}
@Override
public void move(final T obj, final CoordI2 oldPosition, final CoordI2 oldDimension) {
}
@Override
public List<T> find(final CoordI2 position, final CoordI2 dimension, final Predicate<T> filter) {
List<T> result = null;
for (int i = 0; i < this.objects.size(); i++) {
final T obj = this.objects.get(i);
if (WorldUtils2.intersects(position, dimension, obj.getPosition(), obj.getDimension())) {
if (filter == null || filter.test(obj)) {
if (result == null) {
result = new ArrayList<>(dimension.equals(ImmutableCoordI2.one()) ? RESULT_DIM_START : this.resultDim);
}
result.add(obj);
}
}
}
if (result == null) {
return List.of();
}
if (result.size() > this.resultDim) {
this.resultDim = Math.min(result.size(), RESULT_DIM_MAX);
}
return result;
}
@Override
public T findFirstAt(final CoordI2 position, final Predicate<T> filter) {
for (int i = 0; i < this.objects.size(); i++) {
final T obj = this.objects.get(i);
if (WorldUtils2.intersects(position, ImmutableCoordI2.one(), obj.getPosition(), obj.getDimension())) {
if (filter == null || filter.test(obj)) {
return obj;
}
}
}
return null;
}
@Override
public void executeAt(final CoordI2 position, final Consumer<T> consumer, final Predicate<T> filter) {
for (int i = 0; i < this.objects.size(); i++) {
final T obj = this.objects.get(i);
if (WorldUtils2.intersects(position, ImmutableCoordI2.one(), obj.getPosition(), obj.getDimension())) {
if (filter == null || filter.test(obj)) {
consumer.accept(obj);
}
}
}
}
@Override
public void close() {
this.objects = new ArrayList<>();
this.resultDim = RESULT_DIM_START;
}
@Override
public void clear() {
this.objects.clear();
}
}

View File

@@ -0,0 +1,293 @@
/* Copyright (C) 2023 Stephan Birkl - All Rights Reserved.
* THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
* PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
* OR OTHER DEALINGS IN THE SOFTWARE.
*/
package de.extio.game_engine.spatial2.index;
import java.util.ArrayList;
import java.util.ConcurrentModificationException;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import de.extio.game_engine.spatial2.model.CoordI2;
import de.extio.game_engine.spatial2.model.Edge2;
import de.extio.game_engine.spatial2.model.HasPosition2;
import de.extio.game_engine.spatial2.model.ImmutableCoordI2;
import de.extio.game_engine.spatial2.model.MutableCoordI2;
/**
* Implements a 2D matrix
*/
public final class Matrix2<T> implements Iterable<T> {
private List<List<T>> matrix = new ArrayList<>();
private final CoordI2 dimension = MutableCoordI2.create();
private long modCnt;
public Matrix2() {
}
public Matrix2(final int x, final int y) {
this.allocate(Math.max(0, x - 1), Math.max(0, y - 1));
}
public Matrix2(final Matrix2<T> other) {
this.dimension.setX(other.matrix.size());
this.matrix = new ArrayList<>(other.matrix);
for (int x = 0; x < this.matrix.size(); x++) {
final List<T> column = other.matrix.get(x);
this.matrix.set(x, new ArrayList<>(column));
if (column.size() > this.dimension.getY()) {
this.dimension.setY(column.size());
}
}
}
/**
* Clears and frees internal state
*/
public void close() {
this.matrix.clear();
this.dimension.setXY(0, 0);
this.modCnt++;
}
/**
* Clears but keeps internal state allocated
*/
public void clear() {
this.matrix.forEach(col -> col.replaceAll((t) -> null));
this.modCnt++;
}
public void put(final HasPosition2 hasPosition, final T obj) {
this.put(hasPosition.getPosition().getX(), hasPosition.getPosition().getY(), obj);
}
public void put(final int x, final int y, final T obj) {
if (x < 0 || y < 0) {
return;
}
this.allocate(x, y);
this.matrix.get(x).set(y, obj);
this.modCnt++;
}
public void remove(final HasPosition2 hasPosition) {
this.remove(hasPosition.getPosition().getX(), hasPosition.getPosition().getY());
}
public void remove(final int x, final int y) {
if (!this.checkBounds(x, y)) {
return;
}
this.matrix.get(x).set(y, null);
this.modCnt++;
}
public void removeAll() {
for (int x = 0; x < this.matrix.size(); x++) {
for (int y = 0; y < this.matrix.get(x).size(); y++) {
this.matrix.get(x).set(y, null);
}
}
this.modCnt++;
}
public T get(final HasPosition2 hasPosition) {
return this.get(hasPosition.getPosition().getX(), hasPosition.getPosition().getY());
}
public T get(final int x, final int y) {
if (!this.checkBounds(x, y)) {
return null;
}
return this.matrix.get(x).get(y);
}
public boolean contains(final HasPosition2 hasPosition) {
return this.contains(hasPosition.getPosition().getX(), hasPosition.getPosition().getY());
}
public boolean contains(final int x, final int y) {
return this.checkBounds(x, y) && this.matrix.get(x).get(y) != null;
}
// x and y are indices, not the dimension or size!
public void allocate(final int x, final int y) {
if (x < 0 || y < 0) {
return;
}
int delta = this.matrix.size() - x;
if (delta < 1) {
for (delta = -delta; delta >= 0; delta--) {
final List<T> row = new ArrayList<>(y + 1);
for (int i = 0; i <= y; i++) {
row.add(null);
}
this.matrix.add(row);
}
if (x >= this.dimension.getX()) {
this.dimension.setX(x + 1);
}
if (y >= this.dimension.getY()) {
this.dimension.setY(y + 1);
}
}
delta = this.matrix.get(x).size() - y;
if (delta < 1) {
for (delta = -delta; delta >= 0; delta--) {
this.matrix.get(x).add(null);
}
if (y >= this.dimension.getY()) {
this.dimension.setY(y + 1);
}
}
}
public void rotate(final Edge2 direction) {
final List<List<T>> original = new ArrayList<>(this.matrix);
final CoordI2 origDim = ImmutableCoordI2.create(this.dimension);
this.close();
this.allocate(origDim.getY() - 1, origDim.getX() - 1);
final CoordI2 posSrc = MutableCoordI2.create();
for (int x = 0; x < origDim.getY(); x++) {
for (int y = 0; y < origDim.getX(); y++) {
if (direction == Edge2.LEFT) {
posSrc.setXY(origDim.getX() - y - 1, x);
}
else {
posSrc.setXY(y, origDim.getY() - x - 1);
}
final List<T> originalObjs = original.get(posSrc.getX());
if (originalObjs.size() > posSrc.getY()) {
this.matrix.get(x).set(y, originalObjs.get(posSrc.getY()));
}
}
}
}
@Override
public Iterator<T> iterator() {
return new Iterator<>() {
private int x = 0;
private int y = 0;
private T obj = null;
private long modCnt;
{
this.modCnt = Matrix2.this.modCnt;
}
@Override
public boolean hasNext() {
return this.findNext(false) != null;
}
@Override
public T next() {
try {
if (this.modCnt != Matrix2.this.modCnt) {
throw new ConcurrentModificationException();
}
return this.findNext(true);
}
finally {
this.obj = null;
}
}
private T findNext(final boolean nseExc) {
if (this.obj != null) {
return this.obj;
}
int x = this.x;
int y = this.y;
for (; x < Matrix2.this.matrix.size(); x++) {
for (; y < Matrix2.this.matrix.get(x).size(); y++) {
this.obj = Matrix2.this.matrix.get(x).get(y);
if (this.obj != null) {
this.x = x;
this.y = y + 1;
if (this.y >= Matrix2.this.matrix.get(x).size()) {
this.y = 0;
this.x++;
}
return this.obj;
}
}
y = 0;
}
if (nseExc) {
throw new NoSuchElementException();
}
return null;
}
};
}
@Override
public void forEach(final Consumer<? super T> action) {
for (int x = 0; x < this.matrix.size(); x++) {
for (int y = 0; y < this.matrix.get(x).size(); y++) {
final T obj = this.matrix.get(x).get(y);
if (obj != null) {
action.accept(obj);
}
}
}
}
public void forEach(final BiConsumer<CoordI2, T> consumer) {
final CoordI2 coord = MutableCoordI2.create();
for (int x = 0; x < this.matrix.size(); x++) {
for (int y = 0; y < this.matrix.get(x).size(); y++) {
final T obj = this.matrix.get(x).get(y);
if (obj != null) {
coord.setXY(x, y);
consumer.accept(coord, obj);
}
}
}
}
public CoordI2 getDimension() {
return ImmutableCoordI2.create(this.dimension);
}
private boolean checkBounds(final int x, final int y) {
return x >= 0 && y >= 0 && x < this.matrix.size() && y < this.matrix.get(x).size();
}
}

View File

@@ -0,0 +1,733 @@
/* Copyright (C) 2023 Stephan Birkl - All Rights Reserved.
* THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
* PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
* OR OTHER DEALINGS IN THE SOFTWARE.
*/
package de.extio.game_engine.spatial2.index;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Predicate;
import de.extio.game_engine.spatial2.model.CoordI2;
import de.extio.game_engine.spatial2.model.ImmutableCoordI2;
import de.extio.game_engine.spatial2.model.MutableCoordI2;
import de.extio.game_engine.spatial2.model.RectI2;
import de.extio.game_engine.spatial2.model.SpatialIndex2Capable;
/**
* This is the preferred SpatialIndex2D implementation to index bigger amounts of spatial data and query them efficiently.
* You can consider using GridIndex2D instead for small grids with one-dimensional objects only.
* A quadtree is a tree data structure in which each internal node has exactly four children. Quadtrees are the two-dimensional analog of octrees and are most often used to partition a two-dimensional space by recursively subdividing it into four quadrants or regions.
*/
public final class QuadTree<T extends SpatialIndex2Capable> implements SpatialIndex2D<T> {
private static final int ORDER = 4;
private static final int DEPTH_START = 8;
private static final int RESULT_DIM_START = 6;
private static final int RESULT_DIM_MAX = 96;
private Node<T> root;
private int depth;
private int[] traverseStack;
private Node<T>[] nodesCache;
private int resultDim = RESULT_DIM_START;
private final RectI2 rectCached = new RectI2(MutableCoordI2.create(), MutableCoordI2.create());
private final AtomicReference<List<T>> resultHolder = new AtomicReference<>();
public QuadTree() {
}
@SuppressWarnings("unchecked")
@Override
public void add(final T obj) {
final RectI2 rect = this.toRectI2(obj.getPosition(), obj.getDimension());
this.expandTree(rect);
Node<T> node = this.root;
traversal: while (true) {
if (node.branches == null) {
// Add leaf to fresh branch
this.addLeave(node, obj);
if (node.leaves.size() < ORDER) {
break;
}
// Overflow -> Split if not already at lowest level
if (node.createBranches()) {
final List<T> toSplit = node.leaves;
if (toSplit != null) {
node.leaves = null;
splitLeaves: for (int l = 0; l < toSplit.size(); l++) {
final T leaf = toSplit.get(l);
for (int b = 0; b < ORDER; b++) {
if (this.contains(node.branches[b], leaf)) {
this.addLeave(node.branches[b], leaf);
continue splitLeaves;
}
}
this.addLeave(node, leaf);
}
}
}
break;
}
else {
// Traverse
for (int i = 0; i < ORDER; i++) {
if (this.contains(node.branches[i], rect)) {
node = node.branches[i];
continue traversal;
}
}
}
// Node does not fit into branches -> add to current level
this.addLeave(node, obj);
break;
}
}
@SuppressWarnings("unchecked")
private void expandTree(final RectI2 rect) {
if (this.root == null) {
final int side = 1 << DEPTH_START;
this.root = new Node<>(rect.getC0().getX() - side, rect.getC0().getY() - side, side << 1);
this.root.createBranches();
this.recalcDepth();
}
for (int i = 0; !this.contains(this.root, rect); i++) {
final RectI2 expansion = new RectI2(MutableCoordI2.create(), MutableCoordI2.create());
final int[] branchesIntersect = new int[4];
int intersects = 0;
int containsBranch = -1;
expansion.getC0().setXY(this.root.xMin, this.root.yMin);
expansion.getC1().setXY(Integer.MAX_VALUE, Integer.MAX_VALUE);
if (this.contains(expansion, rect)) {
containsBranch = 0;
}
else {
if (this.intersects(expansion, rect)) {
branchesIntersect[intersects++] = 0;
}
expansion.getC0().setXY(Integer.MIN_VALUE, this.root.yMin);
expansion.getC1().setXY(this.root.xMax, Integer.MAX_VALUE);
if (this.contains(expansion, rect)) {
containsBranch = 2;
}
else {
if (this.intersects(expansion, rect)) {
branchesIntersect[intersects++] = 2;
}
expansion.getC0().setXY(Integer.MIN_VALUE, Integer.MIN_VALUE);
expansion.getC1().setXY(this.root.xMax, this.root.yMax);
if (this.contains(expansion, rect)) {
containsBranch = 3;
}
else {
if (this.intersects(expansion, rect)) {
branchesIntersect[intersects++] = 3;
}
expansion.getC0().setXY(this.root.xMin, Integer.MIN_VALUE);
expansion.getC1().setXY(Integer.MAX_VALUE, this.root.yMax);
if (this.contains(expansion, rect)) {
containsBranch = 1;
}
else if (this.intersects(expansion, rect)) {
branchesIntersect[intersects++] = 1;
}
}
}
}
Node<T> rootNew = null;
final int branch = containsBranch != -1 ? containsBranch : branchesIntersect[i % intersects];
final int size = this.root.xMax - this.root.xMin + 1;
switch (branch) {
case 0:
rootNew = new Node<>(this.root.xMin, this.root.yMin, size << 1);
break;
case 1:
rootNew = new Node<>(this.root.xMin, this.root.yMin - size, size << 1);
break;
case 2:
rootNew = new Node<>(this.root.xMin - size, this.root.yMin, size << 1);
break;
case 3:
rootNew = new Node<>(this.root.xMin - size, this.root.yMin - size, size << 1);
break;
}
rootNew.createBranches();
rootNew.branches[branch] = this.root;
rootNew.branches[branch].parent = rootNew;
this.root = rootNew;
this.recalcDepth();
}
// Alternative approach, center around the origin. In principle very elegant but the tree always expands to all directions whereas the data may only expand to a specific direction
// while (!this.contains(this.root, rect)) {
// final int side = this.root.xMax - this.root.xMin + 1;
// final Node<T> rootNew = new Node<>(this.root.xMin - (side >> 1), this.root.yMin - (side >> 1), side << 1);
// rootNew.createBranches();
// for (int i = 0; i < ORDER; i++) {
// rootNew.branches[i].createBranches();
// }
// rootNew.branches[0].branches[3] = this.root.branches[0];
// rootNew.branches[0].branches[3].parent = rootNew.branches[0];
// rootNew.branches[1].branches[2] = this.root.branches[1];
// rootNew.branches[1].branches[2].parent = rootNew.branches[1];
// rootNew.branches[2].branches[1] = this.root.branches[2];
// rootNew.branches[2].branches[1].parent = rootNew.branches[2];
// rootNew.branches[3].branches[0] = this.root.branches[3];
// rootNew.branches[3].branches[0].parent = rootNew.branches[3];
// rootNew.leaves = this.root.leaves;
//
// this.root = rootNew;
// this.recalcDepth();
// }
}
@SuppressWarnings("unchecked")
private void recalcDepth() {
this.depth = Integer.numberOfTrailingZeros(Integer.highestOneBit(this.root.xMax - this.root.xMin)) + 1;
this.traverseStack = new int[this.depth];
this.nodesCache = new Node[this.depth];
}
private void addLeave(final Node<T> node, final T obj) {
if (node.leaves == null) {
node.leaves = new ArrayList<>();
}
node.leaves.add(obj);
}
@Override
public void remove(final T obj) {
final RectI2 rect = this.toRectI2Bounded(obj.getPosition(), obj.getDimension());
if (rect == null) {
return;
}
this.remove(obj, rect);
}
@SuppressWarnings("unchecked")
private void remove(final T obj, final RectI2 rect) {
Node<T> node = this.root;
traversal: while (true) {
if (node.leaves != null) {
node.leaves.remove(obj);
if (node.leaves.isEmpty()) {
node.leaves = null;
}
this.shrink(node);
}
if (node.branches != null) {
for (int i = 0; i < ORDER; i++) {
if (this.contains(node.branches[i], rect)) {
node = node.branches[i];
continue traversal;
}
}
}
break;
}
}
private void shrink(final Node<T> node_) {
Node<T> node = node_;
while (node.parent != null) {
for (int i = 0; i < ORDER; i++) {
if (node.parent.branches[i].leaves != null || node.parent.branches[i].branches != null) {
return;
}
}
node.parent.branches = null;
node = node.parent;
}
}
@SuppressWarnings("unchecked")
@Override
public void move(final T obj, final CoordI2 oldPosition, final CoordI2 oldDimension) {
final RectI2 rect = this.toRectI2Bounded(oldPosition, oldDimension);
if (rect != null) {
int nodes = 0;
Node<T> node = this.root;
traversal: while (true) {
if (node.leaves != null) {
this.nodesCache[nodes++] = node;
}
if (node.branches != null) {
for (int i = 0; i < ORDER; i++) {
if (this.contains(node.branches[i], rect)) {
node = node.branches[i];
continue traversal;
}
}
}
if (!this.contains(node, obj)) {
while (nodes-- > 0) {
final Node<T> curNode = this.nodesCache[nodes];
if (curNode.leaves != null) {
curNode.leaves.remove(obj);
if (curNode.leaves.isEmpty()) {
curNode.leaves = null;
}
}
}
this.shrink(node);
this.add(obj);
}
break;
}
}
else {
this.add(obj);
}
}
@Override
public List<T> find(final CoordI2 position, final CoordI2 dimension, final Predicate<T> filter) {
if (dimension.equals(ImmutableCoordI2.one())) {
return this.findDim1(position, filter);
}
final RectI2 rect = this.toRectI2Bounded(position, dimension);
if (rect == null) {
return List.of();
}
return this.findDimN(rect, filter);
}
private List<T> findDim1(final CoordI2 position, final Predicate<T> filter) {
this.resultHolder.set(null);
this.executeAt(position, obj -> {
if (this.resultHolder.get() == null) {
this.resultHolder.set(new ArrayList<>(RESULT_DIM_START));
}
this.resultHolder.get().add(obj);
}, filter);
final List<T> result = this.resultHolder.get();
if (result == null) {
return List.of();
}
this.resultHolder.set(null);
return result;
}
@SuppressWarnings("unchecked")
private List<T> findDimN(final RectI2 rect, final Predicate<T> filter) {
List<T> result = null;
int level = 0;
int branch = 0;
Node<T> node = this.root;
traversal: while (true) {
if (node.branches != null) {
for (int b = branch; b < ORDER; b++) {
if (this.intersects(node.branches[b], rect)) {
node = node.branches[b];
this.traverseStack[level++] = b + 1;
branch = 0;
continue traversal;
}
}
}
if (node.leaves != null) {
for (int i = 0; i < node.leaves.size(); i++) {
final T obj = node.leaves.get(i);
if (this.intersects(obj, rect) && (filter == null || filter.test((T) obj))) {
if (result == null) {
result = new ArrayList<>(this.resultDim);
}
result.add(obj);
}
}
}
if (node.parent == null) {
break;
}
node = node.parent;
branch = this.traverseStack[--level];
}
if (result == null) {
return List.of();
}
if (result.size() > this.resultDim) {
this.resultDim = Math.min(result.size(), RESULT_DIM_MAX);
}
return result;
}
@SuppressWarnings("unchecked")
@Override
public T findFirstAt(final CoordI2 position, final Predicate<T> filter) {
final RectI2 rect = this.toRectI2Bounded(position, ImmutableCoordI2.one());
if (rect == null) {
return null;
}
Node<T> node = this.root;
traversal: while (true) {
if (node.leaves != null) {
for (int i = 0; i < node.leaves.size(); i++) {
final T obj = node.leaves.get(i);
if (this.contains(obj, rect) && (filter == null || filter.test((T) obj))) {
return obj;
}
}
}
if (node.branches != null) {
for (int i = 0; i < ORDER; i++) {
if (this.contains(node.branches[i], rect)) {
node = node.branches[i];
continue traversal;
}
}
}
break;
}
return null;
}
@SuppressWarnings("unchecked")
@Override
public void executeAt(final CoordI2 position, final Consumer<T> consumer, final Predicate<T> filter) {
final RectI2 rect = this.toRectI2Bounded(position, ImmutableCoordI2.one());
if (rect == null) {
return;
}
Node<T> node = this.root;
traversal: while (true) {
if (node.leaves != null) {
for (int i = 0; i < node.leaves.size(); i++) {
final T obj = node.leaves.get(i);
if (this.contains(obj, rect) && (filter == null || filter.test((T) obj))) {
consumer.accept(obj);
}
}
}
if (node.branches != null) {
for (int i = 0; i < ORDER; i++) {
if (this.contains(node.branches[i], rect)) {
node = node.branches[i];
continue traversal;
}
}
}
break;
}
}
@Override
public Iterator<T> iterator() {
return new Iterator<>() {
private final Deque<Integer> branchStack = new ArrayDeque<>(QuadTree.this.depth);
private T next;
private Node<T> node = QuadTree.this.root;
private int branch;
private int leaf;
@SuppressWarnings("unchecked")
@Override
public boolean hasNext() {
if (this.next != null) {
return true;
}
traversal: while (true) {
if (this.node.branches != null) {
if (this.branch < ORDER) {
this.node = this.node.branches[this.branch];
this.branchStack.addFirst(this.branch + 1);
this.branch = 0;
continue traversal;
}
}
if (this.node.leaves != null) {
if (this.leaf < this.node.leaves.size()) {
final T obj = this.node.leaves.get(this.leaf++);
this.next = obj;
break traversal;
}
this.leaf = 0;
}
if (this.node.parent == null) {
break;
}
this.node = this.node.parent;
this.branch = this.branchStack.removeFirst();
}
return this.next != null;
}
@Override
public T next() {
try {
return this.next;
}
finally {
this.next = null;
}
}
@Override
public void remove() {
if (this.leaf > 0 && this.leaf <= this.node.leaves.size()) {
this.node.leaves.remove(this.leaf-- - 1);
}
}
};
}
@Override
public void forEach(final Consumer<? super T> action) {
this.traverseAll(node -> {
if (node.leaves != null) {
for (int i = 0; i < node.leaves.size(); i++) {
final T obj = node.leaves.get(i);
action.accept(obj);
}
}
});
}
@Override
public void close() {
this.resultDim = RESULT_DIM_START;
this.traverseStack = null;
this.nodesCache = null;
this.root = null;
}
@Override
public void clear() {
this.traverseAll(node -> {
if (node.leaves != null) {
node.leaves = null;
}
});
}
@SuppressWarnings("unchecked")
private void traverseAll(final Consumer<Node<T>> consumer) {
if (this.root == null) {
return;
}
int level = 0;
int branch = 0;
Node<T> node = this.root;
traversal: while (true) {
if (node.branches != null && branch < ORDER) {
node = node.branches[branch];
this.traverseStack[level++] = branch + 1;
branch = 0;
continue traversal;
}
consumer.accept(node);
if (node.parent == null) {
break;
}
node = node.parent;
branch = this.traverseStack[--level];
}
}
private RectI2 toRectI2(final CoordI2 position, final CoordI2 dimension) {
this.rectCached.getC0().setXY(position.getX(), position.getY());
this.rectCached.getC1().setXY(position.getX() + dimension.getX() - 1, position.getY() + dimension.getY() - 1);
return this.rectCached;
}
private RectI2 toRectI2Bounded(final CoordI2 position, final CoordI2 dimension) {
if (this.root == null) {
return null;
}
this.rectCached.getC0().setXY(Math.max(position.getX(), this.root.xMin), Math.max(position.getY(), this.root.yMin));
this.rectCached.getC1().setXY(Math.min(position.getX() + dimension.getX() - 1, this.root.xMax), Math.min(position.getY() + dimension.getY() - 1, this.root.yMax));
if (this.rectCached.getC1().getX() - this.rectCached.getC0().getX() < 0 || this.rectCached.getC1().getY() - this.rectCached.getC0().getY() < 0) {
return null;
}
return this.rectCached;
}
private boolean contains(final Node<T> node, final RectI2 rect) {
return rect.getC0().getX() >= node.xMin &&
rect.getC0().getY() >= node.yMin &&
rect.getC1().getX() <= node.xMax &&
rect.getC1().getY() <= node.yMax;
}
private boolean contains(final Node<T> node, final T obj) {
return obj.getPosition().getX() >= node.xMin &&
obj.getPosition().getY() >= node.yMin &&
obj.getPosition().getX() + obj.getDimension().getX() - 1 <= node.xMax &&
obj.getPosition().getY() + obj.getDimension().getY() - 1 <= node.yMax;
}
private boolean contains(final T obj, final RectI2 rect) {
return rect.getC0().getX() >= obj.getPosition().getX() &&
rect.getC0().getY() >= obj.getPosition().getY() &&
rect.getC1().getX() <= obj.getPosition().getX() + obj.getDimension().getX() - 1 &&
rect.getC1().getY() <= obj.getPosition().getY() + obj.getDimension().getY() - 1;
}
private boolean contains(final RectI2 rect0, final RectI2 rect1) {
return rect1.getC0().getX() >= rect0.getC0().getX() &&
rect1.getC0().getY() >= rect0.getC0().getY() &&
rect1.getC1().getX() <= rect0.getC1().getX() &&
rect1.getC1().getY() <= rect0.getC1().getY();
}
public boolean intersects(final Node<T> node, final RectI2 rect) {
return ((node.xMax + 1 < node.xMin || node.xMax >= rect.getC0().getX()) &&
(node.yMax + 1 < node.yMin || node.yMax >= rect.getC0().getY()) &&
(rect.getC1().getX() + 1 < rect.getC0().getX() || rect.getC1().getX() >= node.xMin) &&
(rect.getC1().getY() + 1 < rect.getC0().getY() || rect.getC1().getY() >= node.yMin));
}
public boolean intersects(final T obj, final RectI2 rect) {
final int oUpperX = obj.getDimension().getX() + obj.getPosition().getX();
final int oUpperY = obj.getDimension().getY() + obj.getPosition().getY();
return ((oUpperX < obj.getPosition().getX() || oUpperX > rect.getC0().getX()) &&
(oUpperY < obj.getPosition().getY() || oUpperY > rect.getC0().getY()) &&
(rect.getC1().getX() + 1 < rect.getC0().getX() || rect.getC1().getX() >= obj.getPosition().getX()) &&
(rect.getC1().getY() + 1 < rect.getC0().getY() || rect.getC1().getY() >= obj.getPosition().getY()));
}
public boolean intersects(final RectI2 rect0, final RectI2 rect1) {
return ((rect0.getC1().getX() + 1 < rect0.getC0().getX() || rect0.getC1().getX() >= rect1.getC0().getX()) &&
(rect0.getC1().getY() + 1 < rect0.getC0().getY() || rect0.getC1().getY() >= rect1.getC0().getY()) &&
(rect1.getC1().getX() + 1 < rect1.getC0().getX() || rect1.getC1().getX() >= rect0.getC0().getX()) &&
(rect1.getC1().getY() + 1 < rect1.getC0().getY() || rect1.getC1().getY() >= rect0.getC0().getY()));
}
private static class Node<T extends SpatialIndex2Capable> {
int xMin;
int xMax;
int yMin;
int yMax;
List<T> leaves;
@SuppressWarnings("rawtypes")
Node[] branches;
Node<T> parent;
Node(final int xMin, final int yMin, final int size) {
this.xMin = xMin;
this.xMax = xMin + size - 1;
this.yMin = yMin;
this.yMax = yMin + size - 1;
}
Node(final int xMin, final int xMax, final int yMin, final int yMax, final Node<T> parent) {
this.xMin = xMin;
this.xMax = xMax;
this.yMin = yMin;
this.yMax = yMax;
this.parent = parent;
}
boolean createBranches() {
final int partition = (this.xMax - this.xMin + 1) >> 1;
if (partition == 0) {
return false;
}
this.branches = new Node[ORDER];
this.branches[0] = new Node<>(this.xMin, this.xMin + partition - 1, this.yMin, this.yMin + partition - 1, this);
this.branches[1] = new Node<>(this.xMin, this.xMin + partition - 1, this.yMin + partition, this.yMax, this);
this.branches[2] = new Node<>(this.xMin + partition, this.xMax, this.yMin, this.yMin + partition - 1, this);
this.branches[3] = new Node<>(this.xMin + partition, this.xMax, this.yMin + partition, this.yMax, this);
return true;
}
@Override
public String toString() {
final StringBuilder builder = new StringBuilder();
builder.append("Node [xMin=");
builder.append(this.xMin);
builder.append(", xMax=");
builder.append(this.xMax);
builder.append(", yMin=");
builder.append(this.yMin);
builder.append(", yMax=");
builder.append(this.yMax);
builder.append("]");
return builder.toString();
}
}
}

View File

@@ -0,0 +1,106 @@
/* Copyright (C) 2023 Stephan Birkl - All Rights Reserved.
* THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
* PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
* OR OTHER DEALINGS IN THE SOFTWARE.
*/
package de.extio.game_engine.spatial2.index;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
import java.util.function.Predicate;
import de.extio.game_engine.spatial2.model.CoordI2;
import de.extio.game_engine.spatial2.model.HasPositionAndDimension2;
import de.extio.game_engine.spatial2.model.ImmutableCoordI2;
import de.extio.game_engine.spatial2.model.RectI2;
import de.extio.game_engine.spatial2.model.SpatialIndex2Capable;
/**
* Spatial indexes are used for spatial queries. See also QuadTree
*/
public interface SpatialIndex2D<T extends SpatialIndex2Capable> extends Iterable<T>, AutoCloseable {
static AtomicLong SESSION = new AtomicLong(1);
/**
* Adds an object to the index
*/
void add(T obj);
/**
* Removes an object from the index
*/
void remove(T obj);
/**
* Moves an indexed object to a new location. Implementations are usually faster than doing separate remove() and add() operations
*/
void move(T obj, CoordI2 oldPosition, CoordI2 oldDimension);
/**
* Queries all object intersecting with a given area
*/
default Collection<T> find(final RectI2 rect) {
return this.find(rect.getC0(), ImmutableCoordI2.create(rect.getC1()).substract(rect.getC0()), null);
}
/**
* Queries all object intersecting with a given area
*/
default List<T> find(final HasPositionAndDimension2 hasPositionAndDimension2) {
return this.find(hasPositionAndDimension2.getPosition(), hasPositionAndDimension2.getDimension(), null);
}
/**
* Queries all object intersecting with a given area
*/
default List<T> find(final CoordI2 position, final CoordI2 dimension) {
return this.find(position, dimension, null);
}
/**
* Queries all object intersecting with a given area. Objects can be pre-filtered which is usually faster than returning them first and then filter in your own code.
*/
List<T> find(CoordI2 position, CoordI2 dimension, Predicate<T> filter);
/**
* Returns the first object at a given position
*/
default T findFirstAt(final CoordI2 position) {
return this.findFirstAt(position, null);
}
/**
* Returns the first object at a given position. Objects can be pre-filtered which is usually faster than returning them first and then filter in your own code.
*/
T findFirstAt(CoordI2 position, Predicate<T> filter);
/**
* Executes a consumer for all objects at a given position.
*/
default void executeAt(final CoordI2 position, final Consumer<T> consumer) {
this.executeAt(position, consumer, null);
}
/**
* Executes a consumer for all objects at a given position. Objects can be pre-filtered which is usually faster than returning them first and then filter in your own code.
*/
void executeAt(CoordI2 position, Consumer<T> consumer, Predicate<T> filter);
/**
* Clears and frees internal state
*/
@Override
void close();
/**
* Clears but keeps internal state allocated. Optimization for re-use.
*/
void clear();
}

View File

@@ -0,0 +1,92 @@
/* Copyright (C) 2023 Stephan Birkl - All Rights Reserved.
* THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
* PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
* OR OTHER DEALINGS IN THE SOFTWARE.
*/
package de.extio.game_engine.spatial2.model;
/**
* A 2D area, having a position (top left corner) and a dimension
*/
public class Area2 extends Point2 implements HasPositionAndDimension2, SpatialIndex2Capable {
private CoordI2 dimension;
private transient long bsi2Session;
public Area2() {
super();
}
public Area2(final CoordI2 coord, final CoordI2 dimension) {
super(coord);
this.setDimension(dimension);
}
public Area2(final HasPositionAndDimension2 other) {
super(ImmutableCoordI2.create(other.getPosition()));
this.setDimension(ImmutableCoordI2.create(other.getDimension()));
}
@Override
public CoordI2 getDimension() {
return this.dimension;
}
public void setDimension(final CoordI2 dimension) {
this.dimension = dimension;
}
@Override
public boolean isInSpatialIndex2ResultSet(final long session) {
return this.bsi2Session == session;
}
@Override
public void includeInSpatialIndex2ResultSet(final long session) {
this.bsi2Session = session;
}
@Override
public int hashCode() {
final int prime = 31;
int result = super.hashCode();
result = prime * result + ((this.dimension == null) ? 0 : this.dimension.hashCode());
return result;
}
@Override
public boolean equals(final Object obj) {
if (this == obj) {
return true;
}
if (!super.equals(obj)) {
return false;
}
final Area2 other = (Area2) obj;
if (this.dimension == null) {
if (other.dimension != null) {
return false;
}
}
else if (!this.dimension.equals(other.dimension)) {
return false;
}
return true;
}
@Override
public String toString() {
final StringBuilder builder = new StringBuilder();
builder.append("Area2 [getPosition()=");
builder.append(this.getPosition());
builder.append(", dimension=");
builder.append(this.dimension);
builder.append("]");
return builder.toString();
}
}

View File

@@ -0,0 +1,78 @@
/* Copyright (C) 2023 Stephan Birkl - All Rights Reserved.
* THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
* PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
* OR OTHER DEALINGS IN THE SOFTWARE.
*/
package de.extio.game_engine.spatial2.model;
import de.extio.game_engine.spatial2.WorldUtils2;
/**
* Helper to convert between mutable and immutable representations of tuples (coordinates and vectors)
*/
public interface Coord2Conversion {
default CoordI2 toMutableCoordI2() {
if (this instanceof CoordI2) {
return MutableCoordI2.create((CoordI2) this);
}
else {
return MutableCoordI2.create((CoordD2) this);
}
}
default CoordI2 toImmutableCoordI2() {
if (this instanceof CoordI2) {
return ImmutableCoordI2.create((CoordI2) this);
}
else {
return ImmutableCoordI2.create((CoordD2) this);
}
}
default CoordD2 toMutableCoordD2() {
if (this instanceof CoordI2) {
return MutableCoordD2.create((CoordI2) this);
}
else {
return MutableCoordD2.create((CoordD2) this);
}
}
default CoordD2 toImmutableCoordD2() {
if (this instanceof CoordI2) {
return ImmutableCoordD2.create((CoordI2) this);
}
else {
return ImmutableCoordD2.create((CoordD2) this);
}
}
/**
* Converts a vector to a unit normal vector
*/
default CoordD2 toVNorm() {
CoordD2 vNorm;
if (this instanceof ImmutableCoordI2) {
vNorm = ImmutableCoordD2.create((ImmutableCoordI2) this);
}
else if (this instanceof MutableCoordI2) {
vNorm = MutableCoordD2.create((MutableCoordI2) this);
}
else {
vNorm = (CoordD2) this;
}
final double length = WorldUtils2.getDistance(vNorm);
if (length > 0.0) {
vNorm = vNorm.divide(length);
}
return vNorm;
}
}

View File

@@ -0,0 +1,57 @@
/* Copyright (C) 2023 Stephan Birkl - All Rights Reserved.
* THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
* PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
* OR OTHER DEALINGS IN THE SOFTWARE.
*/
package de.extio.game_engine.spatial2.model;
/**
* A floating point number tuple (can represent a point, a coordinate or a vector in 2D space).
*/
public interface CoordD2 extends Comparable<CoordD2>, Coord2Conversion {
double getX();
void setX(final double x);
double getY();
void setY(final double y);
CoordD2 setXY(final CoordD2 other);
CoordD2 setXY(final double x, final double y);
CoordD2 add(final CoordD2 delta);
CoordD2 add(final double dx, final double dy);
CoordD2 add(double n);
CoordD2 substract(final CoordD2 delta);
CoordD2 substract(final double dx, final double dy);
CoordD2 substract(double n);
CoordD2 multiply(final CoordD2 delta);
CoordD2 multiply(final double dx, final double dy);
CoordD2 multiply(final double n);
CoordD2 divide(final CoordD2 delta);
CoordD2 divide(final double dx, final double dy);
CoordD2 divide(final double n);
@Override
default int compareTo(final CoordD2 other) {
return this.getY() == other.getY() ? Double.compare(this.getX(), other.getX()) : Double.compare(this.getY(), other.getY());
}
}

View File

@@ -0,0 +1,62 @@
/* Copyright (C) 2023 Stephan Birkl - All Rights Reserved.
* THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
* PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
* OR OTHER DEALINGS IN THE SOFTWARE.
*/
package de.extio.game_engine.spatial2.model;
/**
* An integer tuple (can represent a point, a coordinate or a vector in 2D space).
*/
public interface CoordI2 extends HasPosition2, Comparable<CoordI2>, Coord2Conversion {
int getX();
void setX(final int x);
int getY();
void setY(final int y);
CoordI2 setXY(final HasPosition2 other);
CoordI2 setXY(final int x, final int y);
CoordI2 add(final HasPosition2 delta);
CoordI2 add(final int dx, final int dy);
CoordI2 add(final int n);
CoordI2 substract(final HasPosition2 delta);
CoordI2 substract(final int dx, final int dy);
CoordI2 substract(final int n);
CoordI2 multiply(final HasPosition2 delta);
CoordI2 multiply(final int dx, final int dy);
CoordI2 multiply(final int n);
CoordI2 divide(final HasPosition2 delta);
CoordI2 divide(final int dx, final int dy);
CoordI2 divide(final int n);
@Override
default CoordI2 getPosition() {
return this;
}
@Override
default int compareTo(final CoordI2 other) {
return this.getY() == other.getY() ? Integer.compare(this.getX(), other.getX()) : Integer.compare(this.getY(), other.getY());
}
}

View File

@@ -0,0 +1,156 @@
/* Copyright (C) 2023 Stephan Birkl - All Rights Reserved.
* THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
* PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
* OR OTHER DEALINGS IN THE SOFTWARE.
*/
package de.extio.game_engine.spatial2.model;
/**
* Identifies a vertex or a side
*/
public enum Edge2 {
RIGHT(),
BOTTOM(),
LEFT(),
TOP();
public final static Edge2[] VALUES_CACHED = new Edge2[] {
RIGHT,
BOTTOM,
LEFT,
TOP
};
private final static CoordD2 VD_BOTTOM = MutableCoordD2.create(0.0, 1.0);
private final static CoordD2 VD_LEFT = MutableCoordD2.create(-1.0, 0.0);
private final static CoordD2 VD_RIGHT = MutableCoordD2.create(1.0, 0.0);
private final static CoordD2 VD_TOP = MutableCoordD2.create(0.0, -1.0);
private final static CoordI2 VI_BOTTOM = ImmutableCoordI2.create(0, 1);
private final static CoordI2 VI_LEFT = ImmutableCoordI2.create(-1, 0);
private final static CoordI2 VI_RIGHT = ImmutableCoordI2.create(1, 0);
private final static CoordI2 VI_TOP = ImmutableCoordI2.create(0, -1);
public static Edge2 fromBit(final int bit) {
if ((bit & RIGHT.bit) == RIGHT.bit) {
return RIGHT;
}
else if ((bit & BOTTOM.bit) == BOTTOM.bit) {
return BOTTOM;
}
else if ((bit & LEFT.bit) == LEFT.bit) {
return LEFT;
}
else if ((bit & TOP.bit) == TOP.bit) {
return TOP;
}
return null;
}
/**
* Rotates edge from RIGHT to destination clock-wise. BOTTOM rotates by 1 stop, LEFT by 2 and TOP by 3.
* @param dest stops
* @return Rotated edge2
*/
public Edge2 rotate(final Edge2 dest) {
return Edge2.fromBit(Edge2.rotate(this.getBit(), dest));
}
/**
* Rotates edges represented by bit mask (Edge2::getBit)
* @param edges Bit mask of edges
* @param dest Rotate from RIGHT to destination clock-wise. BOTTOM rotates by 1 stop, LEFT by 2 and TOP by 3.
* @return Rotated bit mask
*/
public static int rotate(final int edges, final Edge2 dest) {
// Formula:
// bits: Number of significant bits, in our case 4 (4 edges)
// shift: rotate by n steps
//
// ((x << shift) | (x >> (bits - shift))) & ((1 << bits) - 1)
// |----- Circular left shift ----| |-- Cut significant bits --|
if (dest == null) {
return edges;
}
return ((edges << Integer.numberOfTrailingZeros(dest.bit)) | (edges >> (Edge2.VALUES_CACHED.length - Integer.numberOfTrailingZeros(dest.bit)))) & ((1 << Edge2.VALUES_CACHED.length) - 1);
}
/**
* Returns a vector with length 1 pointing to the direction edge2 representates
* @param edge2
* @return Vector
*/
public static CoordD2 toVectorD(final Edge2 edge2) {
switch (edge2) {
case BOTTOM:
return VD_BOTTOM;
case LEFT:
return VD_LEFT;
case RIGHT:
return VD_RIGHT;
case TOP:
return VD_TOP;
}
return null;
}
/**
* Returns a vector with length 1 pointing to the direction edge2 representates
* @param edge2
* @return Vector
*/
public static CoordI2 toVectorI(final Edge2 edge2) {
switch (edge2) {
case BOTTOM:
return VI_BOTTOM;
case LEFT:
return VI_LEFT;
case RIGHT:
return VI_RIGHT;
case TOP:
return VI_TOP;
}
return null;
}
public static String toString(final int edges) {
final StringBuilder sb = new StringBuilder();
sb.append("Edges [");
for (final Edge2 edge : Edge2.VALUES_CACHED) {
if ((edges & (1 << edge.ordinal())) != 0) {
sb.append(edge.name());
sb.append(" ");
}
}
sb.append("]");
return sb.toString();
}
private int bit;
Edge2() {
this.bit = 1 << this.ordinal();
}
public int getBit() {
return this.bit;
}
}

View File

@@ -0,0 +1,18 @@
/* Copyright (C) 2023 Stephan Birkl - All Rights Reserved.
* THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
* PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
* OR OTHER DEALINGS IN THE SOFTWARE.
*/
package de.extio.game_engine.spatial2.model;
/**
* Interface for objects having a position (top left corner)
*/
public interface HasPosition2 {
CoordI2 getPosition();
}

View File

@@ -0,0 +1,18 @@
/* Copyright (C) 2023 Stephan Birkl - All Rights Reserved.
* THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
* PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
* OR OTHER DEALINGS IN THE SOFTWARE.
*/
package de.extio.game_engine.spatial2.model;
/**
* Interface for objects occupying 2D area, having a position (top left corner) and a dimension
*/
public interface HasPositionAndDimension2 extends HasPosition2 {
CoordI2 getDimension();
}

View File

@@ -0,0 +1,192 @@
/* Copyright (C) 2023 Stephan Birkl - All Rights Reserved.
* THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
* PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
* OR OTHER DEALINGS IN THE SOFTWARE.
*/
package de.extio.game_engine.spatial2.model;
/**
* Immutable implementation of CoordD2
*/
public final class ImmutableCoordD2 implements CoordD2 {
private final static ImmutableCoordD2 COORD_ZERO = new ImmutableCoordD2(0, 0);
private final static ImmutableCoordD2 COORD_ONE = new ImmutableCoordD2(1, 1);
public static ImmutableCoordD2 zero() {
return COORD_ZERO;
}
public static ImmutableCoordD2 one() {
return COORD_ONE;
}
public static ImmutableCoordD2 create() {
return COORD_ZERO;
}
public static ImmutableCoordD2 create(final CoordD2 other) {
if (other == null) {
return null;
}
if (other instanceof ImmutableCoordD2) {
return (ImmutableCoordD2) other;
}
return new ImmutableCoordD2(other.getX(), other.getY());
}
public static ImmutableCoordD2 create(final double x, final double y) {
return new ImmutableCoordD2(x, y);
}
public static ImmutableCoordD2 create(final HasPosition2 hasPosition2) {
return new ImmutableCoordD2(hasPosition2.getPosition().getX(), hasPosition2.getPosition().getY());
}
private double x;
private double y;
ImmutableCoordD2() {
}
ImmutableCoordD2(final double x, final double y) {
this.x = x;
this.y = y;
}
@Override
public double getX() {
return this.x;
}
@Override
public void setX(final double x) {
throw new UnsupportedOperationException();
}
@Override
public double getY() {
return this.y;
}
@Override
public void setY(final double y) {
throw new UnsupportedOperationException();
}
@Override
public ImmutableCoordD2 setXY(final CoordD2 other) {
return create(other);
}
@Override
public ImmutableCoordD2 setXY(final double x, final double y) {
return create(x, y);
}
@Override
public ImmutableCoordD2 add(final CoordD2 delta) {
return create(this.x + delta.getX(), this.y + delta.getY());
}
@Override
public ImmutableCoordD2 add(final double dx, final double dy) {
return create(this.x + dx, this.y + dy);
}
@Override
public ImmutableCoordD2 add(final double n) {
return create(this.x + n, this.y + n);
}
@Override
public ImmutableCoordD2 substract(final CoordD2 delta) {
return create(this.x - delta.getX(), this.y - delta.getY());
}
@Override
public ImmutableCoordD2 substract(final double dx, final double dy) {
return create(this.x - dx, this.y - dy);
}
@Override
public ImmutableCoordD2 substract(final double n) {
return create(this.x - n, this.y - n);
}
@Override
public ImmutableCoordD2 multiply(final CoordD2 delta) {
return create(this.x * delta.getX(), this.y * delta.getY());
}
@Override
public ImmutableCoordD2 multiply(final double dx, final double dy) {
return create(this.x * dx, this.y * dy);
}
@Override
public CoordD2 multiply(final double n) {
return create(this.x * n, this.y * n);
}
@Override
public ImmutableCoordD2 divide(final CoordD2 delta) {
final double dx = delta.getX() != 0.0 ? delta.getX() : 1.0;
final double dy = delta.getY() != 0.0 ? delta.getY() : 1.0;
return create(this.x / dx, this.y / dy);
}
@Override
public ImmutableCoordD2 divide(final double dx, final double dy) {
final double dx_ = dx != 0.0 ? dx : 1.0;
final double dy_ = dy != 0.0 ? dy : 1.0;
return create(this.x / dx_, this.y / dy_);
}
@Override
public CoordD2 divide(final double n) {
final double n_ = n != 0.0 ? n : 1.0;
return create(this.x / n_, this.y / n_);
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
long temp;
temp = Double.doubleToLongBits(this.x);
result = prime * result + (int) (temp ^ (temp >>> 32));
temp = Double.doubleToLongBits(this.y);
result = prime * result + (int) (temp ^ (temp >>> 32));
return result;
}
@Override
public boolean equals(final Object obj) {
if (this == obj) {
return true;
}
if ((obj == null) || !(obj instanceof CoordD2)) {
return false;
}
final CoordD2 other = (CoordD2) obj;
if (Double.doubleToLongBits(this.x) != Double.doubleToLongBits(other.getX())) {
return false;
}
if (Double.doubleToLongBits(this.y) != Double.doubleToLongBits(other.getY())) {
return false;
}
return true;
}
@Override
public String toString() {
return "D2i[" + this.x + ", " + this.y + "]";
}
}

View File

@@ -0,0 +1,220 @@
/* Copyright (C) 2023 Stephan Birkl - All Rights Reserved.
* THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
* PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
* OR OTHER DEALINGS IN THE SOFTWARE.
*/
package de.extio.game_engine.spatial2.model;
/**
* Immutable implementation of CoordI2
*/
public final class ImmutableCoordI2 implements CoordI2 {
private final static int COORD_CACHE_DIM = 100;
private final static int COORD_CACHE_BOUND = 50;
private final static int COORD_CACHE_SIZE = COORD_CACHE_DIM * COORD_CACHE_DIM;
private final static ImmutableCoordI2[] COORD_CACHE = new ImmutableCoordI2[COORD_CACHE_SIZE];
private volatile static ImmutableCoordI2 LAST_CACHED = new ImmutableCoordI2(0, 0);
private final static ImmutableCoordI2 COORD_ZERO = new ImmutableCoordI2(0, 0);
private final static ImmutableCoordI2 COORD_ONE = new ImmutableCoordI2(1, 1);
static {
int i = 0;
for (int x = -COORD_CACHE_BOUND; x < COORD_CACHE_BOUND; x++) {
for (int y = -COORD_CACHE_BOUND; y < COORD_CACHE_BOUND; y++) {
COORD_CACHE[i++] = new ImmutableCoordI2(x, y);
}
}
}
public static ImmutableCoordI2 zero() {
return COORD_ZERO;
}
public static ImmutableCoordI2 one() {
return COORD_ONE;
}
public static ImmutableCoordI2 create() {
return COORD_ZERO;
}
public static ImmutableCoordI2 create(final HasPosition2 other) {
if (other == null) {
return null;
}
if (other instanceof ImmutableCoordI2) {
return (ImmutableCoordI2) other;
}
return ImmutableCoordI2.create(other.getPosition().getX(), other.getPosition().getY());
}
public static ImmutableCoordI2 create(final int x, final int y) {
if (x >= -COORD_CACHE_BOUND && x < COORD_CACHE_BOUND && y >= -COORD_CACHE_BOUND && y < COORD_CACHE_BOUND) {
final int idx = (x + COORD_CACHE_BOUND) * COORD_CACHE_DIM + (y + COORD_CACHE_BOUND);
if (idx >= 0 && idx < COORD_CACHE_SIZE) {
return COORD_CACHE[idx];
}
}
ImmutableCoordI2 cached = LAST_CACHED;
if (cached.getX() != x || cached.getY() != y) {
cached = new ImmutableCoordI2(x, y);
LAST_CACHED = cached;
}
return cached;
}
public static ImmutableCoordI2 create(final CoordD2 other) {
return ImmutableCoordI2.create((int) other.getX(), (int) other.getY());
}
private int x;
private int y;
ImmutableCoordI2() {
}
ImmutableCoordI2(final int x, final int y) {
this.x = x;
this.y = y;
}
@Override
public int getX() {
return this.x;
}
@Override
public void setX(final int x) {
throw new UnsupportedOperationException();
}
@Override
public int getY() {
return this.y;
}
@Override
public void setY(final int y) {
throw new UnsupportedOperationException();
}
@Override
public ImmutableCoordI2 setXY(final HasPosition2 other) {
return create(other);
}
@Override
public ImmutableCoordI2 setXY(final int x, final int y) {
return create(x, y);
}
@Override
public ImmutableCoordI2 add(final HasPosition2 delta) {
return create(this.x + delta.getPosition().getX(), this.y + delta.getPosition().getY());
}
@Override
public ImmutableCoordI2 add(final int dx, final int dy) {
return create(this.x + dx, this.y + dy);
}
@Override
public ImmutableCoordI2 add(final int n) {
return create(this.x + n, this.y + n);
}
@Override
public ImmutableCoordI2 substract(final HasPosition2 delta) {
return create(this.x - delta.getPosition().getX(), this.y - delta.getPosition().getY());
}
@Override
public ImmutableCoordI2 substract(final int dx, final int dy) {
return create(this.x - dx, this.y - dy);
}
@Override
public ImmutableCoordI2 substract(final int n) {
return create(this.x - n, this.y - n);
}
@Override
public ImmutableCoordI2 multiply(final HasPosition2 delta) {
return create(this.x * delta.getPosition().getX(), this.y * delta.getPosition().getY());
}
@Override
public ImmutableCoordI2 multiply(final int dx, final int dy) {
return create(this.x * dx, this.y * dy);
}
@Override
public ImmutableCoordI2 multiply(final int n) {
return create(this.x * n, this.y * n);
}
@Override
public ImmutableCoordI2 divide(final HasPosition2 delta) {
final int dx = delta.getPosition().getX() != 0 ? delta.getPosition().getX() : 1;
final int dy = delta.getPosition().getY() != 0 ? delta.getPosition().getY() : 1;
return create(this.x / dx, this.y / dy);
}
@Override
public ImmutableCoordI2 divide(final int dx, final int dy) {
final int dx_ = dx != 0 ? dx : 1;
final int dy_ = dy != 0 ? dy : 1;
return create(this.x / dx_, this.y / dy_);
}
@Override
public ImmutableCoordI2 divide(final int n) {
final int n_ = n != 0 ? n : 1;
return create(this.x / n_, this.y / n_);
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + this.x;
result = prime * result + this.y;
return result;
}
@Override
public boolean equals(final Object obj) {
if (this == obj) {
return true;
}
if ((obj == null) || !(obj instanceof CoordI2)) {
return false;
}
final CoordI2 other = (CoordI2) obj;
if (this.x != other.getX()) {
return false;
}
if (this.y != other.getY()) {
return false;
}
return true;
}
@Override
public String toString() {
return "I2i[" + this.x + ", " + this.y + "]";
}
}

View File

@@ -0,0 +1,222 @@
/* Copyright (C) 2023 Stephan Birkl - All Rights Reserved.
* THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
* PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
* OR OTHER DEALINGS IN THE SOFTWARE.
*/
package de.extio.game_engine.spatial2.model;
/**
* Immutable implementation of CoordD2
*/
public final class MutableCoordD2 implements CoordD2 {
public static MutableCoordD2 create() {
return new MutableCoordD2(0.0, 0.0);
}
public static MutableCoordD2 create(final double x, final double y) {
return new MutableCoordD2(x, y);
}
public static MutableCoordD2 create(final CoordD2 other) {
return new MutableCoordD2(other.getX(), other.getY());
}
public static MutableCoordD2 create(final HasPosition2 hasPosition2) {
return new MutableCoordD2(hasPosition2.getPosition().getX(), hasPosition2.getPosition().getY());
}
private double x;
private double y;
MutableCoordD2() {
}
MutableCoordD2(final double x, final double y) {
this.x = x;
this.y = y;
}
@Override
public double getX() {
return this.x;
}
@Override
public void setX(final double x) {
this.x = x;
}
@Override
public double getY() {
return this.y;
}
@Override
public void setY(final double y) {
this.y = y;
}
@Override
public MutableCoordD2 setXY(final CoordD2 other) {
this.x = other.getX();
this.y = other.getY();
return this;
}
@Override
public MutableCoordD2 setXY(final double x, final double y) {
this.x = x;
this.y = y;
return this;
}
@Override
public MutableCoordD2 add(final CoordD2 delta) {
this.x += delta.getX();
this.y += delta.getY();
return this;
}
@Override
public MutableCoordD2 add(final double dx, final double dy) {
this.x += dx;
this.y += dy;
return this;
}
@Override
public MutableCoordD2 add(final double n) {
this.x += n;
this.y += n;
return this;
}
@Override
public MutableCoordD2 substract(final CoordD2 delta) {
this.x -= delta.getX();
this.y -= delta.getY();
return this;
}
@Override
public MutableCoordD2 substract(final double dx, final double dy) {
this.x -= dx;
this.y -= dy;
return this;
}
@Override
public MutableCoordD2 substract(final double n) {
this.x -= n;
this.y -= n;
return this;
}
@Override
public MutableCoordD2 multiply(final CoordD2 delta) {
this.x *= delta.getX();
this.y *= delta.getY();
return this;
}
@Override
public MutableCoordD2 multiply(final double dx, final double dy) {
this.x *= dx;
this.y *= dy;
return this;
}
@Override
public CoordD2 multiply(final double n) {
this.x *= n;
this.y *= n;
return this;
}
@Override
public MutableCoordD2 divide(final CoordD2 delta) {
if (delta.getX() != 0.0) {
this.x /= delta.getX();
}
if (delta.getY() != 0.0) {
this.y /= delta.getY();
}
return this;
}
@Override
public MutableCoordD2 divide(final double dx, final double dy) {
if (dx != 0.0) {
this.x /= dx;
}
if (dy != 0.0) {
this.y /= dy;
}
return this;
}
@Override
public CoordD2 divide(final double n) {
if (n != 0.0) {
this.x /= n;
this.y /= n;
}
return this;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
long temp;
temp = Double.doubleToLongBits(this.x);
result = prime * result + (int) (temp ^ (temp >>> 32));
temp = Double.doubleToLongBits(this.y);
result = prime * result + (int) (temp ^ (temp >>> 32));
return result;
}
@Override
public boolean equals(final Object obj) {
if (this == obj) {
return true;
}
if ((obj == null) || !(obj instanceof CoordD2)) {
return false;
}
final CoordD2 other = (CoordD2) obj;
if (Double.doubleToLongBits(this.x) != Double.doubleToLongBits(other.getX())) {
return false;
}
if (Double.doubleToLongBits(this.y) != Double.doubleToLongBits(other.getY())) {
return false;
}
return true;
}
@Override
public String toString() {
return "D2m[" + this.x + ", " + this.y + "]";
}
}

View File

@@ -0,0 +1,219 @@
/* Copyright (C) 2023 Stephan Birkl - All Rights Reserved.
* THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
* PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
* OR OTHER DEALINGS IN THE SOFTWARE.
*/
package de.extio.game_engine.spatial2.model;
/**
* Immutable implementation of CoordI2
*/
public final class MutableCoordI2 implements CoordI2 {
public static MutableCoordI2 create() {
return new MutableCoordI2(0, 0);
}
public static MutableCoordI2 create(final int x, final int y) {
return new MutableCoordI2(x, y);
}
public static MutableCoordI2 create(final HasPosition2 other) {
return new MutableCoordI2(other.getPosition().getX(), other.getPosition().getY());
}
public static MutableCoordI2 create(final CoordD2 other) {
return new MutableCoordI2((int) other.getX(), (int) other.getY());
}
private int x;
private int y;
MutableCoordI2() {
}
MutableCoordI2(final int x, final int y) {
this.x = x;
this.y = y;
}
@Override
public int getX() {
return this.x;
}
@Override
public void setX(final int x) {
this.x = x;
}
@Override
public int getY() {
return this.y;
}
@Override
public void setY(final int y) {
this.y = y;
}
@Override
public MutableCoordI2 setXY(final HasPosition2 other) {
this.x = other.getPosition().getX();
this.y = other.getPosition().getY();
return this;
}
@Override
public MutableCoordI2 setXY(final int x, final int y) {
this.x = x;
this.y = y;
return this;
}
@Override
public MutableCoordI2 add(final HasPosition2 delta) {
this.x += delta.getPosition().getX();
this.y += delta.getPosition().getY();
return this;
}
@Override
public MutableCoordI2 add(final int dx, final int dy) {
this.x += dx;
this.y += dy;
return this;
}
@Override
public MutableCoordI2 add(final int n) {
this.x += n;
this.y += n;
return this;
}
@Override
public MutableCoordI2 substract(final HasPosition2 delta) {
this.x -= delta.getPosition().getX();
this.y -= delta.getPosition().getY();
return this;
}
@Override
public MutableCoordI2 substract(final int dx, final int dy) {
this.x -= dx;
this.y -= dy;
return this;
}
@Override
public MutableCoordI2 substract(final int n) {
this.x -= n;
this.y -= n;
return this;
}
@Override
public MutableCoordI2 multiply(final HasPosition2 delta) {
this.x *= delta.getPosition().getX();
this.y *= delta.getPosition().getY();
return this;
}
@Override
public MutableCoordI2 multiply(final int dx, final int dy) {
this.x *= dx;
this.y *= dy;
return this;
}
@Override
public MutableCoordI2 multiply(final int n) {
this.x *= n;
this.y *= n;
return this;
}
@Override
public MutableCoordI2 divide(final HasPosition2 delta) {
if (delta.getPosition().getX() != 0) {
this.x /= delta.getPosition().getX();
}
if (delta.getPosition().getY() != 0) {
this.y /= delta.getPosition().getY();
}
return this;
}
@Override
public MutableCoordI2 divide(final int dx, final int dy) {
if (dx != 0) {
this.x /= dx;
}
if (dy != 0) {
this.y /= dy;
}
return this;
}
@Override
public MutableCoordI2 divide(final int n) {
if (n != 0) {
this.x /= n;
this.y /= n;
}
return this;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + this.x;
result = prime * result + this.y;
return result;
}
@Override
public boolean equals(final Object obj) {
if (this == obj) {
return true;
}
if ((obj == null) || !(obj instanceof CoordI2)) {
return false;
}
final CoordI2 other = (CoordI2) obj;
if (this.x != other.getX()) {
return false;
}
if (this.y != other.getY()) {
return false;
}
return true;
}
@Override
public String toString() {
return "I2m[" + this.x + ", " + this.y + "]";
}
}

View File

@@ -0,0 +1,72 @@
/* Copyright (C) 2023 Stephan Birkl - All Rights Reserved.
* THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
* PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
* OR OTHER DEALINGS IN THE SOFTWARE.
*/
package de.extio.game_engine.spatial2.model;
/**
* A point in 2D space
*/
public class Point2 implements HasPosition2 {
private CoordI2 coord;
public Point2() {
//
}
public Point2(final CoordI2 coord) {
this.coord = coord;
}
public void setPosition(final CoordI2 coord) {
this.coord = coord;
}
@Override
public CoordI2 getPosition() {
return this.coord;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((this.coord == null) ? 0 : this.coord.hashCode());
return result;
}
@Override
public boolean equals(final Object obj) {
if (this == obj) {
return true;
}
if ((obj == null) || (this.getClass() != obj.getClass())) {
return false;
}
final Point2 other = (Point2) obj;
if (this.coord == null) {
if (other.coord != null) {
return false;
}
}
else if (!this.coord.equals(other.coord)) {
return false;
}
return true;
}
@Override
public String toString() {
final StringBuilder builder = new StringBuilder();
builder.append("Point2 [coord=");
builder.append(this.coord);
builder.append("]");
return builder.toString();
}
}

View File

@@ -0,0 +1,109 @@
/* Copyright (C) 2023 Stephan Birkl - All Rights Reserved.
* THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
* PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
* OR OTHER DEALINGS IN THE SOFTWARE.
*/
package de.extio.game_engine.spatial2.model;
/**
* A rectangle represented by top left (c0) and bottom right (c1) vertices
*/
public class RectI2 {
private CoordI2 c0;
private CoordI2 c1;
public RectI2() {
}
public RectI2(final CoordI2 c0, final CoordI2 c1) {
this.c0 = c0;
this.c1 = c1;
}
public RectI2(final RectI2 other) {
if (other == null) {
return;
}
if (other.c0 != null) {
if (other.c0 instanceof ImmutableCoordI2) {
this.c0 = ImmutableCoordI2.create(other.c0);
}
else {
this.c0 = MutableCoordI2.create(other.c0);
}
}
if (other.c1 != null) {
if (other.c1 instanceof ImmutableCoordI2) {
this.c1 = ImmutableCoordI2.create(other.c1);
}
else {
this.c1 = MutableCoordI2.create(other.c1);
}
}
}
public CoordI2 getC0() {
return this.c0;
}
public void setC0(final CoordI2 c0) {
this.c0 = c0;
}
public CoordI2 getC1() {
return this.c1;
}
public void setC1(final CoordI2 c1) {
this.c1 = c1;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((this.c0 == null) ? 0 : this.c0.hashCode());
result = prime * result + ((this.c1 == null) ? 0 : this.c1.hashCode());
return result;
}
@Override
public boolean equals(final Object obj) {
if (this == obj) {
return true;
}
if ((obj == null) || (this.getClass() != obj.getClass())) {
return false;
}
final RectI2 other = (RectI2) obj;
if (this.c0 == null) {
if (other.c0 != null) {
return false;
}
}
else if (!this.c0.equals(other.c0)) {
return false;
}
if (this.c1 == null) {
if (other.c1 != null) {
return false;
}
}
else if (!this.c1.equals(other.c1)) {
return false;
}
return true;
}
@Override
public String toString() {
return "RectI2 [c0=" + this.c0 + ", c1=" + this.c1 + "]";
}
}

View File

@@ -0,0 +1,20 @@
/* Copyright (C) 2023 Stephan Birkl - All Rights Reserved.
* THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
* PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
* OR OTHER DEALINGS IN THE SOFTWARE.
*/
package de.extio.game_engine.spatial2.model;
/**
* Objects that can be indexed in SpatialIndex2D implementations
*/
public interface SpatialIndex2Capable extends HasPositionAndDimension2 {
boolean isInSpatialIndex2ResultSet(long session);
void includeInSpatialIndex2ResultSet(long session);
}