|
|
|
@@ -36,6 +36,7 @@ import com.aaaaahhhhhhh.bananapuncher714.mesh.region.RegionRule;
|
|
|
|
|
* The entire algorithm from polygons to triangles can be summarized as follows:
|
|
|
|
|
* 1. Simplify the polygons into one massive DCEL/PSLG. This includes:
|
|
|
|
|
* - Merging vertices that are within VERTEX_TOLERANCE of each other
|
|
|
|
|
* - Shifting vertices that are within VERTEX_TOLERANCE horizontally
|
|
|
|
|
* - Merging vertices on edges that are at most VERTEX_TOLERANCE away
|
|
|
|
|
* - Merging collinear edges
|
|
|
|
|
* - Creating new vertices where two or more edges intersect
|
|
|
|
@@ -54,22 +55,24 @@ import com.aaaaahhhhhhh.bananapuncher714.mesh.region.RegionRule;
|
|
|
|
|
* This step in the algorithm is destructive in the sense that it removes edges, but it
|
|
|
|
|
* does not move any vertices. This step also removes merges collinear edges where possible.
|
|
|
|
|
*
|
|
|
|
|
* 3. Meshing and triangulating the resulting polygons. Firstly, a copy of the PSLG is generated
|
|
|
|
|
* since this step adds edges and vertices to the PSLG, which may be undesirable for reusability.
|
|
|
|
|
* Then, we remove collinear edges which may have been generated as a result of the monotone partitioning.
|
|
|
|
|
* 3. Meshing and triangulating the resulting polygons. Firstly, the mesh is duplicated so that
|
|
|
|
|
* the original mesh can be reused for future modifications, since this step adds edges and
|
|
|
|
|
* vertices to the PSLG, which may be undesirable for reusability. Then, if the user decides to,
|
|
|
|
|
* (which is enabled by default), chain links can be calculated and merged to optimally minimize
|
|
|
|
|
* the total amount of triangles used to triangluate this mesh.
|
|
|
|
|
*
|
|
|
|
|
* Following a general O(nlogn) algorithm, the PSLG is split into polygons strictly monotone with
|
|
|
|
|
* respect to Y. The PSLG is not guaranteed to be comprised of simple polygons, and may contain
|
|
|
|
|
* holes.
|
|
|
|
|
*
|
|
|
|
|
* Once complete, the simple polygons are separated into their own PSLG(that is, they do not share
|
|
|
|
|
* any vertices or edge POJOs), and can be triangulated in parallel. The triangulation method used
|
|
|
|
|
* any vertices or edge objects), and can be triangulated in parallel. The triangulation method used
|
|
|
|
|
* is the algorithm described in Computation Geometry Algorithms and Applications 3rd Ed., which
|
|
|
|
|
* is a linear-time algorithm.
|
|
|
|
|
*
|
|
|
|
|
* The time complexity for the entire algorithm should be roughly O(n^2logn).
|
|
|
|
|
*
|
|
|
|
|
* TODO Remove guaranteed checks or add some compile time thing to remove
|
|
|
|
|
* TODO Update this description
|
|
|
|
|
*
|
|
|
|
|
* @author BananaPuncher714
|
|
|
|
|
*/
|
|
|
|
@@ -91,10 +94,18 @@ public class Mesh< T extends Region > {
|
|
|
|
|
|
|
|
|
|
protected Collection< MeshEventHandler > handlers = new LinkedHashSet< MeshEventHandler >();
|
|
|
|
|
|
|
|
|
|
protected boolean mergeChains = true;
|
|
|
|
|
|
|
|
|
|
public Mesh( final Supplier< T > defaultRegionSupplier ) {
|
|
|
|
|
this.defaultRegionSupplier = defaultRegionSupplier;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Add a polygon with a rule for the edges of the polygon to this mesh.
|
|
|
|
|
*
|
|
|
|
|
* @param poly
|
|
|
|
|
* @param rule
|
|
|
|
|
*/
|
|
|
|
|
public void addPolygon( final Polygon poly, final RegionRule< T > rule ) {
|
|
|
|
|
// Is it at least a triangle?
|
|
|
|
|
if ( poly.getPoints().size() < 3 ) {
|
|
|
|
@@ -135,7 +146,7 @@ public class Mesh< T extends Region > {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Set< Point > getVertices() {
|
|
|
|
|
return vertices.parallelStream().map( v -> { return new Point( v.getPosition() ); } ).collect( Collectors.toSet() );
|
|
|
|
|
return vertices.parallelStream().map( v -> new Point( v.getPosition() ) ).collect( Collectors.toSet() );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Set< Segment > getEdges() {
|
|
|
|
@@ -155,6 +166,14 @@ public class Mesh< T extends Region > {
|
|
|
|
|
return rules.size();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public boolean isMergeChains() {
|
|
|
|
|
return mergeChains;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void setMergeChains( boolean merge ) {
|
|
|
|
|
mergeChains = merge;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void clear() {
|
|
|
|
|
state = MeshState.TRIANGULATION_READY;
|
|
|
|
|
vertices.clear();
|
|
|
|
@@ -232,7 +251,7 @@ public class Mesh< T extends Region > {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
*
|
|
|
|
|
* Process this mesh and triangulate it
|
|
|
|
|
*
|
|
|
|
|
* @return A collection of triangles
|
|
|
|
|
*/
|
|
|
|
@@ -319,6 +338,7 @@ public class Mesh< T extends Region > {
|
|
|
|
|
// At this point in time, if the edge is zero,
|
|
|
|
|
// then the origin and destination must be the same
|
|
|
|
|
if ( edge.getOrigin() != edge.getDest() ) {
|
|
|
|
|
// TODO Remove this
|
|
|
|
|
throw new IllegalStateException( "Zero edge but the origin and destination are not the same!" );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -389,6 +409,7 @@ public class Mesh< T extends Region > {
|
|
|
|
|
* If both or none are there, then that is an issue.
|
|
|
|
|
*/
|
|
|
|
|
if ( !scannedEdges.remove( edge ) ) {
|
|
|
|
|
// TODO Remove this
|
|
|
|
|
throw new IllegalStateException( "Tried to remove an invalid edge!" );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@@ -401,6 +422,21 @@ public class Mesh< T extends Region > {
|
|
|
|
|
// Update the list of vertices with our new updated list of vertices
|
|
|
|
|
vertices = scannedVertices;
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* At this point, each vertex should be merged and moved to the
|
|
|
|
|
* correct position, and all edges should be connected to the
|
|
|
|
|
* correct vertex, but not necessarily in the right order.
|
|
|
|
|
*
|
|
|
|
|
* Invariants:
|
|
|
|
|
* - All vertices are at least VERTEX_TOLERANCE away from each other
|
|
|
|
|
* - All vertices are at least VERTEX_TOLERANCE away from each other horizontally
|
|
|
|
|
* - All vertices will not be moved after this
|
|
|
|
|
* - No vertices have only 1 edge
|
|
|
|
|
* - No vertices are within VERTEX_TOLERANCE of another edge which they are not part of
|
|
|
|
|
* - No edges have length of 0
|
|
|
|
|
* - No edges intersect or overlap
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
state = MeshState.SIMPLIFIED;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -424,14 +460,17 @@ public class Mesh< T extends Region > {
|
|
|
|
|
continue;
|
|
|
|
|
} else if ( isSimilar( v, b ) || isSimilar( v, a ) ) {
|
|
|
|
|
// The vertices should have been merged already
|
|
|
|
|
// TODO Remove this
|
|
|
|
|
throw new IllegalStateException( "Vertex not merged" );
|
|
|
|
|
} else if ( edge.isZero() ) {
|
|
|
|
|
// TODO Remove this
|
|
|
|
|
throw new IllegalStateException( "Edge is zero!" );
|
|
|
|
|
} else {
|
|
|
|
|
// Equations taken from https://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment
|
|
|
|
|
final double edgeLenSq = a.distanceSquared( b );
|
|
|
|
|
final Vector2d toB = b.subtracted( a );
|
|
|
|
|
final double t = v.subtracted( a ).dot( toB ) / edgeLenSq;
|
|
|
|
|
// TODO Maybe check for tolerance here?
|
|
|
|
|
if ( t >= 0 && t <= 1 ) {
|
|
|
|
|
// Get perpendicular distance from v to the line formed by a + b
|
|
|
|
|
final Vector2d projection = toB.multiplied( t ).add( a );
|
|
|
|
@@ -482,11 +521,13 @@ public class Mesh< T extends Region > {
|
|
|
|
|
final IntersectionEdgeToEdge intEdge = ( IntersectionEdgeToEdge ) intersection;
|
|
|
|
|
|
|
|
|
|
if ( positiveEdge.getDest() == other.getDest() ) {
|
|
|
|
|
// TODO Remove this
|
|
|
|
|
throw new IllegalStateException( "Invalid intersection" );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final Vector2d point = intEdge.getPoint();
|
|
|
|
|
if ( isSimilar( point, other.getDest().getPosition() ) || isSimilar( point, positiveEdge.getDest().getPosition() ) ) {
|
|
|
|
|
// TODO Remove this
|
|
|
|
|
throw new IllegalStateException( "Invalid intersection point" );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -538,6 +579,7 @@ public class Mesh< T extends Region > {
|
|
|
|
|
// Splice the two together
|
|
|
|
|
HalfEdge.splice( shorter.getSym(), split );
|
|
|
|
|
} else {
|
|
|
|
|
// TODO Remove this
|
|
|
|
|
throw new IllegalStateException( "Unknown intersection type" );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@@ -575,6 +617,7 @@ public class Mesh< T extends Region > {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else if ( isSimilar( a.getOrigin(), b.getOrigin() ) ) {
|
|
|
|
|
// TODO Remove this
|
|
|
|
|
throw new IllegalArgumentException( "Vertex not merged!" );
|
|
|
|
|
} else if ( !isSimilar( a.getDest(), b.getDest() ) ) {
|
|
|
|
|
// Both edges may intersect somewhere in the middle
|
|
|
|
@@ -710,6 +753,7 @@ public class Mesh< T extends Region > {
|
|
|
|
|
for ( final HalfEdge edge : vertex ) {
|
|
|
|
|
if ( !isPositive( edge ) ) {
|
|
|
|
|
if ( !edges.remove( edge.getSym() ) ) {
|
|
|
|
|
// TODO Remove this
|
|
|
|
|
throw new IllegalStateException( "Negative edge was not added to the edge collection" );
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
@@ -810,16 +854,33 @@ public class Mesh< T extends Region > {
|
|
|
|
|
|
|
|
|
|
vertices = keep;
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* Invariants:
|
|
|
|
|
* - All vertices have an even number of edges
|
|
|
|
|
* - All vertices are in the correct order
|
|
|
|
|
* - There are no consecutive collinear edges part of the same polygon
|
|
|
|
|
* - Exactly one side of every edge is an interior edge
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
state = MeshState.TRIANGULATION_READY;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sort the edges around this vertex.
|
|
|
|
|
/**
|
|
|
|
|
* Sort this vertex such that the edges on this vertex are in counter
|
|
|
|
|
* clockwise order, then set the vertex's edge to the least edge.
|
|
|
|
|
*
|
|
|
|
|
* Multiple vertices can be sorted in parallel without affecting the other.
|
|
|
|
|
*
|
|
|
|
|
* @param vertex
|
|
|
|
|
* @return
|
|
|
|
|
*/
|
|
|
|
|
private static List< HalfEdge > sort( Vertex vertex ) {
|
|
|
|
|
final Vector2d UP = new Vector2d( 0, 1 );
|
|
|
|
|
final List< HalfEdge > edges = new ArrayList< HalfEdge >();
|
|
|
|
|
|
|
|
|
|
for ( final HalfEdge edge : vertex ) {
|
|
|
|
|
if ( edge.isZero() ) {
|
|
|
|
|
// TODO Remove this
|
|
|
|
|
throw new IllegalStateException( "Edge has length of 0" );
|
|
|
|
|
}
|
|
|
|
|
edges.add( edge );
|
|
|
|
@@ -877,6 +938,7 @@ public class Mesh< T extends Region > {
|
|
|
|
|
HalfEdge edge = internalEdge;
|
|
|
|
|
|
|
|
|
|
if ( edge.getPrev() == edge ) {
|
|
|
|
|
// TODO Remove this
|
|
|
|
|
throw new IllegalStateException( "Double sided interior edges detected" );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -996,17 +1058,16 @@ public class Mesh< T extends Region > {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Have a single set of edges. Use a fuzzy compare here for anything that might
|
|
|
|
|
// have made it past the simplification process where two vertices may be within
|
|
|
|
|
// VERTEX_TOLERANCE of each other horizontally. Realistically not necessary, and
|
|
|
|
|
// probably more of a false assurance having it here than not.
|
|
|
|
|
final TreeSet< Vertex > fuzzyVertices = new TreeSet< Vertex >( Mesh::fuzzyCompare );
|
|
|
|
|
fuzzyVertices.addAll( newVertices.values() );
|
|
|
|
|
// At this point each vertex should be connected to the correct edges
|
|
|
|
|
// but needs to be sorted first to ensure that the order is correct.
|
|
|
|
|
final TreeSet< Vertex > vertices = new TreeSet< Vertex >( Mesh::compare );
|
|
|
|
|
vertices.addAll( newVertices.values() );
|
|
|
|
|
|
|
|
|
|
// Sort each vertex so that the edge they are pointing to is the least edge
|
|
|
|
|
fuzzyVertices.parallelStream().forEach( v -> sort( v ) );
|
|
|
|
|
vertices.parallelStream().forEach( v -> sort( v ) );
|
|
|
|
|
|
|
|
|
|
final Collection< Link > allChains = findChains( fuzzyVertices, edges );
|
|
|
|
|
if ( mergeChains ) {
|
|
|
|
|
final Collection< Link > allChains = findChains( vertices, edges );
|
|
|
|
|
final Collection< Link > chains = maximizeChains( allChains );
|
|
|
|
|
if ( !handlers.isEmpty() ) {
|
|
|
|
|
final Collection< Chain > newChains = new HashSet< Chain >();
|
|
|
|
@@ -1060,9 +1121,7 @@ public class Mesh< T extends Region > {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mergeChains( chains, edges );
|
|
|
|
|
|
|
|
|
|
final TreeSet< Vertex > vertices = new TreeSet< Vertex >( Mesh::compare );
|
|
|
|
|
vertices.addAll( fuzzyVertices );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final Collection< EdgePolygon > polys = partitionMonotone( vertices, edges );
|
|
|
|
|
|
|
|
|
@@ -1088,19 +1147,16 @@ public class Mesh< T extends Region > {
|
|
|
|
|
.collect( Collectors.toSet() );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* This algorithm revolves around first generating chains, then sorting which chains and chain links to keep.
|
|
|
|
|
/**
|
|
|
|
|
* Find all chain links that occur within the provided mesh. Mark which ones intersect, and which links are connected.
|
|
|
|
|
*
|
|
|
|
|
* A chain is a collection of chain links.
|
|
|
|
|
* A chain link is 3 collinear vertices.
|
|
|
|
|
* 2 chain links are considered linked if the first link's 2 last vertices are the same as the second link's 2 first vertices.
|
|
|
|
|
* For example, given collinear vertices A, B, C and D, chain links u and v are linked together if
|
|
|
|
|
* chain link u has vertices A, B, C, and if chain link v has vertices B, C and D. No more than 2 chain links
|
|
|
|
|
* can be linked together at one end. Two chains are considered crossing if a link from both chains intersects each other.
|
|
|
|
|
* At most 3 chain links can intersect another chain link, since an intersection only occurs if the segment formed by 2
|
|
|
|
|
* vertices from one chain link intersects another segment formed by 2 vertices from a different chain link.
|
|
|
|
|
* Each link represents 3 directly collinear vertices.
|
|
|
|
|
*
|
|
|
|
|
* The complexity comes from analyzing which combinations of chain links provides the greatest reduction of vertices.
|
|
|
|
|
* Time complexity: O(n^2logn)
|
|
|
|
|
*
|
|
|
|
|
* @param vertices
|
|
|
|
|
* @param interior
|
|
|
|
|
* @return
|
|
|
|
|
*/
|
|
|
|
|
private static Collection< Link > findChains( final TreeSet< Vertex > vertices, final Collection< HalfEdge > interior ) {
|
|
|
|
|
final Vector2d CROSS = new Vector2d( 1, 0 );
|
|
|
|
@@ -1214,8 +1270,10 @@ public class Mesh< T extends Region > {
|
|
|
|
|
final List< HalfEdge > links = region.links.get( region.upper );
|
|
|
|
|
|
|
|
|
|
if ( links == null ) {
|
|
|
|
|
// TODO Remove this
|
|
|
|
|
throw new IllegalStateException( "No links found for scanned vertex!" );
|
|
|
|
|
} else if ( links.size() < 2 ) {
|
|
|
|
|
// TODO Remove this
|
|
|
|
|
throw new IllegalStateException( "Missing links!" );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -1248,17 +1306,13 @@ public class Mesh< T extends Region > {
|
|
|
|
|
region.upper = region.lower;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// There should be exactly 1 link, the lower region's link
|
|
|
|
|
if ( region.links.size() != 1 ) {
|
|
|
|
|
throw new IllegalStateException( "Dangling link!" );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
final List< HalfEdge > links = region.links.get( region.upper );
|
|
|
|
|
|
|
|
|
|
if ( links == null ) {
|
|
|
|
|
// TODO Remove this
|
|
|
|
|
throw new IllegalStateException( "No links found for scanned vertex!" );
|
|
|
|
|
} else if ( links.size() < 2 ) {
|
|
|
|
|
// TODO Remove this
|
|
|
|
|
throw new IllegalStateException( "Missing links!" );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -1284,6 +1338,7 @@ public class Mesh< T extends Region > {
|
|
|
|
|
if ( region.upper != MAX_CROSS ) {
|
|
|
|
|
final List< HalfEdge > prevLinks = region.links.get( region.upper );
|
|
|
|
|
if ( prevLinks.size() < 2 ) {
|
|
|
|
|
// TODO Remove this
|
|
|
|
|
throw new IllegalStateException( "Missing links!" );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -1295,6 +1350,7 @@ public class Mesh< T extends Region > {
|
|
|
|
|
|
|
|
|
|
// As of right now there should only be 1 or 0 links
|
|
|
|
|
if ( region.links.size() > 1 ) {
|
|
|
|
|
// TODO Remove this
|
|
|
|
|
throw new IllegalStateException( "Too many links!" );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@@ -1309,6 +1365,7 @@ public class Mesh< T extends Region > {
|
|
|
|
|
links.add( region.upperEdge );
|
|
|
|
|
links.add( edge );
|
|
|
|
|
if ( region.links.put( region.upper, links ) != null ) {
|
|
|
|
|
// TODO Remove this
|
|
|
|
|
throw new IllegalStateException( "Cross value already used!" );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@@ -1344,8 +1401,10 @@ public class Mesh< T extends Region > {
|
|
|
|
|
final List< HalfEdge > links = region.links.get( region.lower );
|
|
|
|
|
|
|
|
|
|
if ( links == null ) {
|
|
|
|
|
// TODO Remove this
|
|
|
|
|
throw new IllegalStateException( "No links found for scanned vertex!" );
|
|
|
|
|
} else if ( links.size() < 2 ) {
|
|
|
|
|
// TODO Remove this
|
|
|
|
|
throw new IllegalStateException( "Missing links!" );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -1377,16 +1436,13 @@ public class Mesh< T extends Region > {
|
|
|
|
|
region.lower = region.upper;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// There should be exactly 1 link, the lower region's link
|
|
|
|
|
if ( region.links.size() != 1 ) {
|
|
|
|
|
throw new IllegalStateException( "Dangling link!" );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final List< HalfEdge > links = region.links.get( region.lower );
|
|
|
|
|
|
|
|
|
|
if ( links == null ) {
|
|
|
|
|
// TODO Remove this
|
|
|
|
|
throw new IllegalStateException( "No links found for scanned vertex!" );
|
|
|
|
|
} else if ( links.size() < 2 ) {
|
|
|
|
|
// TODO Remove this
|
|
|
|
|
throw new IllegalStateException( "Missing links!" );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -1412,6 +1468,7 @@ public class Mesh< T extends Region > {
|
|
|
|
|
if ( region.lower != MIN_CROSS ) {
|
|
|
|
|
final List< HalfEdge > prevLinks = region.links.get( region.lower );
|
|
|
|
|
if ( prevLinks.size() < 2 ) {
|
|
|
|
|
// TODO Remove this
|
|
|
|
|
throw new IllegalStateException( "Missing links!" );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -1423,6 +1480,7 @@ public class Mesh< T extends Region > {
|
|
|
|
|
|
|
|
|
|
// As of right now there should only be 1 or 0 links
|
|
|
|
|
if ( region.links.size() > 1 ) {
|
|
|
|
|
// TODO Remove this
|
|
|
|
|
throw new IllegalStateException( "Too many links!" );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@@ -1437,6 +1495,7 @@ public class Mesh< T extends Region > {
|
|
|
|
|
links.add( region.lowerEdge );
|
|
|
|
|
links.add( edge );
|
|
|
|
|
if ( region.links.put( region.lower, links ) != null ) {
|
|
|
|
|
// TODO Remove this
|
|
|
|
|
throw new IllegalStateException( "Cross value already used!" );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@@ -1481,16 +1540,20 @@ public class Mesh< T extends Region > {
|
|
|
|
|
|
|
|
|
|
final Vector2d p = chainOrigin.getPosition();
|
|
|
|
|
final Vector2d r = chainDestination.getPosition().subtracted( p );
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* There are two cases where it might seem possible, but are not:
|
|
|
|
|
* - Where this link is the previous link for an existing link
|
|
|
|
|
* - Where the destination of this link is another link's midpoint
|
|
|
|
|
*
|
|
|
|
|
* This is because it depends on the new link's destination being less than
|
|
|
|
|
* any other vertex in all the existing links, which should never be possible.
|
|
|
|
|
*/
|
|
|
|
|
chains.links.parallelStream().forEach( other -> {
|
|
|
|
|
if ( other.getMidpoint() == chainOrigin && other.getDest() == chainMidpoint ) {
|
|
|
|
|
// The new chain is a continuation of this chain
|
|
|
|
|
other.next = chain;
|
|
|
|
|
chain.previous = other;
|
|
|
|
|
// } else if ( chainMidpoint == other.getOrigin() && chainDestination == other.getMidpoint() ) {
|
|
|
|
|
// You may think that this is a valid case, but it is not possible for this to occur since the
|
|
|
|
|
// new chain's destination must always be greater than or equal to the destination of all existing chains.
|
|
|
|
|
// other.previous = chain;
|
|
|
|
|
// chain.next = other;
|
|
|
|
|
} else if ( chainOrigin == other.getMidpoint() ) {
|
|
|
|
|
// In the case that the new chain's origin is another chain's midpoint,
|
|
|
|
|
// we need to check which direction the other chain is facing.
|
|
|
|
@@ -1521,31 +1584,6 @@ public class Mesh< T extends Region > {
|
|
|
|
|
}
|
|
|
|
|
other.intersections.add( chain );
|
|
|
|
|
}
|
|
|
|
|
// } else if ( chainDestination == other.getMidpoint() ) {
|
|
|
|
|
// For the same reason above, this case is also impossible.
|
|
|
|
|
// final Vector2d otherDirection = other.getDest().getPosition().subtracted( other.getOrigin().getPosition() );
|
|
|
|
|
// final double cross = r.cross( otherDirection );
|
|
|
|
|
//
|
|
|
|
|
// boolean intersects = true;
|
|
|
|
|
// final HalfEdge firstLink = other.links.get( 0 );
|
|
|
|
|
// final HalfEdge secondLink = other.links.get( 1 );
|
|
|
|
|
// if ( firstLink.getDest() == other.getMidpoint() ) {
|
|
|
|
|
// // Is the first link a direct connection?
|
|
|
|
|
// intersects = interior.contains( firstLink ) ^ cross < 0;
|
|
|
|
|
// } else if ( secondLink.getDest() == other.getDest() ) {
|
|
|
|
|
// // Is the second link a direct connection?
|
|
|
|
|
// intersects = interior.contains( secondLink ) ^ cross < 0;
|
|
|
|
|
// } else {
|
|
|
|
|
// // The chain is comprised of only vertices...
|
|
|
|
|
// intersects = otherDirection.cross( secondLink.toVector2d() ) < 0 ^ cross < 0;
|
|
|
|
|
// }
|
|
|
|
|
//
|
|
|
|
|
// if ( intersects ) {
|
|
|
|
|
// synchronized ( intersections ) {
|
|
|
|
|
// intersections.add( other );
|
|
|
|
|
// }
|
|
|
|
|
// other.intersections.add( chain );
|
|
|
|
|
// }
|
|
|
|
|
} else if ( other.getDest() == chainMidpoint ) {
|
|
|
|
|
// Likewise, the new chain's midpoint may be another existing chain's
|
|
|
|
|
// destination. We need to check for that case too.
|
|
|
|
@@ -1702,6 +1740,7 @@ public class Mesh< T extends Region > {
|
|
|
|
|
} else {
|
|
|
|
|
leftGoing.add( edge );
|
|
|
|
|
if ( !edgeCollection.remove( edge.getSym() ) ) {
|
|
|
|
|
// TODO Remove this
|
|
|
|
|
throw new IllegalStateException( "Edge not added to the edge collection!" );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@@ -1761,6 +1800,7 @@ public class Mesh< T extends Region > {
|
|
|
|
|
if ( toSplit != null ) {
|
|
|
|
|
|
|
|
|
|
if ( rightGoing.size() < 2 ) {
|
|
|
|
|
// TODO Remove this
|
|
|
|
|
throw new IllegalStateException( "Vertex has less than 2 right edges!" );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -1861,6 +1901,7 @@ public class Mesh< T extends Region > {
|
|
|
|
|
|
|
|
|
|
newPartitions.add( copy );
|
|
|
|
|
} else {
|
|
|
|
|
// TODO Remove this
|
|
|
|
|
throw new IllegalStateException( "A partition with an interior edge does not exist!" );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@@ -1899,6 +1940,7 @@ public class Mesh< T extends Region > {
|
|
|
|
|
// into a single partition.
|
|
|
|
|
|
|
|
|
|
if ( leftGoing.size() < 2 ) {
|
|
|
|
|
// TODO Remove this
|
|
|
|
|
throw new IllegalStateException( "Vertex has less than 2 left edges!" );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -1909,8 +1951,10 @@ public class Mesh< T extends Region > {
|
|
|
|
|
final Partition bottom = upperEdgeMap.remove( bottomLeft.getSym() );
|
|
|
|
|
|
|
|
|
|
if ( top == null ) {
|
|
|
|
|
// TODO Remove this
|
|
|
|
|
throw new IllegalStateException( "Upper partition not found!" );
|
|
|
|
|
} else if ( bottom == null ) {
|
|
|
|
|
// TODO Remove this
|
|
|
|
|
throw new IllegalStateException( "Lower partition not found!" );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -1956,6 +2000,7 @@ public class Mesh< T extends Region > {
|
|
|
|
|
if ( partition != null ) {
|
|
|
|
|
|
|
|
|
|
if ( partition.upper != left.getSym() ) {
|
|
|
|
|
// TODO Remove this
|
|
|
|
|
throw new IllegalStateException( "Partition upper edge is inconsistent!" );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -1971,6 +2016,7 @@ public class Mesh< T extends Region > {
|
|
|
|
|
// Add a new region for this vertex
|
|
|
|
|
partition.add( event, new VisibilityRegion( left, right ) );
|
|
|
|
|
} else {
|
|
|
|
|
// TODO Remove this
|
|
|
|
|
throw new IllegalStateException( "Interior edge does not have a partition!" );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@@ -1984,6 +2030,7 @@ public class Mesh< T extends Region > {
|
|
|
|
|
if ( partition != null ) {
|
|
|
|
|
|
|
|
|
|
if ( partition.lower != left.getSym() ) {
|
|
|
|
|
// TODO Remove this
|
|
|
|
|
throw new IllegalStateException( "Partition lower edge is inconsistent!" );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -1998,6 +2045,7 @@ public class Mesh< T extends Region > {
|
|
|
|
|
// Add a new region for this vertex
|
|
|
|
|
partition.add( event, new VisibilityRegion( right, left ) );
|
|
|
|
|
} else {
|
|
|
|
|
// TODO Remove this
|
|
|
|
|
throw new IllegalStateException( "Interior edge does not have a partition!" );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@@ -2014,6 +2062,7 @@ public class Mesh< T extends Region > {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( !upperEdgeMap.isEmpty() || !lowerEdgeMap.isEmpty() ) {
|
|
|
|
|
// TODO Remove this
|
|
|
|
|
throw new IllegalStateException( "Dangling partitions!" );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -2023,6 +2072,16 @@ public class Mesh< T extends Region > {
|
|
|
|
|
return aggregateLinks;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Given a collection of chain links, maximize the amount of non intersecting
|
|
|
|
|
* links. Use a greedy method to select whichever links have the least amount
|
|
|
|
|
* of intersections, until there are no more chain links left to be selected.
|
|
|
|
|
*
|
|
|
|
|
* Time complexity: O(n^2) worst case
|
|
|
|
|
*
|
|
|
|
|
* @param chains
|
|
|
|
|
* @return
|
|
|
|
|
*/
|
|
|
|
|
private static Collection< Link > maximizeChains( final Collection< Link > chains ) {
|
|
|
|
|
final Collection< Link > remainingChains = new LinkedHashSet< Link >( chains );
|
|
|
|
|
final Collection< Link > selectedChains = new HashSet< Link >();
|
|
|
|
@@ -2060,6 +2119,14 @@ public class Mesh< T extends Region > {
|
|
|
|
|
return selectedChains;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Given a collection of chain links, merge them into the resulting mesh.
|
|
|
|
|
*
|
|
|
|
|
* Time complexity: O(n)
|
|
|
|
|
*
|
|
|
|
|
* @param selected
|
|
|
|
|
* @param interior
|
|
|
|
|
*/
|
|
|
|
|
private static void mergeChains( final Collection< Link > selected, final Collection< HalfEdge > interior ) {
|
|
|
|
|
final Collection< Link > chains = new HashSet< Link >( selected );
|
|
|
|
|
while ( !chains.isEmpty() ) {
|
|
|
|
@@ -2106,6 +2173,7 @@ public class Mesh< T extends Region > {
|
|
|
|
|
if ( interior.contains( head ) ) {
|
|
|
|
|
// Left handed
|
|
|
|
|
if ( right != null ) {
|
|
|
|
|
// TODO Remove this
|
|
|
|
|
throw new IllegalStateException( "Unterminated left edge!" );
|
|
|
|
|
}
|
|
|
|
|
right = new HalfEdge();
|
|
|
|
@@ -2144,6 +2212,7 @@ public class Mesh< T extends Region > {
|
|
|
|
|
} else {
|
|
|
|
|
// Right handed
|
|
|
|
|
if ( left != null ) {
|
|
|
|
|
// TODO Remove this
|
|
|
|
|
throw new IllegalStateException( "Unterminated right edge!" );
|
|
|
|
|
}
|
|
|
|
|
left = new HalfEdge();
|
|
|
|
@@ -2328,7 +2397,7 @@ public class Mesh< T extends Region > {
|
|
|
|
|
edge = edge.getPrev();
|
|
|
|
|
|
|
|
|
|
if ( --size < 0 ) {
|
|
|
|
|
throw new IllegalStateException( "Not good vertex! With only " + vertex.size() );
|
|
|
|
|
throw new IllegalStateException( "Checked too many edges!" );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@@ -2359,9 +2428,11 @@ public class Mesh< T extends Region > {
|
|
|
|
|
final SortedEdgeCollection< HalfEdge > edgeCollection = new SortedEdgeCollection< HalfEdge >( ( a, b ) -> {
|
|
|
|
|
// Assume each region is marked by an upper edge going from right to left, up to down
|
|
|
|
|
if ( a.isZero() || b.isZero() ) {
|
|
|
|
|
// TODO Remove this
|
|
|
|
|
throw new IllegalStateException( "Zero length edge detected" );
|
|
|
|
|
}
|
|
|
|
|
if ( !( isPositive( a ) && isPositive( b ) ) ) {
|
|
|
|
|
// TODO Remove this
|
|
|
|
|
throw new IllegalStateException( "Negative edge detected" );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -2419,6 +2490,7 @@ public class Mesh< T extends Region > {
|
|
|
|
|
} else {
|
|
|
|
|
leftGoing.add( edge );
|
|
|
|
|
if ( !edgeCollection.remove( edge.getSym() ) ) {
|
|
|
|
|
// TODO Remove this
|
|
|
|
|
throw new IllegalStateException( "Edge not added to the edge collection!" );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@@ -2453,10 +2525,13 @@ public class Mesh< T extends Region > {
|
|
|
|
|
// The current vertex must be between the upper and the lower
|
|
|
|
|
// edge, and it cannot be part of either edge
|
|
|
|
|
if ( upper == null ) {
|
|
|
|
|
// TODO Remove this
|
|
|
|
|
throw new IllegalStateException( "Lower does not have an upper edge!" );
|
|
|
|
|
} if ( !interior.contains( upper.getSym() ) ) {
|
|
|
|
|
// TODO Remove this
|
|
|
|
|
throw new IllegalStateException( "Polygon is not simple!" );
|
|
|
|
|
} else if ( upper.getOrigin() == event || lower.getOrigin() == event ) {
|
|
|
|
|
// TODO Remove this
|
|
|
|
|
throw new IllegalStateException( "Vertex is invalid!" );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -2464,6 +2539,7 @@ public class Mesh< T extends Region > {
|
|
|
|
|
eventMark = new MarkedEdge( lower, upper );
|
|
|
|
|
}
|
|
|
|
|
} else if ( rightGoing.isEmpty() && leftGoing.isEmpty() ) {
|
|
|
|
|
// TODO Remove this
|
|
|
|
|
throw new IllegalStateException( "Cannot have a vertex with no edges!" );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -2503,6 +2579,7 @@ public class Mesh< T extends Region > {
|
|
|
|
|
// upper/lower region as this vertex, then
|
|
|
|
|
// that must be because this vertex is left marked
|
|
|
|
|
if ( !leftGoing.isEmpty() ) {
|
|
|
|
|
// TODO Remove this
|
|
|
|
|
throw new IllegalStateException( "Vertex is not left-marked!" );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -2583,6 +2660,7 @@ public class Mesh< T extends Region > {
|
|
|
|
|
if ( isPositive( edge ) ) {
|
|
|
|
|
edgeCollection.insert( edge );
|
|
|
|
|
} else {
|
|
|
|
|
// TODO Remove this
|
|
|
|
|
throw new IllegalStateException( "Edge suddenly changed sign!" );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@@ -2590,6 +2668,7 @@ public class Mesh< T extends Region > {
|
|
|
|
|
|
|
|
|
|
// All right marked vertices MUST be resolved by now.
|
|
|
|
|
if ( !rightMarked.isEmpty() ) {
|
|
|
|
|
// TODO Remove this
|
|
|
|
|
throw new IllegalStateException( "Unresolved right marked vertices!" );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -2648,6 +2727,14 @@ public class Mesh< T extends Region > {
|
|
|
|
|
return newPolygons;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Generate triangles as described in Computational Geometry Algorithms and Applications 3rd Edition.
|
|
|
|
|
*
|
|
|
|
|
* A linear time algorithm.
|
|
|
|
|
*
|
|
|
|
|
* @param polygon
|
|
|
|
|
* @return
|
|
|
|
|
*/
|
|
|
|
|
private static Collection< Polygon > triangulate( final EdgePolygon polygon ) {
|
|
|
|
|
// Go down each monotone chain and connect the vertices where possible.
|
|
|
|
|
// Implements the O(n) triangulation of a polygon as described in
|
|
|
|
@@ -2757,8 +2844,10 @@ public class Mesh< T extends Region > {
|
|
|
|
|
// TODO Remove this NOW
|
|
|
|
|
toSort.parallelStream().forEach( v -> sort( v ) );
|
|
|
|
|
|
|
|
|
|
Map< Vertex, Point > pointMap = polygon.vertices.parallelStream().collect( Collectors.toMap( Function.identity(), v -> { return new Point( v.getPosition() ); } ) );
|
|
|
|
|
// Convert each vertex to a point
|
|
|
|
|
final Map< Vertex, Point > pointMap = polygon.vertices.parallelStream().collect( Collectors.toMap( Function.identity(), v -> new Point( v.getPosition() ) ) );
|
|
|
|
|
|
|
|
|
|
// Convert each triangle to a polygon
|
|
|
|
|
final Collection< Polygon > polygons = new ArrayDeque< Polygon >();
|
|
|
|
|
final Set< HalfEdge > scanned = new HashSet< HalfEdge >();
|
|
|
|
|
for ( final HalfEdge edge : polygon.getEdges() ) {
|
|
|
|
@@ -2771,6 +2860,7 @@ public class Mesh< T extends Region > {
|
|
|
|
|
} while ( scanned.add( temp = temp.getNext() ) );
|
|
|
|
|
|
|
|
|
|
if ( points.size() != 3 ) {
|
|
|
|
|
// TODO Remove this
|
|
|
|
|
throw new IllegalStateException( "Not a triangle: " + points.size() );
|
|
|
|
|
}
|
|
|
|
|
polygons.add( new Polygon( points ) );
|
|
|
|
@@ -2821,28 +2911,6 @@ public class Mesh< T extends Region > {
|
|
|
|
|
return compare( edge.getOrigin(), edge.getDest() ) <= 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* Since sometimes vertices will be off by a few floating point errors,
|
|
|
|
|
* it will cause problems because we have checks for tolerance, but if
|
|
|
|
|
* the sorting for vertices does not, it causes issues.
|
|
|
|
|
*/
|
|
|
|
|
private static int fuzzyCompare( final Vertex a, final Vertex b ) {
|
|
|
|
|
if ( a == b ) {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
final Vector2d aPos = a.getPosition();
|
|
|
|
|
final Vector2d bPos = b.getPosition();
|
|
|
|
|
|
|
|
|
|
final double xDiff = aPos.getX() - bPos.getX();
|
|
|
|
|
if ( Math.abs( xDiff ) < VERTEX_TOLERANCE ) {
|
|
|
|
|
return Double.compare( aPos.getY(), bPos.getY() );
|
|
|
|
|
} else {
|
|
|
|
|
return Double.compare( aPos.getX(), bPos.getX() );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static int compare( final Vertex a, final Vertex b ) {
|
|
|
|
|
if ( a == b ) {
|
|
|
|
|
return 0;
|
|
|
|
|