Improve tessellation algorithm
Added new mesh class Rewrite mesh algorithms to be less error prone Make code more concise Tried to add some documentation Added half edge set
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
package com.aaaaahhhhhhh.bananapuncher714.mesh;
|
||||
|
||||
import java.util.HashSet;
|
||||
|
||||
/**
|
||||
* A set for easily checking if a set contains/does not contain a half-edge or its sym.
|
||||
*
|
||||
* @author BananaPuncher714
|
||||
*/
|
||||
public class EdgeSet extends HashSet< HalfEdge > {
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private static final long serialVersionUID = 4042266887750818558L;
|
||||
|
||||
@Override
|
||||
public boolean add( HalfEdge edge ) {
|
||||
if ( !contains( edge ) ) {
|
||||
return super.add( edge );
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean remove( Object object ) {
|
||||
if ( object instanceof HalfEdge ) {
|
||||
HalfEdge edge = ( HalfEdge ) object;
|
||||
final boolean r1 = super.remove( edge );
|
||||
final boolean r2 = super.remove( edge.getSym() );
|
||||
|
||||
if ( r1 && r2 ) {
|
||||
throw new IllegalStateException( "Removed edge was added twice!" );
|
||||
}
|
||||
|
||||
return r1 || r2;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contains( Object object ) {
|
||||
if ( object instanceof HalfEdge ) {
|
||||
HalfEdge edge = ( HalfEdge ) object;
|
||||
return super.contains( edge ) || super.contains( edge.getSym() );
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
package com.aaaaahhhhhhh.bananapuncher714.mesh;
|
||||
|
||||
/**
|
||||
* A half-edge class used to represent a DCEL(doubly connected edge list): https://en.wikipedia.org/wiki/Doubly_connected_edge_list
|
||||
*
|
||||
* In this case, it is intended to be used for a 2d PSLG(planar straight-line graph), but can probably be repurposed
|
||||
* for some other nefarious use case.
|
||||
*
|
||||
* An edge is a line segment connecting an origin to a destination. A half-edge is a partial line segment containing only
|
||||
* the origin, but not the destination.
|
||||
*
|
||||
* Each half-edge structure contains:
|
||||
* - A reference to the origin
|
||||
* - A reference to the symmetrical, or opposite half-edge which is attached to the destination.
|
||||
* - A reference to the next clockwise half-edge of the opposite half edge. This can be used to
|
||||
* traverse counter-clockwise through the half-edges of a polygon.
|
||||
* - A reference to the previous edge, which is the next counter-clockwise edge of this half edge.
|
||||
* This can be used to traverse all the edges of a vertex, in counter clockwise order.
|
||||
*
|
||||
* For a given half-edge attached to two points that contain no other edges, the previous edge is
|
||||
* itself, and the next edge is the same as the opposite edge.
|
||||
*
|
||||
* There are 2 main operations that are used when manipulating half-edges to attach/detach them from
|
||||
* a vertex:
|
||||
* - splicing
|
||||
* - splitting
|
||||
*
|
||||
* Splicing is the most important of the 2 operations, by a large margin. Given two half-edges, A and B,
|
||||
* swap the previous edges, and set the next edge for both of the previous edges to the opposite half edge.
|
||||
* This has the desired effect of either detaching two edges if they are connected to the same vertex, or
|
||||
* attaching two separate half-edges together to the same vertex.
|
||||
*
|
||||
* Splitting is adding an edge between the opposite of an edge and attaching it to the destination. It
|
||||
* splits a single edge into two edges with a vertex in the middle.
|
||||
*
|
||||
* @author BananaPuncher714
|
||||
*/
|
||||
public class HalfEdge {
|
||||
/**
|
||||
* The origin
|
||||
*/
|
||||
protected Vertex origin;
|
||||
|
||||
/**
|
||||
* The symmetrical half-edge
|
||||
*/
|
||||
protected HalfEdge sym;
|
||||
/**
|
||||
* The half-edge counter-clockwise on the same origin
|
||||
*/
|
||||
protected HalfEdge prev;
|
||||
/**
|
||||
* The half-edge clockwise from the symmetrical half-edge
|
||||
*/
|
||||
protected HalfEdge next;
|
||||
|
||||
// TODO Consider adding a constructor with a origin and destination
|
||||
|
||||
public HalfEdge() {
|
||||
init();
|
||||
|
||||
// Whenever we create a new half-edge, we need to make
|
||||
// sure that it has a symmetrical half-edge.
|
||||
new HalfEdge( this );
|
||||
}
|
||||
|
||||
private HalfEdge( HalfEdge o ) {
|
||||
init();
|
||||
|
||||
sym = o;
|
||||
o.sym = this;
|
||||
|
||||
next = o;
|
||||
o.next = this;
|
||||
}
|
||||
|
||||
private void init() {
|
||||
origin = new Vertex( this );
|
||||
prev = this;
|
||||
}
|
||||
|
||||
public Vertex getOrigin() {
|
||||
return origin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the origin. Does not affect any additional
|
||||
*
|
||||
* @param vert
|
||||
* @return
|
||||
*/
|
||||
public HalfEdge setOrigin( Vertex vert ) {
|
||||
origin = vert;
|
||||
return this;
|
||||
}
|
||||
|
||||
public HalfEdge getSym() {
|
||||
return sym;
|
||||
}
|
||||
|
||||
public HalfEdge getPrev() {
|
||||
return prev;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the previous half-edge. Does not update additional properties.
|
||||
*
|
||||
* @param edge The half-edge to set as the previous half-edge.
|
||||
* @return This half-edge
|
||||
*/
|
||||
public HalfEdge setPrev( HalfEdge edge ) {
|
||||
this.prev = edge;
|
||||
return this;
|
||||
}
|
||||
|
||||
public HalfEdge getNext() {
|
||||
return next;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the next half-edge. Does not update additional properties.
|
||||
*
|
||||
* @param edge The half-edge to set as the next half-edge.
|
||||
* @return This half-edge
|
||||
*/
|
||||
public HalfEdge setNext( HalfEdge edge ) {
|
||||
this.next = edge;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Vertex getDest() {
|
||||
return sym.origin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the difference between the destination position and the origin position
|
||||
*
|
||||
* @return A vector representing the destination minus the origin
|
||||
*/
|
||||
public Vector2d toVector2d() {
|
||||
return getDest().getPosition().subtracted( getOrigin().getPosition() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the origin and destination are the same
|
||||
*
|
||||
* @return If the origin is equal to the destination
|
||||
*/
|
||||
public boolean isZero() {
|
||||
return getOrigin().equals( getDest() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Split this half-edge in half and return the newly created
|
||||
* half-edge, as the next half-edge of this half-edge.
|
||||
*
|
||||
* @return A new half-edge with a default initialized origin.
|
||||
*/
|
||||
public HalfEdge split() {
|
||||
final HalfEdge edge = new HalfEdge();
|
||||
|
||||
splice( edge, getNext() );
|
||||
|
||||
splice( getSym(), getNext() );
|
||||
splice( getSym(), edge.getSym() );
|
||||
|
||||
edge.setOrigin( getDest() );
|
||||
edge.getOrigin().setEdge( edge );
|
||||
|
||||
getSym().setOrigin( edge.getDest() );
|
||||
getDest().setEdge( getSym() );
|
||||
|
||||
edge.getOrigin().setEdge( edge );
|
||||
|
||||
return edge.getSym();
|
||||
}
|
||||
|
||||
/**
|
||||
* Splice two half-edges together. A commutative operation.
|
||||
*
|
||||
* This does _not_ modify any vertices.
|
||||
*
|
||||
* @param a The first half-edge
|
||||
* @param b The second half-edge
|
||||
*/
|
||||
public static void splice( HalfEdge a, HalfEdge b ) {
|
||||
final HalfEdge ap = a.prev;
|
||||
final HalfEdge bp = b.prev;
|
||||
|
||||
a.prev = bp;
|
||||
b.prev = ap;
|
||||
|
||||
ap.sym.next = b;
|
||||
bp.sym.next = a;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format( "HalfEdge{orig=%1$s,dest=%2$s}", getOrigin(), getDest() );
|
||||
}
|
||||
}
|
||||
1324
src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/Mesh.java
Normal file
1324
src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/Mesh.java
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,66 @@
|
||||
package com.aaaaahhhhhhh.bananapuncher714.mesh;
|
||||
|
||||
/**
|
||||
* A 2d cartesian coordinate
|
||||
*
|
||||
* @author BananaPuncher714
|
||||
*/
|
||||
public class Point {
|
||||
protected double x, y;
|
||||
|
||||
public Point( double x, double y ) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
public double getX() {
|
||||
return x;
|
||||
}
|
||||
|
||||
public double getY() {
|
||||
return y;
|
||||
}
|
||||
|
||||
public Point setX( double x ) {
|
||||
this.x = x;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Point setY( double y ) {
|
||||
this.y = y;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
long temp;
|
||||
temp = Double.doubleToLongBits(x);
|
||||
result = prime * result + (int) (temp ^ (temp >>> 32));
|
||||
temp = Double.doubleToLongBits(y);
|
||||
result = prime * result + (int) (temp ^ (temp >>> 32));
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj)
|
||||
return true;
|
||||
if (obj == null)
|
||||
return false;
|
||||
if (getClass() != obj.getClass())
|
||||
return false;
|
||||
Point other = (Point) obj;
|
||||
if (Double.doubleToLongBits(x) != Double.doubleToLongBits(other.x))
|
||||
return false;
|
||||
if (Double.doubleToLongBits(y) != Double.doubleToLongBits(other.y))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format( "Point{x=%f,y=%f}", x, y );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.aaaaahhhhhhh.bananapuncher714.mesh;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A list of points denoting the shape of the polygon. May contain self intersections,
|
||||
* duplicate vertices, and overlapping edges.
|
||||
*
|
||||
* @author BananaPuncher714
|
||||
*/
|
||||
public class Polygon {
|
||||
protected List< Point > points;
|
||||
|
||||
public Polygon( List< Point > points ) {
|
||||
this.points = points;
|
||||
}
|
||||
|
||||
public List< Point > getPoints() {
|
||||
return points;
|
||||
}
|
||||
|
||||
public double area() {
|
||||
double area = 0;
|
||||
for ( int i = 0; i < points.size(); ++i ) {
|
||||
final Vector2d a = new Vector2d( points.get( i ) );
|
||||
final Vector2d b = new Vector2d( points.get( ( i + 1 ) % points.size() ) );
|
||||
area += a.cross( b );
|
||||
}
|
||||
return Math.abs( area ) / 2.0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package com.aaaaahhhhhhh.bananapuncher714.mesh;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
public class SortedEdgeCollection< T > implements Iterable< T > {
|
||||
Map< T, Node > quickMap = new ConcurrentHashMap< T, Node >();
|
||||
Node head = new Node( null );
|
||||
|
||||
BiFunction< T, T, Boolean > isGreaterThan;
|
||||
|
||||
public SortedEdgeCollection( BiFunction< T, T, Boolean > compare ) {
|
||||
isGreaterThan = compare;
|
||||
}
|
||||
|
||||
public T bottom() {
|
||||
return head.upper == head ? null : head.upper.value;
|
||||
}
|
||||
|
||||
public T upper( T val ) {
|
||||
Node n = quickMap.get( val );
|
||||
return n == null ? null : n.upper.value;
|
||||
}
|
||||
|
||||
public T lower( T val ) {
|
||||
Node n = quickMap.get( val );
|
||||
return n == null ? null : n.lower.value;
|
||||
}
|
||||
|
||||
public boolean remove( T val ) {
|
||||
Node n = quickMap.remove( val );
|
||||
if ( n != null ) {
|
||||
n.lower.upper = n.upper;
|
||||
n.upper.lower = n.lower;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean contains( T val ) {
|
||||
return quickMap.containsKey( val );
|
||||
}
|
||||
|
||||
public void insert( T val ) {
|
||||
if ( !quickMap.containsKey( val ) ) {
|
||||
Node insertBefore = head.upper;
|
||||
// Find the first node that the value is NOT greater than
|
||||
// Otherwise, break and insert before
|
||||
while ( insertBefore.value != null && isGreaterThan.apply( val, insertBefore.value ) ) {
|
||||
insertBefore = insertBefore.upper;
|
||||
}
|
||||
|
||||
Node newNode = new Node( val );
|
||||
|
||||
newNode.lower = insertBefore.lower;
|
||||
newNode.lower.upper = newNode;
|
||||
newNode.upper = insertBefore;
|
||||
|
||||
insertBefore.lower = newNode;
|
||||
|
||||
quickMap.put( val, newNode );
|
||||
} else {
|
||||
// Should not really happen
|
||||
throw new IllegalStateException( "Tried to insert value twice" );
|
||||
}
|
||||
}
|
||||
|
||||
// Find the first value that is greater than the one provided
|
||||
public T searchUpper( T val ) {
|
||||
Node n = quickMap.get( val );
|
||||
if ( n == null ) {
|
||||
Node greater = head.upper;
|
||||
while ( greater.value != null && isGreaterThan.apply( val, greater.value ) ) {
|
||||
greater = greater.upper;
|
||||
}
|
||||
return greater.value;
|
||||
} else {
|
||||
return n.upper.value;
|
||||
}
|
||||
}
|
||||
|
||||
// Find the highest value that is lower than the one provided
|
||||
public T searchLower( T val ) {
|
||||
Node n = quickMap.get( val );
|
||||
if ( n == null ) {
|
||||
Node lower = head.upper;
|
||||
while ( lower.value != null && isGreaterThan.apply( val, lower.value ) ) {
|
||||
lower = lower.upper;
|
||||
}
|
||||
return lower.lower.value;
|
||||
} else {
|
||||
return n.lower.value;
|
||||
}
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
head = new Node( null );
|
||||
quickMap.clear();
|
||||
}
|
||||
|
||||
protected class Node {
|
||||
Node lower;
|
||||
Node upper;
|
||||
T value;
|
||||
|
||||
Node( T val ) {
|
||||
lower = this;
|
||||
upper = this;
|
||||
|
||||
this.value = val;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator< T > iterator() {
|
||||
return new Iterator< T >() {
|
||||
Node current = head;
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return current.upper != head;
|
||||
}
|
||||
|
||||
@Override
|
||||
public T next() {
|
||||
current = current.upper;
|
||||
return current.value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove() {
|
||||
T val = current.value;
|
||||
current = current.lower;
|
||||
SortedEdgeCollection.this.remove( val );
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.aaaaahhhhhhh.bananapuncher714.mesh;
|
||||
|
||||
public class Test {
|
||||
public static void main( String[] args ) {
|
||||
final Vector2d a = new Vector2d( -1, -1 );
|
||||
final Vector2d b = new Vector2d( 10, 10 );
|
||||
final Vector2d c = new Vector2d( 0, -1 );
|
||||
final Vector2d d = new Vector2d( 10, 11 );
|
||||
|
||||
test( a, b, c, d );
|
||||
}
|
||||
|
||||
public static void test( final Vector2d a, final Vector2d b, final Vector2d c, final Vector2d d ) {
|
||||
final Vector2d ab = b.subtracted( a );
|
||||
final Vector2d cd = d.subtracted( c );
|
||||
|
||||
final Vector2d ac = c.subtracted( a );
|
||||
|
||||
final double acab = ac.cross( ab );
|
||||
final double abcd = ab.cross( cd );
|
||||
|
||||
System.out.println( "Parallel value: " + abcd );
|
||||
if ( abcd != 0 ) {
|
||||
final double s = ac.cross( cd );
|
||||
final double t = s / abcd;
|
||||
final double u = acab / abcd;
|
||||
|
||||
System.out.println( "S: " + s );
|
||||
System.out.println( "T: " + t );
|
||||
System.out.println( "U: " + u );
|
||||
|
||||
System.out.println( "AB Length: " + ( t * ab.length() ) );
|
||||
System.out.println( "CD Length: " + ( u * cd.length() ) );
|
||||
|
||||
// Gives the intersection correctly
|
||||
System.out.println( "AB int: " + ab.multiplied( t ).add( a ) );
|
||||
System.out.println( "CD int: " + cd.multiplied( u ).add( c ) );
|
||||
} else {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
package com.aaaaahhhhhhh.bananapuncher714.mesh;
|
||||
|
||||
/**
|
||||
* A basic 2d vector implementation.
|
||||
*
|
||||
* @author BananaPuncher714
|
||||
*/
|
||||
public class Vector2d extends Point {
|
||||
public Vector2d() {
|
||||
this( 0, 0 );
|
||||
}
|
||||
|
||||
public Vector2d( double x, double y ) {
|
||||
super( x, y );
|
||||
}
|
||||
|
||||
public Vector2d( Point o ) {
|
||||
this( o.x, o.y );
|
||||
}
|
||||
|
||||
public double angle( Point o ) {
|
||||
return Math.atan2( cross( o ), dot( o ) );
|
||||
}
|
||||
|
||||
public double cross( Point o ) {
|
||||
return ( x * o.y ) - ( y * o.x );
|
||||
}
|
||||
|
||||
public double lengthSquared() {
|
||||
return dot( this );
|
||||
}
|
||||
|
||||
public double length() {
|
||||
return Math.sqrt( lengthSquared() );
|
||||
}
|
||||
|
||||
public double distanceSquared( Point o ) {
|
||||
return distanceSquared( this, o );
|
||||
}
|
||||
|
||||
public double distance( Point o ) {
|
||||
return distance( this, o );
|
||||
}
|
||||
|
||||
public double dot( Point o ) {
|
||||
return dot( this, o );
|
||||
}
|
||||
|
||||
public Vector2d add( double v ) {
|
||||
x += v;
|
||||
y += v;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Vector2d added( double v ) {
|
||||
return new Vector2d( x + v, y + v );
|
||||
}
|
||||
|
||||
public Vector2d add( Point o ) {
|
||||
x += o.x;
|
||||
y += o.y;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Vector2d added( Point o ) {
|
||||
return add( this, o );
|
||||
}
|
||||
|
||||
public Vector2d subtract( double v ) {
|
||||
x -= v;
|
||||
y -= v;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Vector2d subtracted( double v ) {
|
||||
return new Vector2d( x - v, y - v );
|
||||
}
|
||||
|
||||
public Vector2d subtract( Point o ) {
|
||||
x -= o.x;
|
||||
y -= o.y;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Vector2d subtracted( Point o ) {
|
||||
return new Vector2d( x - o.x, y - o.y );
|
||||
}
|
||||
|
||||
public Vector2d multiply( double v ) {
|
||||
x *= v;
|
||||
y *= v;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Vector2d multiplied( double v ) {
|
||||
return new Vector2d( x * v, y * v );
|
||||
}
|
||||
|
||||
public Vector2d multiply( Point o ) {
|
||||
x *= o.x;
|
||||
y *= o.y;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Vector2d multiplied( Point o ) {
|
||||
return multiply( this, o );
|
||||
}
|
||||
|
||||
public Vector2d divide( double v ) {
|
||||
x /= v;
|
||||
y /= v;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Vector2d divided( double v ) {
|
||||
return new Vector2d( x / v, y / v );
|
||||
}
|
||||
|
||||
public Vector2d divide( Point o ) {
|
||||
x /= o.x;
|
||||
y /= o.y;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Vector2d divided( Point o ) {
|
||||
return new Vector2d( x / o.x, y / o.y );
|
||||
}
|
||||
|
||||
public Vector2d normalize() {
|
||||
return divide( length() );
|
||||
}
|
||||
|
||||
public Vector2d normalized() {
|
||||
return divided( length() );
|
||||
}
|
||||
|
||||
public boolean isZero() {
|
||||
return x == 0 && y == 0;
|
||||
}
|
||||
|
||||
public static double dot( Point a, Point b ) {
|
||||
return a.x * b.x + a.y * b.y;
|
||||
}
|
||||
|
||||
public static Vector2d add( Point a, Point b ) {
|
||||
return new Vector2d( a.x + b.x, a.y + b.y );
|
||||
}
|
||||
|
||||
public static Vector2d multiply( Point a, Point b ) {
|
||||
return new Vector2d( a.x * b.x, a.y * b.y );
|
||||
}
|
||||
|
||||
public static double distanceSquared( Vector2d a, Point b ) {
|
||||
return a.subtracted( b ).lengthSquared();
|
||||
}
|
||||
|
||||
public static double distance( Vector2d a, Point b ) {
|
||||
return Math.sqrt( distanceSquared( a, b ) );
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
long temp;
|
||||
temp = Double.doubleToLongBits(x);
|
||||
result = prime * result + (int) (temp ^ (temp >>> 32));
|
||||
temp = Double.doubleToLongBits(y);
|
||||
result = prime * result + (int) (temp ^ (temp >>> 32));
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj)
|
||||
return true;
|
||||
if (obj == null)
|
||||
return false;
|
||||
if (getClass() != obj.getClass())
|
||||
return false;
|
||||
Vector2d other = (Vector2d) obj;
|
||||
if (Double.doubleToLongBits(x) != Double.doubleToLongBits(other.x))
|
||||
return false;
|
||||
if (Double.doubleToLongBits(y) != Double.doubleToLongBits(other.y))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format( "Vector2d{x=%f,y=%f}", x, y );
|
||||
}
|
||||
}
|
||||
121
src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/Vertex.java
Normal file
121
src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/Vertex.java
Normal file
@@ -0,0 +1,121 @@
|
||||
package com.aaaaahhhhhhh.bananapuncher714.mesh;
|
||||
|
||||
import java.util.Iterator;
|
||||
|
||||
/**
|
||||
* The vertex class is used to represent a point in space. For our use cases, it is going to be 2d.
|
||||
*
|
||||
* A vertex contains two pieces of information:
|
||||
* - The position
|
||||
* - A half-edge which is attached to this vertex
|
||||
*
|
||||
* To traverse over all the edges which are connected to this vertex,
|
||||
* loop over the previous edge of the half edge until all of the edges
|
||||
* have been reached at least once.
|
||||
*
|
||||
* Whenever performing a splicing operation on an edge, it is a good
|
||||
* idea to update all edges attached to this vertex to point to this
|
||||
* vertex, in case if they are pointing at a different one.
|
||||
*
|
||||
* @author BananaPuncher714
|
||||
*/
|
||||
public class Vertex implements Iterable< HalfEdge > {
|
||||
protected HalfEdge edge;
|
||||
protected Vector2d position;
|
||||
|
||||
public Vertex( final HalfEdge edge ) {
|
||||
this( edge, new Vector2d() );
|
||||
}
|
||||
|
||||
public Vertex( final HalfEdge edge, final Vector2d position ) {
|
||||
setEdge( edge );
|
||||
setPosition( position );
|
||||
}
|
||||
|
||||
public HalfEdge getEdge() {
|
||||
return edge;
|
||||
}
|
||||
|
||||
public Vertex setEdge( HalfEdge edge ) {
|
||||
this.edge = edge;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Vector2d getPosition() {
|
||||
return position;
|
||||
}
|
||||
|
||||
public Vertex setPosition( Vector2d vec ) {
|
||||
this.position = vec;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* O(n)
|
||||
*/
|
||||
public int size() {
|
||||
int size = 0;
|
||||
for ( final Iterator< HalfEdge > it = iterator(); it.hasNext(); it.next() ) {
|
||||
++size;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update all attached half-edges to point to this vertex
|
||||
* as the origin
|
||||
*/
|
||||
public void update() {
|
||||
HalfEdge edge = this.edge;
|
||||
do {
|
||||
edge.setOrigin( this );
|
||||
} while ( ( edge = edge.getPrev() ) != this.edge );
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator< HalfEdge > iterator() {
|
||||
return new Iterator< HalfEdge >() {
|
||||
private final HalfEdge start = edge;
|
||||
private HalfEdge index = null;
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return index != start;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HalfEdge next() {
|
||||
if ( index == null ) {
|
||||
index = start.getPrev();
|
||||
return start;
|
||||
} else {
|
||||
final HalfEdge curr = index;
|
||||
index = index.getPrev();
|
||||
return curr;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj)
|
||||
return true;
|
||||
if (obj == null)
|
||||
return false;
|
||||
if (getClass() != obj.getClass())
|
||||
return false;
|
||||
Vertex other = (Vertex) obj;
|
||||
if (position == null) {
|
||||
if (other.position != null)
|
||||
return false;
|
||||
} else if (!position.equals(other.position))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Vertex{x=" + position.getX() + ",y=" + position.getY() + "}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.aaaaahhhhhhh.bananapuncher714.mesh.region;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.ListIterator;
|
||||
|
||||
public class CompositeRegionRule< T extends Region > implements RegionRule< T > {
|
||||
final protected List< RegionRule< T > > rules;
|
||||
|
||||
public CompositeRegionRule() {
|
||||
this.rules = new ArrayList< RegionRule< T > >();
|
||||
}
|
||||
|
||||
public CompositeRegionRule( final List< RegionRule< T > > rules ) {
|
||||
this.rules = new ArrayList< RegionRule< T > >( rules );
|
||||
}
|
||||
|
||||
public CompositeRegionRule< T > addRule( final RegionRule< T > rule ) {
|
||||
rules.add( rule );
|
||||
return this;
|
||||
}
|
||||
|
||||
public List< RegionRule< T > > getRules() {
|
||||
return rules;
|
||||
}
|
||||
|
||||
@Override
|
||||
public T apply( T region ) {
|
||||
for ( final RegionRule< T > rule : rules ) {
|
||||
region = rule.apply( region );
|
||||
}
|
||||
return region;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RegionRule< T > inverse() {
|
||||
CompositeRegionRule< T > rule = new CompositeRegionRule< T >();
|
||||
for ( ListIterator< RegionRule< T > > it = rules.listIterator(); it.hasPrevious(); ) {
|
||||
rule.rules.add( it.previous().inverse() );
|
||||
}
|
||||
return rule;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RegionRule< T > apply( RegionRule< T > rule ) {
|
||||
final CompositeRegionRule< T > copy = new CompositeRegionRule< T >( rules );
|
||||
if ( rule instanceof CompositeRegionRule ) {
|
||||
final CompositeRegionRule< T > other = ( CompositeRegionRule< T > ) rule;
|
||||
for ( RegionRule< T > r : other.getRules() ) {
|
||||
copy.addRule( r );
|
||||
}
|
||||
} else {
|
||||
copy.addRule( rule );
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.aaaaahhhhhhh.bananapuncher714.mesh.region;
|
||||
|
||||
/**
|
||||
* A continuous portion in space that can be clearly defined as "inside" or "outside".
|
||||
*
|
||||
* The equality check is mainly used to guarantee winding rule consistency.
|
||||
*
|
||||
* @author BananaPuncher714
|
||||
*/
|
||||
public abstract class Region {
|
||||
// Represents the area under a wing
|
||||
public abstract boolean isInterior();
|
||||
public abstract boolean equals( Object other );
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.aaaaahhhhhhh.bananapuncher714.mesh.region;
|
||||
|
||||
/**
|
||||
* A region rule describes how a region is modified, particularly when crossing a border
|
||||
* between a known region to an unknown region. The inverse region rule must provide a
|
||||
* symmetric change such that applying the region rule then the inverse, the resulting region
|
||||
* must be equivalent to the original region. Normally, the same rule is applied to all polygons
|
||||
* that need to be tessellated, such as the odd number rule, but if your polygons conform to different or
|
||||
* more complex interior/exterior critera, then you can easily set different rules per polygon.
|
||||
* An example might be if you have two sets of polygons that you would like to tessellate
|
||||
* together, and distinctly mark regions where they might overlap. You can use a combination
|
||||
* of two odd number rules to properly mark the boundaries.
|
||||
*
|
||||
* For more information:
|
||||
* - https://en.wikipedia.org/wiki/Nonzero-rule
|
||||
*
|
||||
* @author BananaPuncher714
|
||||
*
|
||||
* @param <T> A region type that can be modified by this rule
|
||||
*/
|
||||
public interface RegionRule< T extends Region > {
|
||||
// Apply this rule to the region, and return a new region
|
||||
T apply( T region );
|
||||
// Get the inverse of this rule
|
||||
RegionRule< T > inverse();
|
||||
// Apply this rule to the given rule, and return a new rule
|
||||
RegionRule< T > apply( RegionRule< T > rule );
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.aaaaahhhhhhh.bananapuncher714.mesh.region.simple;
|
||||
|
||||
import com.aaaaahhhhhhh.bananapuncher714.mesh.region.CompositeRegionRule;
|
||||
import com.aaaaahhhhhhh.bananapuncher714.mesh.region.RegionRule;
|
||||
|
||||
/**
|
||||
* A simple winding number rule.
|
||||
*
|
||||
* Changes the winding number for a given region.
|
||||
*
|
||||
* @author BananaPuncher714
|
||||
*
|
||||
*/
|
||||
public class RegionRuleWinding implements RegionRule< RegionSimple > {
|
||||
public static final RegionRuleWinding CLOCKWISE = new RegionRuleWinding( 1 );
|
||||
public static final RegionRuleWinding COUNTER_CLOCKWISE = CLOCKWISE.inverse();
|
||||
|
||||
private final int amount;
|
||||
private RegionRuleWinding inverse;
|
||||
|
||||
private RegionRuleWinding( final int amount ) {
|
||||
this.amount = amount;
|
||||
}
|
||||
|
||||
public int getAmount() {
|
||||
return amount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RegionSimple apply( final RegionSimple region ) {
|
||||
RegionSimple copy = new RegionSimple( region );
|
||||
copy.increment( amount );
|
||||
return copy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RegionRuleWinding inverse() {
|
||||
if ( inverse == null ) {
|
||||
inverse = new RegionRuleWinding( -amount );
|
||||
inverse.inverse = this;
|
||||
}
|
||||
return inverse;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RegionRule< RegionSimple > apply( final RegionRule< RegionSimple > rule ) {
|
||||
if ( rule instanceof RegionRuleWinding ) {
|
||||
final RegionRuleWinding other = ( RegionRuleWinding ) rule;
|
||||
return new RegionRuleWinding( amount + other.amount );
|
||||
} else {
|
||||
final CompositeRegionRule< RegionSimple > composite = new CompositeRegionRule< RegionSimple >();
|
||||
composite.addRule( this ).addRule( rule );
|
||||
return composite;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format( "RegionRuleWinding{amount=%1$d}", amount );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.aaaaahhhhhhh.bananapuncher714.mesh.region.simple;
|
||||
|
||||
import com.aaaaahhhhhhh.bananapuncher714.mesh.region.Region;
|
||||
|
||||
/**
|
||||
* A region with a winding number. The interior-ness of
|
||||
* this region is determined by the GLU winding rule:
|
||||
*
|
||||
* https://www.songho.ca/opengl/gl_tessellation.html#winding
|
||||
*
|
||||
* @author BananaPuncher714
|
||||
*/
|
||||
public class RegionSimple extends Region {
|
||||
protected int windingNumber;
|
||||
protected GluWindingRule rule;
|
||||
|
||||
public RegionSimple( RegionSimple o ) {
|
||||
windingNumber = o.windingNumber;
|
||||
rule = o.rule;
|
||||
}
|
||||
|
||||
public RegionSimple( GluWindingRule rule ) {
|
||||
this.windingNumber = 0;
|
||||
this.rule = rule;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInterior() {
|
||||
switch ( rule ) {
|
||||
case ODD:
|
||||
return ( windingNumber & 1 ) == 1;
|
||||
case NONZERO:
|
||||
return windingNumber != 0;
|
||||
case POSITIVE:
|
||||
return windingNumber > 0;
|
||||
case NEGATIVE:
|
||||
return windingNumber < 0;
|
||||
case ABS_GEQ_TWO:
|
||||
return Math.abs( windingNumber ) >= 2;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj)
|
||||
return true;
|
||||
if (obj == null)
|
||||
return false;
|
||||
if (getClass() != obj.getClass())
|
||||
return false;
|
||||
RegionSimple other = (RegionSimple) obj;
|
||||
if (rule != other.rule)
|
||||
return false;
|
||||
if (windingNumber != other.windingNumber)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format( "Region{number=%1$d,rule=%2$s}", windingNumber, rule );
|
||||
}
|
||||
|
||||
public int getWindingNumber() {
|
||||
return windingNumber;
|
||||
}
|
||||
|
||||
public RegionSimple setWindingNumber( int number ) {
|
||||
this.windingNumber = number;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int increment( final int amount ) {
|
||||
return windingNumber += amount;
|
||||
}
|
||||
|
||||
public enum GluWindingRule {
|
||||
ODD,
|
||||
NONZERO,
|
||||
POSITIVE,
|
||||
NEGATIVE,
|
||||
ABS_GEQ_TWO
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user