package com.aaaaahhhhhhh.bananapuncher714.mesh; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Optional; import java.util.PriorityQueue; import java.util.Queue; import java.util.Set; import java.util.Stack; import java.util.TreeSet; import java.util.function.Supplier; import java.util.stream.Collectors; import com.aaaaahhhhhhh.bananapuncher714.mesh.region.Region; import com.aaaaahhhhhhh.bananapuncher714.mesh.region.RegionRule; /** * A general mesh class containing polygons. Inspiration taken from: https://github.com/memononen/libtess2 * * This general algorithm loosely follows the libtess2 source code, but is more or less re-done * from scratch to be simpler and easier to follow and comprehend. In addition, there is support * for custom region winding. * * 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 * - Merging vertices on edges that are at most VERTEX_TOLERANCE away * - Merging colinear edges * - Creating new vertices where two or more edges intersect * This makes the resulting graph much easier to traverse and process. Unlike libtess2, * we do the simplification separate from step 2, so that we don't run into cases where * modifying/fixing a vertex will change some property of a previous vertex, thus invalidating * previously processed sections. * * In addition to the simplification, this also merges overlapping edges, which is performed * before generating regions. All RegionRules for each polygon _must_ be able to be merged. * That is, given rule A, B and C, and region u and v, v = B * A * u, then C = B * A and v = C * u. * * 2. Generating regions based on the PSLG and RegionRules. Given the default region outside * the PSLG, determine which regions enclosed by edges are interior or exterior regions. * Any edges between two interior/exterior regions is a no-op edge, and can be removed. * 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 colinear 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 colinear edges which may have been generated as a result of the monotone partitioning. * 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 * 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(nlogn). * * TODO Remove guaranteed checks or add some compile time thing to remove * * @author BananaPuncher714 */ public class Mesh< T extends Region > { public static final double VERTEX_TOLERANCE = 1e-7; public static final double ANGLE_TOLERANCE = Math.toRadians( 0.0001 ); // All points in the graph protected Collection< Vertex > vertices = new ArrayDeque< Vertex >(); // All rules associated with a given half edge protected Map< HalfEdge, RegionRule< T > > rules = new HashMap< HalfEdge, RegionRule< T > >(); protected Set< HalfEdge > interiorEdges = new HashSet< HalfEdge >(); protected final Supplier< T > defaultRegionSupplier; protected MeshState state = MeshState.TRIANGULATION_READY; public Mesh( final Supplier< T > defaultRegionSupplier ) { this.defaultRegionSupplier = defaultRegionSupplier; } public void addPolygon( final Polygon poly, final RegionRule< T > rule ) { // Is it at least a triangle? if ( poly.getPoints().size() < 3 ) { return; } HalfEdge edge = null; for ( Point point : poly.getPoints() ) { if ( edge == null ) { edge = new HalfEdge(); HalfEdge.splice( edge, edge.getSym() ); edge.getSym().setOrigin( edge.getOrigin() ); } else { edge = edge.split(); } edge.getOrigin().setPosition( new Vector2d( point ) ); rules.put( edge, rule ); rules.put( edge.getSym(), rule.inverse() ); vertices.add( edge.getOrigin() ); state = MeshState.DIRTY; } } public Collection< Vertex > getVertices() { return vertices; } public int getRuleSize() { return rules.size(); } public void clear() { state = MeshState.TRIANGULATION_READY; vertices.clear(); rules.clear(); interiorEdges.clear(); } public Mesh< T > copyOf() { final Mesh< T > copy = new Mesh< T >( defaultRegionSupplier ); // Keep track of all edges that we've seen final EdgeSet scanned = new EdgeSet(); // Map each old vertex to a new vertex final Map< Vertex, Vertex > newVertices = new HashMap< Vertex, Vertex >(); /* * We now want to create a completely separate PSLG so that we don't modify * the original one. */ for ( Vertex vertex : vertices ) { for ( final HalfEdge edge : vertex ) { if ( scanned.add( edge ) ) { HalfEdge temp = edge; HalfEdge newEdge = null; do { if ( newEdge == null ) { newEdge = new HalfEdge(); HalfEdge.splice( newEdge, newEdge.getSym() ); newEdge.getSym().setOrigin( newEdge.getOrigin() ); } else { newEdge = newEdge.split(); } final Vertex tempVert = temp.getOrigin(); // Re-use a previously created vertex if there is one Vertex vert; if ( newVertices.containsKey( tempVert ) ) { vert = newVertices.get( tempVert ); newEdge.setOrigin( vert ); newEdge.getPrev().setOrigin( vert ); HalfEdge.splice( newEdge.getPrev(), vert.getEdge() ); } else { vert = newEdge.getOrigin(); newVertices.put( tempVert, vert ); vert.setPosition( tempVert.getPosition() ); copy.vertices.add( vert ); } if ( interiorEdges.contains( temp ) ) { copy.interiorEdges.add( temp ); } if ( interiorEdges.contains( temp.getSym() ) ) { copy.interiorEdges.add( temp.getSym() ); } copy.rules.put( newEdge, rules.get( temp ) ); copy.rules.put( newEdge.getSym(), rules.get( temp.getSym() ) ); } while ( scanned.add( temp = temp.getNext() ) ); } } } copy.vertices.parallelStream().forEach( v -> sort( v ) ); copy.state = state; return copy; } /** * Do a check over all vertices to see if there are any within * VERTEX_TOLERANCE of each other. If so, then merge them. * * The exact order of the vertices and their edges is not particularly important. * * In addition, remove vertices without any edges, and solve any * intersections by splitting edges and/or adding new vertices as required. * * This method only removes intersections. It does NOT guarantee that polygons * will be simple, since there can be holes. */ public void simplify() { // Create a queue and insert all vertices in O(nlogn) time. final Queue< Vertex > queue = new PriorityQueue< Vertex >( Mesh::compare ); queue.addAll( vertices ); /* * As we iterate through each vertex, there are certain operations * that are concerned with any edges that we may be potentially sweeping though. * * This means we must keep track of all edges at and a little ahead of the current * scan line. We don't want to iterate through each edge every time since that would * make this method take polynomial time, so instead add edges when we advance the * scanline and remove any edges from vertices that we have already passed. */ final EdgeSet scannedEdges = new EdgeSet(); // This set contains the non-redundant vertices. final Set< Vertex > scannedVertices = new HashSet< Vertex >(); Vertex vertex; while ( ( vertex = queue.poll() ) != null ) { final Vector2d pos = vertex.getPosition(); { final List< Vertex > addBack = new LinkedList< Vertex >(); Vertex other; // We must poll and add back later since priority queues are not // sorted, so an iterator over its elements would not guarantee // the correct ordering unless we poll it. while ( ( other = queue.poll() ) != null ) { final Vector2d otherPos = other.getPosition(); // Once the vertex scanned is past the tolerance in the X direction, // we know there are no more vertices which can be merged with the // current vertex. if ( compareX( otherPos, pos ) > VERTEX_TOLERANCE ) { addBack.add( other ); break; } // Check if the vertices are close enough that they can // be considered "the same" if ( isSimilar( pos, otherPos ) ) { HalfEdge.splice( vertex.getEdge(), other.getEdge() ); } else { addBack.add( other ); } } queue.addAll( addBack ); } { // Remove any zero-edges HalfEdge validEdge = null; for ( HalfEdge edge : vertex ) { if ( edge.isZero() ) { // At this point in time, if the edge is zero, // then the origin and destination must be the same if ( edge.getOrigin() != edge.getDest() ) { throw new IllegalStateException( "Zero edge but the origin and destination are not the same!" ); } // Un-splice the edge and its sym from the vertex HalfEdge.splice( edge, edge.getSym().getNext() ); HalfEdge.splice( edge.getSym(), edge.getNext() ); } else { validEdge = edge; } } // This vertex does not contain any edges!! if ( validEdge == null ) { continue; } else { vertex.setEdge( validEdge ); } } // Add all edges from this vertex if they do not already exist in // the set of scanned edges for ( final HalfEdge edge : vertex ) { scannedEdges.add( edge ); } // Add edges that belong to vertices up to VERTEX_TOLERANCE away in the X direction for ( final Iterator< Vertex > it = queue.iterator(); it.hasNext(); ) { final Vertex other = it.next(); final Vector2d otherPos = other.getPosition(); if ( compareX( otherPos, pos ) > VERTEX_TOLERANCE ) { break; } for ( final HalfEdge edge : other ) { scannedEdges.add( edge ); } } // Update all edges this vertex is connected to for consistency vertex.update(); // Merge any vertices with edges that they may be on resolveVertexIntersections( scannedEdges, vertex ); for ( final HalfEdge edge : vertex ) { // Resolve any intersections belonging to this edge which are still // being scanned if ( scannedEdges.contains( edge ) ) { resolveEdgeIntersections( queue, scannedEdges, edge ); } } // Remove all edges from previously scanned vertices that are no longer useful for ( final HalfEdge edge : vertex ) { if ( scannedVertices.contains( edge.getDest() ) ) { /* * Remove the half edge and it's sym if the destination * has already been removed, since it means that it is * no longer relevant. * * However, only one half-edge should have been present. * If both or none are there, then that is an issue. */ if ( !scannedEdges.remove( edge ) ) { throw new IllegalStateException( "Tried to remove an invalid edge!" ); } } } scannedVertices.add( vertex ); } mergeOverlappingEdges( scannedVertices ); // Update the list of vertices with our new updated list of vertices vertices = scannedVertices; state = MeshState.SIMPLIFIED; } /** * Check for vertices which are directly on an edge. * * @param scannedEdges * @param vertex */ private void resolveVertexIntersections( final EdgeSet scannedEdges, final Vertex vertex ) { // Split any edges passing through vertex, which do not include // the vertex as the origin or destination final Vector2d v = vertex.getPosition(); final Collection< HalfEdge > toInsert = new ArrayDeque< HalfEdge >(); for ( final HalfEdge edge : scannedEdges ) { final Vector2d a = edge.getOrigin().getPosition(); final Vector2d b = edge.getDest().getPosition(); if ( v == a || v == b ) { // Skip if this edge continue; } else if ( isSimilar( v, b ) || isSimilar( v, a ) ) { // The vertices should have been merged already throw new IllegalStateException( "Vertex not merged" ); } else if ( edge.isZero() ) { 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; 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 ); // Check if the distance between the vertex // and the edge is within the tolerance if ( isSimilar( v, projection ) ) { final HalfEdge split = edge.split(); // Set the vertex to this vertex split.setOrigin( vertex ); edge.getSym().setOrigin( vertex ); // Splice the newly split wing into the vertex HalfEdge.splice( vertex.getEdge(), split ); // Don't forget to update the winding rules for the newly created edge!! final RegionRule< T > rule = rules.get( edge ); rules.put( split, rule ); rules.put( split.getSym(), rule.inverse() ); toInsert.add( split ); } } } } scannedEdges.addAll( toInsert ); } /** * Given a set of edges within the scan region, check for * intersections with a half edge. * * @param scannedEdges * @param edge */ private void resolveEdgeIntersections( final Collection< Vertex > vertices, final EdgeSet scannedEdges, final HalfEdge edge ) { // Use the positive edge for finding the intersection final HalfEdge positiveEdge = isPositive( edge ) ? edge : edge.getSym(); for ( HalfEdge other : scannedEdges ) { if ( !isPositive( other ) ) { other = other.getSym(); } final Optional< Intersection > optInt = getIntersection( positiveEdge, other ); if ( optInt.isPresent() ) { final Intersection intersection = optInt.get(); if ( intersection instanceof IntersectionEdgeToEdge ) { final IntersectionEdgeToEdge intEdge = ( IntersectionEdgeToEdge ) intersection; if ( positiveEdge.getDest() == other.getDest() ) { throw new IllegalStateException( "Invalid intersection" ); } final Vector2d point = intEdge.getPoint(); if ( isSimilar( point, other.getDest().getPosition() ) || isSimilar( point, positiveEdge.getDest().getPosition() ) ) { throw new IllegalStateException( "Invalid intersection point" ); } // Split the first edge HalfEdge aSplit = edge.split(); RegionRule< T > aRule = rules.get( edge ); rules.put( aSplit, aRule ); rules.put( aSplit.getSym(), aRule.inverse() ); // Update the position final Vertex origin = aSplit.getOrigin(); origin.setPosition( point ); // Split the other edge HalfEdge bSplit = other.split(); RegionRule< T > bRule = rules.get( other ); rules.put( bSplit, bRule ); rules.put( bSplit.getSym(), bRule.inverse() ); // Splice the edges together into the same vertex HalfEdge.splice( bSplit, aSplit ); origin.update(); vertices.add( origin ); } else if ( intersection instanceof IntersectionColinear ) { final IntersectionColinear intCol = ( IntersectionColinear ) intersection; final HalfEdge shorter = intCol.getShorterEdge(); HalfEdge longer = intCol.getLongerEdge(); // Use the original edge for splitting to preserve the order of // edges around the current vertex if ( longer == positiveEdge ) { longer = edge; } // Update the winding rule for the split final HalfEdge split = longer.split(); RegionRule< T > rule = rules.get( longer ); rules.put( split, rule ); rules.put( split.getSym(), rule.inverse() ); // Update the vertex for the split final Vertex vert = shorter.getDest(); split.setOrigin( vert ); longer.getSym().setOrigin( vert ); // Splice the two together HalfEdge.splice( shorter.getSym(), split ); } else { throw new IllegalStateException( "Unknown intersection type" ); } } } } /** * Calculate the intersection for two edges * * @param a * @param b * @return */ private static Optional< Intersection > getIntersection( HalfEdge a, HalfEdge b ) { // Assume a and b are both positive // This assumes that if a and b are colinear, then they _must_ share // the same origin, since we are traversing through the vertices // starting with the smallest. if ( a.getOrigin() == b.getOrigin() ) { // Do not split edges that are pretty much equal, since they will get merged later if ( !isSimilar( a.getDest(), b.getDest() ) ) { final Vector2d r = a.toVector2d().normalize(); final Vector2d s = b.toVector2d().normalize(); final double angle = r.cross( s ); // Ignore edges that are not colinear if ( Math.abs( angle ) <= ANGLE_TOLERANCE ) { // Compare which edge is shorter if ( r.lengthSquared() < s.lengthSquared() ) { return Optional.of( new IntersectionColinear( a, b ) ); } else { return Optional.of( new IntersectionColinear( b, a ) ); } } } } else if ( isSimilar( a.getOrigin(), b.getOrigin() ) ) { throw new IllegalArgumentException( "Vertex not merged!" ); } else if ( !isSimilar( a.getDest(), b.getDest() ) ) { // Both edges may intersect somewhere in the middle final Vector2d p = a.getOrigin().getPosition(); final Vector2d r = a.toVector2d(); final Vector2d q = b.getOrigin().getPosition(); final Vector2d s = b.toVector2d(); final Vector2d pq = q.subtracted( p ); final double pqr = pq.cross( r ); final double rs = r.cross( s ); // Check if parallel if ( rs != 0 ) { // Calculate t and u final double t = pq.cross( s ) / rs; final double u = pqr / rs; final double tTol = VERTEX_TOLERANCE / r.length(); final double uTol = VERTEX_TOLERANCE / s.length(); // Do the line segments intersect // They should NOT intersect at their endpoints if ( t > tTol && t < ( 1 - tTol ) && u > uTol && u < ( 1 - uTol ) ) { // Calculate the point of intersection final Vector2d intersection = r.multiply( t ).add( p ); return Optional.of( new IntersectionEdgeToEdge( intersection ) ); } } } return Optional.empty(); } /** * The goal for this method is to sort vertices * and to ensure that edges with the same endpoint and * destination are combined. */ private void mergeOverlappingEdges( final Collection< Vertex > vertices ) { for ( final Vertex vertex : vertices ) { sort( vertex ); // Keep track of which ranges of edges are attached to which final Map< Vertex, Queue< HalfEdge > > ranges = new HashMap< Vertex, Queue< HalfEdge > >(); // Get a list of edges that have the same endpoint for ( final HalfEdge edge : vertex ) { // Only check positive edges // Technically we can do both, but this will result in us // having to check ~50% less edges if ( isPositive( edge ) ) { Queue< HalfEdge > edges; if ( ranges.containsKey( edge.getDest() ) ) { edges = ranges.get( edge.getDest() ); } else { edges = new ArrayDeque< HalfEdge >(); ranges.put( edge.getDest(), edges ); } edges.add( edge ); } } for ( final Entry< Vertex, Queue< HalfEdge > > entry : ranges.entrySet() ) { final Queue< HalfEdge > edges = entry.getValue(); // Only merge the edges if there are more than 1 edges to merge if ( edges.size() > 1 ) { // Keep the first edge, and discard the rest final HalfEdge first = edges.poll(); RegionRule< T > rule = rules.get( first ); while ( !edges.isEmpty() ) { final HalfEdge edge = edges.poll(); // Update the rule rule = rule.apply( rules.get( edge ) ); // Update the destination's edge edge.getDest().setEdge( edge.getNext() ); // Don't need to worry about updating this vertex's edge // because the edge is either negative, the first edge in a range, // or because it was not touched. // Now remove the edge HalfEdge.splice( edge, edge.getSym().getNext() ); HalfEdge.splice( edge.getSym(), edge.getNext() ); // Manually remove the edge from the rule map rules.remove( edge ); rules.remove( edge.getSym() ); } rules.put( first, rule ); rules.put( first.getSym(), rule.inverse() ); } } } } /** * The vertices must be simplified, and must not contain any self * intersections or overlaps. */ public void generateRegions() { if ( state == MeshState.TRIANGULATION_READY ) { // The regions have already been generated return; } else if ( state != MeshState.SIMPLIFIED ) { throw new IllegalStateException( "Mesh is not simplified!" ); } final Queue< Vertex > queue = new PriorityQueue< Vertex >( Mesh::compare ); queue.addAll( vertices ); // Keep track of what regions belong to what edge final Map< HalfEdge, T > regions = new HashMap< HalfEdge, T >(); // Keep track of edges from down to up final SortedEdgeCollection< HalfEdge > edges = new SortedEdgeCollection< HalfEdge >( Mesh::greaterThanOrEqualTo ); // Keep track of edges that face inside or outside final Set< HalfEdge > interiorAboveEdges = new HashSet< HalfEdge >(); final Set< HalfEdge > interiorBelowEdges = new HashSet< HalfEdge >(); // Keep track of the edges that need to be removed, // because they do not change if a region is interior/exterior final Set< HalfEdge > toRemove = new HashSet< HalfEdge >(); // Assume the edges on each vertex is already sorted Vertex vertex; while ( ( vertex = queue.poll() ) != null ) { // Insert all edges into the edge set for ( final HalfEdge edge : vertex ) { if ( !isPositive( edge ) ) { if ( !edges.remove( edge.getSym() ) ) { throw new IllegalStateException( "Negative edge was not added to the edge collection" ); } } else { edges.insert( edge ); } } T currentRegion = defaultRegionSupplier.get(); // Loop over the edges from bottom to top for ( final HalfEdge edge : edges ) { final RegionRule< T > rule = rules.get( edge ); // Apply the rule to the previous region final boolean wasInterior = currentRegion.isInterior(); currentRegion = rule.apply( currentRegion ); if ( currentRegion.isInterior() == wasInterior ) { toRemove.add( edge ); } if ( regions.containsKey( edge ) ) { final T old = regions.get( edge ); if ( !old.equals( currentRegion ) ) { // See if the previously calculated region for this edge is consistent throw new IllegalStateException( "Inconsistent winding rule!" ); } else if ( wasInterior ^ interiorBelowEdges.contains( edge ) ) { // If the previous region's interior status does not match // what was recorded previously throw new IllegalStateException( "Inconsistent region status" ); } } else { regions.put( edge, currentRegion ); if ( currentRegion.isInterior() ) { // Mark this edge as an above interior edge, that is, // the region above this edge is considered "inside" interiorAboveEdges.add( edge ); } if ( wasInterior ) { // Mark this edge as a below interior edge, // where the region below this edge is considered "inside" interiorBelowEdges.add( edge ); } } } } // Keep track of which vertices we want to keep final Set< Vertex > keep = new HashSet< Vertex >( vertices ); /* * Remove edges from the graph, because the two regions on * either side of the edge are both interior/exterior. * * This _will_ cause issues if you want to preserve * more information than just interior/exterior, * so this method is technically a destructive * method, and the resulting graph cannot be re-used. * * However, if the rule you are using is not more * complex than a simple winding number, then you * can probably re-use the vertices. */ for ( final HalfEdge edge : toRemove ) { // Update the edge of the vertex, or remove the vertex // if this is the only edge on the vertex if ( edge.getPrev() == edge ) { keep.remove( edge.getOrigin() ); } else { edge.getOrigin().setEdge( edge.getPrev() ); HalfEdge.splice( edge, edge.getSym().getNext() ); } if ( edge.getNext() == edge.getSym() ) { keep.remove( edge.getDest() ); } else { edge.getDest().setEdge( edge.getNext() ); HalfEdge.splice( edge.getSym(), edge.getNext() ); } // Manually remove the edge from the rule and region map rules.remove( edge ); rules.remove( edge.getSym() ); regions.remove( edge ); interiorAboveEdges.remove( edge ); interiorBelowEdges.remove( edge ); } // Merge any colinear edges before returning mergeColinearEdges( keep, interiorAboveEdges ).forEach( e -> { rules.remove( e ); rules.remove( e.getSym() ); } ); interiorEdges.clear(); interiorEdges.addAll( interiorAboveEdges ); vertices = keep; state = MeshState.TRIANGULATION_READY; } // Sort the edges around this vertex. private static List< HalfEdge > sort( Vertex vertex ) { final List< HalfEdge > edges = new ArrayList< HalfEdge >(); for ( final HalfEdge edge : vertex ) { if ( edge.isZero() ) { throw new IllegalStateException( "Edge has length of 0" ); } edges.add( edge ); } // If there are 2 or less edges, they will always be in order if ( edges.size() > 2 ) { // Sort the edges in a counter clockwise direction, // where 11:59 is the least, and 12:00 is the greatest Collections.sort( edges, ( e1, e2 ) -> { // This assumes that no edge has length of 0 final boolean e1p = isPositive( e1 ); final boolean e2p = isPositive( e2 ); if ( e1p ^ e2p ) { return e1p ? 1 : -1; } else if ( e1.getDest() == e2.getDest() ) { return 0; } else { final double cross = e2.toVector2d().cross( e1.toVector2d() ); return Double.compare( cross, 0 ); } } ); for ( int i = 0; i < edges.size(); ++i ) { // Get this edge and the next edge final HalfEdge e1 = edges.get( i ); final HalfEdge e2 = edges.get( ( i + 1 ) % edges.size() ); // Make sure that they are in order if ( e1.getPrev() != e2 ) { e1.setPrev( e2 ); e2.getSym().setNext( e1 ); } } vertex.setEdge( edges.get( 0 ) ); } return edges; } private static Collection< HalfEdge > mergeColinearEdges( final Collection< Vertex > vertices, final Collection< HalfEdge > internalEdges ) { final Collection< HalfEdge > toRemove = new ArrayDeque< HalfEdge >(); final Set< HalfEdge > edges = new HashSet< HalfEdge >(); for ( final HalfEdge internalEdge : internalEdges ) { // Skip any edges that we have already checked if ( edges.contains( internalEdge ) ) { continue; } HalfEdge edge = internalEdge; if ( edge.getPrev() == edge ) { throw new IllegalStateException( "Double sided interior edges detected" ); } // Fast forward to the start of a potential colinear chain, since this edge may be in the middle of one while ( Math.abs( edge.toVector2d().normalize().cross( edge.getPrev().toVector2d().normalize() ) ) <= ANGLE_TOLERANCE ) { edge = edge.getNext(); } while ( !edges.contains( edge ) ) { HalfEdge next = edge.getNext(); final Vector2d a = edge.toVector2d().normalize(); final Vector2d b = next.toVector2d().normalize(); if ( Math.abs( a.cross( b ) ) <= ANGLE_TOLERANCE ) { // We can more or less unlink edges for free at this point // since we can guarantee each edge borders one exterior // and one interior region, and no two regions share an edge. final HalfEdge after = next.getNext(); // Unlink the next edge from after HalfEdge.splice( next.getSym(), after ); // Unlink this edge from the next edge HalfEdge.splice( edge.getSym(), next ); // Since we removed an edge entirely, check if it's still connected // to anything that's important, or if we should remove the // vertex it belongs to if ( next.getPrev() == next ) { vertices.remove( next.getOrigin() ); } else { // Set the next origin to something else next.getOrigin().setEdge( next.getPrev() ); // Unlink it from its own origin HalfEdge.splice( next, next.getSym().getNext() ); } // Unlink the next edge from the current vertex HalfEdge.splice( next, next.getSym().getNext() ); // Link this edge and the edge after next together HalfEdge.splice( edge.getSym(), after ); // Set the destination vertex edge.getSym().setOrigin( after.getOrigin() ); edge.getDest().update(); after.getOrigin().setEdge( after ); edges.add( next ); toRemove.add( next ); } else { edges.add( edge ); edge = edge.getNext(); } } } internalEdges.removeAll( toRemove ); return toRemove; } public Collection< EdgePolygon > mesh() { if ( state != MeshState.TRIANGULATION_READY ) { throw new IllegalStateException( "Mesh has not been split into regions!" ); } // Keep track of all edges that we've seen final Set< HalfEdge > scanned = new HashSet< HalfEdge >(); // Map each old vertex to a new vertex final Map< Vertex, Vertex > newVertices = new HashMap< Vertex, Vertex >(); // Keep track of all newly added interior edges final Collection< HalfEdge > edges = new HashSet< HalfEdge >(); // Get a collection of vertices that need to be sorted, again final Collection< Vertex > toSort = new HashSet< Vertex >(); /* * We now want to create a completely separate PSLG so that we don't modify * the original one. */ for ( final HalfEdge edge : interiorEdges ) { if ( scanned.add( edge ) ) { HalfEdge temp = edge; HalfEdge newEdge = null; do { if ( newEdge == null ) { newEdge = new HalfEdge(); HalfEdge.splice( newEdge, newEdge.getSym() ); newEdge.getSym().setOrigin( newEdge.getOrigin() ); } else { newEdge = newEdge.split(); } final Vertex tempVert = temp.getOrigin(); // Re-use a previously created vertex if there is one Vertex vert; if ( newVertices.containsKey( tempVert ) ) { vert = newVertices.get( tempVert ); newEdge.setOrigin( vert ); newEdge.getPrev().setOrigin( vert ); HalfEdge.splice( newEdge.getPrev(), vert.getEdge() ); toSort.add( vert ); } else { vert = newEdge.getOrigin(); newVertices.put( tempVert, vert ); vert.setPosition( tempVert.getPosition() ); } edges.add( newEdge ); } while ( scanned.add( temp = temp.getNext() ) ); } } toSort.parallelStream().forEach( v -> sort( v ) ); // Have a single set of edges final TreeSet< Vertex > vertices = new TreeSet< Vertex >( Mesh::compare ); vertices.addAll( newVertices.values() ); return partitionMonotone( vertices, edges ).parallelStream() .map( p -> { mergeColinearEdges( p.getVertices(), p.getEdges() ).forEach( e -> p.getVertices().remove( e.getOrigin() ) ); return p; } ) .map( Mesh::triangulate ) .flatMap( p -> p.parallelStream() ) .collect( Collectors.toSet() ); } private static Collection< EdgePolygon > partitionMonotone( final TreeSet< Vertex > vertices, final Collection< HalfEdge > interior ) { // Helper class to keep track of the immediate upper and lower // edges for a given vertex class MarkedEdge { final HalfEdge lower; final HalfEdge upper; MarkedEdge( final HalfEdge lower, final HalfEdge upper ) { this.lower = lower; this.upper = upper; } public boolean equals( MarkedEdge o ) { return o != null && lower == o.lower && upper == o.upper; } } // Keep track of edges from bottom top final SortedEdgeCollection< HalfEdge > edgeCollection = new SortedEdgeCollection< HalfEdge >( Mesh::greaterThanOrEqualTo ); // Keep track of all vertices that need to be linked with a left-going edge final Map< Vertex, MarkedEdge > leftMarked = new HashMap< Vertex, MarkedEdge >(); // Likewise, keep track of all vertices that need to be linked with a right-going edge final Map< Vertex, MarkedEdge > rightMarked = new HashMap< Vertex, MarkedEdge >(); // Keep track of all edges that we added so we can do stuff with them later final EdgeSet supportEdges = new EdgeSet(); // Keep track of which vertices were used as a pylon so that we // can sort them later, since we added at least one extra edge final Set< Vertex > toSort = new HashSet< Vertex >(); // Sweep each vertex and mark the upper and lower regions for ( final Vertex event : vertices ) { // Get the left and right wings final Collection< HalfEdge > rightGoing = new ArrayDeque< HalfEdge >(); final Collection< HalfEdge > leftGoing = new ArrayDeque< HalfEdge >(); for ( final HalfEdge edge : event ) { if ( isPositive( edge ) ) { rightGoing.add( edge ); } else { leftGoing.add( edge ); if ( !edgeCollection.remove( edge.getSym() ) ) { throw new IllegalStateException( "Edge not added to the edge collection!" ); } } } // Create a mark if the vertex has no left xor no right going edges // AKA, if it prevents a polygon from being monotone. // It should never have no left and no right edges, since that // would mean it is a vertex with no edges // If it has a left and right edge, then that means it's a normal // vertex and does not break monotony MarkedEdge eventMark = null; if ( leftGoing.isEmpty() ^ rightGoing.isEmpty() ) { HalfEdge edge = event.getEdge(); if ( !isPositive( edge ) ) { edge = edge.getSym(); } final HalfEdge lower = edgeCollection.searchLower( edge ); // Since we are dealing with simple polygons, we can guarantee that // if the lower edge is an interior edge, then the upper edge is // also an interior edge and this vertex is inside the polygon. if ( interior.contains( lower ) ) { // Get the upper edge final HalfEdge upper = edgeCollection.upper( lower ); // The current vertex must be between the upper and the lower // edge, and it cannot be part of either edge if ( upper == null ) { throw new IllegalStateException( "Lower does not have an upper edge!" ); } if ( !interior.contains( upper.getSym() ) ) { throw new IllegalStateException( "Polygon is not simple!" ); } else if ( upper.getOrigin() == event || lower.getOrigin() == event ) { throw new IllegalStateException( "Vertex is invalid!" ); } // Save this edge since it must be resolved eventually eventMark = new MarkedEdge( lower, upper ); } } else if ( rightGoing.isEmpty() && leftGoing.isEmpty() ) { throw new IllegalStateException( "Cannot have a vertex with no edges!" ); } // Scan all right marked vertices to see if it can be connected to this one for ( final Iterator< Entry< Vertex, MarkedEdge > > it = rightMarked.entrySet().iterator(); it.hasNext(); ) { final Entry< Vertex, MarkedEdge > entry = it.next(); final Vertex prev = entry.getKey(); final MarkedEdge marked = entry.getValue(); final HalfEdge lower = marked.lower; final HalfEdge upper = marked.upper; if ( upper.getDest() == event ) { // Is this vertex the left end of an upper edge? final HalfEdge edge = new HalfEdge(); edge.setOrigin( event ); edge.getSym().setOrigin( prev ); HalfEdge.splice( edge, upper.getSym() ); HalfEdge.splice( edge.getSym(), prev.getEdge() ); supportEdges.add( edge ); } else if ( lower.getDest() == event ) { // Is this vertex the left end of a lower edge? final HalfEdge edge = new HalfEdge(); edge.setOrigin( event ); edge.getSym().setOrigin( prev ); HalfEdge.splice( edge, lower.getNext() ); HalfEdge.splice( edge.getSym(), prev.getEdge() ); supportEdges.add( edge ); } else if ( marked.equals( eventMark ) ) { // If a right-marked vertex is between the same // upper/lower region as this vertex, then // that must be because this vertex is left marked if ( !leftGoing.isEmpty() ) { throw new IllegalStateException( "Vertex is not left-marked!" ); } // Are there two marked vertices between the same edges? final HalfEdge edge = new HalfEdge(); edge.setOrigin( event ); edge.getSym().setOrigin( prev ); HalfEdge.splice( edge, event.getEdge() ); HalfEdge.splice( edge.getSym(), prev.getEdge() ); // Add the edge to the left-going for this vertex // so it can be considered "fixed" leftGoing.add( edge ); supportEdges.add( edge ); // Technically both vertices need to be sorted toSort.add( event ); toSort.add( prev ); // Can break out of this loop early, since this vertex must have // been a left-marked vertex, and therefore cannot fulfill any of the // conditions above anymore. it.remove(); break; } else { continue; } // Remove the right marked vertex since it no longer breaks monotony it.remove(); } if ( eventMark != null ) { // Since the mark may have had left going edges added, it may be // resolved. Only add it if it does not have any left or right going // edges. if ( leftGoing.isEmpty() ) { // Process this after we are done processing all right-marked vertices leftMarked.put( event, eventMark ); } else if ( rightGoing.isEmpty() ) { rightMarked.put( event, eventMark ); // We are queuing the event for sorting now // which is why we didn't queue it for sorting in the earlier // for loop. It would have been queued already anyways. toSort.add( event ); } } // Add all right going edges to the edge collection, // now that we have finally finished adding new edges. for ( final HalfEdge edge : rightGoing ) { if ( isPositive( edge ) ) { edgeCollection.insert( edge ); } else { throw new IllegalStateException( "Edge suddenly changed sign!" ); } } } // All right marked vertices MUST be resolved by now. if ( !rightMarked.isEmpty() ) { throw new IllegalStateException( "Unresolved right marked vertices!" ); } // Reverse sweep the vertices and connect any left marked vertices for ( final Entry< Vertex, MarkedEdge > entry : leftMarked.entrySet() ) { final Vertex vertex = entry.getKey(); final MarkedEdge mark = entry.getValue(); final HalfEdge lower = mark.lower; final HalfEdge upper = mark.upper; // Get whichever vertex is further to the left/down Vertex lesser = upper.getOrigin(); if ( compare( lesser, lower.getOrigin() ) > 0 ) { lesser = lower.getOrigin(); } // Get the largest range of vertices that we need to check final Set< Vertex > checkSet = vertices.descendingSet() .tailSet( vertex, false ) .headSet( lesser, true ); // Find the next previous vertex that: // - Is a marked vertex with the same upper and lower edges // - Is the origin of the upper or lower edge for ( final Vertex prev : checkSet ) { // Do a similar check as with the right marked vertices if ( lower.getOrigin() == prev ) { final HalfEdge edge = new HalfEdge(); edge.setOrigin( prev ); edge.getSym().setOrigin( vertex ); HalfEdge.splice( edge, lower ); HalfEdge.splice( edge.getSym(), vertex.getEdge() ); supportEdges.add( edge ); } else if ( upper.getOrigin() == prev ) { HalfEdge edge = new HalfEdge(); edge.setOrigin( prev ); edge.getSym().setOrigin( vertex ); HalfEdge.splice( edge, upper.getSym().getNext() ); HalfEdge.splice( edge.getSym(), vertex.getEdge() ); supportEdges.add( edge ); } else if ( mark.equals( leftMarked.get( prev ) ) ) { // Need to sort prev, since we can't guarantee it's being // inserted in the correct position final HalfEdge edge = new HalfEdge(); edge.setOrigin( prev ); edge.getSym().setOrigin( vertex ); HalfEdge.splice( edge, prev.getEdge() ); HalfEdge.splice( edge.getSym(), vertex.getEdge() ); supportEdges.add( edge ); toSort.add( prev ); } else { continue; } // Only need to merge with the first vertex found to maintain monotony break; } toSort.add( vertex ); } // Sort the vertices we have to sort, now that we're done toSort.parallelStream().forEach( v -> sort( v ) ); // In preparation for the generation of new regions, // we need to duplicate each support edge so that the // upper and lower polygons can have their edge. for ( final HalfEdge edge : supportEdges ) { final HalfEdge copy = new HalfEdge(); copy.setOrigin( edge.getOrigin() ); copy.getSym().setOrigin( edge.getDest() ); HalfEdge.splice( copy, edge ); HalfEdge.splice( copy.getSym(), edge.getNext() ); interior.add( copy ); interior.add( edge.getSym() ); } // Now that we've split the polygon into monotone regions, we must // also return a collection of the newly created polygon, since each // monotone region is its own separate polygon // That means each polygon should not share any vertices or edges, // which is perfect for us so we can triangulate each polygon in parallel final Collection< EdgePolygon > newPolygons = new ArrayDeque< EdgePolygon >(); // Keep track of all edges that we've seen final Set< HalfEdge > scanned = new HashSet< HalfEdge >(); for ( final HalfEdge edge : interior ) { if ( scanned.add( edge ) ) { // We have not looked at this edge yet, so loop over it // and create a new polygon from it final EdgePolygon newPoly = new EdgePolygon(); HalfEdge temp = edge; do { // As we iterate over each edge, if the origin has more than // 2 edges, then we need to split the vertex. if ( temp.getPrev().getPrev() != temp ) { temp.getOrigin().setEdge( temp.getPrev().getPrev() ); HalfEdge.splice( temp.getPrev(), temp.getSym().getNext() ); // Set their origins to a new vertex final Vertex vertex = new Vertex( temp, temp.getOrigin().getPosition() ); temp.setOrigin( vertex ); temp.getPrev().setOrigin( vertex ); } newPoly.addEdge( temp ); } while ( scanned.add( temp = temp.getNext() ) ); newPolygons.add( newPoly ); } } return newPolygons; } private static Collection< EdgePolygon > 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 // Computation Geometry Algorithms and Applications 3rd Ed. final Collection< Vertex > toSort = new HashSet< Vertex >(); if ( polygon.getEdges().size() == 3 ) { return Arrays.asList( polygon ); } final Queue< HalfEdge > edges = new PriorityQueue< HalfEdge >( ( a, b ) -> { return compare( a.getOrigin(), b.getOrigin() ); } ); edges.addAll( polygon.getEdges() ); final Stack< HalfEdge > stack = new Stack< HalfEdge >(); // Add the first two vertices/edges stack.add( edges.poll() ); stack.add( edges.poll() ); HalfEdge prev = stack.peek(); while ( edges.size() > 1 ) { final HalfEdge edge = edges.poll(); if ( isPositive( edge ) ^ isPositive( stack.peek() ) ) { // Insert an edge from the current event to each vertex in the stack HalfEdge current = stack.pop(); while ( !stack.isEmpty() ) { final HalfEdge newEdge = new HalfEdge(); newEdge.setOrigin( edge.getOrigin() ); newEdge.getSym().setOrigin( current.getOrigin() ); HalfEdge.splice( newEdge, edge ); HalfEdge.splice( newEdge.getSym(), current ); polygon.addEdge( newEdge ); toSort.add( edge.getOrigin() ); toSort.add( current.getOrigin() ); current = stack.pop(); } stack.push( prev ); } else { HalfEdge current = stack.pop(); final boolean isPositive = isPositive( edge ); HalfEdge leftEdge = isPositive ? edge.getPrev() : edge; while ( !stack.isEmpty() ) { final HalfEdge next = stack.peek(); final Vector2d diagonal = next.getOrigin().getPosition().subtracted( edge.getOrigin().getPosition() ); final double cross = diagonal.normalize().cross( leftEdge.toVector2d().normalize() ); // Check if the diagonal is inside the polygon final boolean isInside = isPositive ? cross > ANGLE_TOLERANCE : cross < - ANGLE_TOLERANCE; if ( isInside ) { final HalfEdge newEdge = new HalfEdge(); newEdge.setOrigin( edge.getOrigin() ); newEdge.getSym().setOrigin( next.getOrigin() ); HalfEdge.splice( newEdge, edge ); HalfEdge.splice( newEdge.getSym(), next ); toSort.add( edge.getOrigin() ); toSort.add( next.getOrigin() ); polygon.addEdge( newEdge ); leftEdge = newEdge; current = stack.pop(); } else { break; } } stack.push( current ); } stack.push( edge ); prev = edge; } final HalfEdge last = edges.poll(); stack.pop(); while ( stack.size() > 1 ) { final HalfEdge popped = stack.pop(); final HalfEdge newEdge = new HalfEdge(); newEdge.setOrigin( last.getOrigin() ); newEdge.getSym().setOrigin( popped.getOrigin() ); HalfEdge.splice( newEdge, last ); HalfEdge.splice( newEdge.getSym(), popped ); polygon.addEdge( newEdge ); toSort.add( last.getOrigin() ); toSort.add( popped.getOrigin() ); } // Lazy solution, just sort the vertices // Eventually we will remove this toSort.parallelStream().forEach( v -> sort( v ) ); // TODO Convert to triangles or something? final Collection< EdgePolygon > polygons = new ArrayDeque< EdgePolygon >(); final Set< HalfEdge > scanned = new HashSet< HalfEdge >(); for ( final HalfEdge edge : polygon.getEdges() ) { if ( scanned.add( edge ) ) { HalfEdge temp = edge; EdgePolygon poly = new EdgePolygon(); do { poly.addEdge( temp ); } while ( scanned.add( temp = temp.getNext() ) ); if ( poly.getVertices().size() != poly.getEdges().size() ) { throw new IllegalStateException( "Polygon has inconsistent edges/vertices!" ); } if ( poly.getVertices().size() != 3 ) { throw new IllegalStateException( "Not a triangle! " + poly.getVertices().size() ); } polygons.add( poly ); } } return polygons; } /* * Assumes region a and b overlap somewhere on the x axis * Assume both edges do not intersect, and are nonzero and positive * Assume that a and b cannot both be vertical and share the same origin */ private static boolean greaterThanOrEqualTo( final HalfEdge a, final HalfEdge b ) { // Assume each region is marked by an upper edge going from right to left, up to down if ( a.isZero() || b.isZero() ) { throw new IllegalStateException( "Zero length edge detected" ); } if ( !( isPositive( a ) && isPositive( b ) ) ) { throw new IllegalStateException( "Negative edge detected" ); } // a1 < a2 final Vector2d a1 = a.getOrigin().getPosition(); final Vector2d a2 = a.getDest().getPosition(); // b1 < b2 final Vector2d b1 = b.getOrigin().getPosition(); final Vector2d b2 = b.getDest().getPosition(); final int compared = compare( a.getOrigin(), b.getOrigin() ); if ( compared == 0 ) { return b2.subtracted( b1 ).cross( a2.subtracted( a1 ) ) >= 0; } else if ( compared < 0 ) { return b1.subtracted( a1 ).cross( a2.subtracted( a1 ) ) >= 0; } else { return b2.subtracted( b1 ).cross( a1.subtracted( b1 ) ) >= 0; } } /** * Determines if the edge's origin is less than its destination * * @param edge * @return */ public static boolean isPositive( HalfEdge edge ) { return compare( edge.getOrigin(), edge.getDest() ) <= 0; } private static int compare( final Vertex a, final Vertex b ) { if ( a == b ) { return 0; } final Vector2d aPos = a.getPosition(); final Vector2d bPos = b.getPosition(); final double x = compareX( aPos, bPos ); if ( x == 0 ) { return Double.compare( compareY( aPos, bPos ), 0 ); } return Double.compare( x, 0 ); } private static double compareX( final Vector2d a, final Vector2d b ) { return a.getX() - b.getX(); } private static double compareY( final Vector2d a, final Vector2d b ) { return a.getY() - b.getY(); } // Check if 2 vertices are 'close enough' private static boolean isSimilar( Vertex a, Vertex b ) { return isSimilar( a.getPosition(), b.getPosition() ); } private static boolean isSimilar( Vector2d a, Vector2d b ) { // If for some reason the sqrt is slow, then it is // probably OK using an AABB for the tolerance test return Math.abs( a.distance( b ) ) <= VERTEX_TOLERANCE; } /* * Represents an intersection */ private static abstract class Intersection { } /* * When 2 edges both intersect somewhere in the middle */ private static class IntersectionEdgeToEdge extends Intersection { private final Vector2d point; IntersectionEdgeToEdge( Vector2d point ) { this.point = point; } Vector2d getPoint() { return point; } } /* * When 2 edges are colinear, and do not share the same endpoint */ private static class IntersectionColinear extends Intersection { private final HalfEdge shorter; private final HalfEdge longer; IntersectionColinear( HalfEdge shorter, HalfEdge longer ) { this.shorter = shorter; this.longer = longer; } public HalfEdge getShorterEdge() { return shorter; } public HalfEdge getLongerEdge() { return longer; } } public static class EdgePolygon { private Set< HalfEdge > edges = new HashSet< HalfEdge >(); private Set< Vertex > vertices = new HashSet< Vertex >(); private void addEdge( HalfEdge edge ) { edges.add( edge ); vertices.add( edge.getOrigin() ); } public Set< HalfEdge > getEdges() { return edges; } public Set< Vertex > getVertices() { return vertices; } } protected enum MeshState { DIRTY, SIMPLIFIED, TRIANGULATION_READY } }