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
1548 lines
66 KiB
Java
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
|
|
}
|
|
}
|