Files
mc-mesh/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/Mesh.java
BananaPuncher714 0f430e7a31 Fixed monotone partioning and finished triangulation algorithm
Removed a lot of debug code
Added comments
Fixed partition monotone not setting the vertex after unlinking edges
Added some better java awt stuff
Added a method to copy meshes
2025-05-06 00:11:09 -04:00

1548 lines
66 KiB
Java

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
}
}