From 9e010d3fe1b42fff009f4f4a4638a744139a3126 Mon Sep 17 00:00:00 2001 From: BananaPuncher714 Date: Sat, 17 May 2025 02:41:02 -0400 Subject: [PATCH] Added collinear vertex detection --- .gitignore | 1 + .../bananapuncher714/mesh/Mesh.java | 978 ++++++++++++++++-- .../minietest/MeshingTest2.java | 40 +- 3 files changed, 923 insertions(+), 96 deletions(-) diff --git a/.gitignore b/.gitignore index e8c9928..fd9c5d0 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ target .settings chunks new_chunks +images \ No newline at end of file diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/Mesh.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/Mesh.java index 288dcff..32bd4a1 100644 --- a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/Mesh.java +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/Mesh.java @@ -35,7 +35,7 @@ import com.aaaaahhhhhhh.bananapuncher714.mesh.region.RegionRule; * 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 + * - Merging collinear 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 @@ -50,11 +50,11 @@ import com.aaaaahhhhhhh.bananapuncher714.mesh.region.RegionRule; * 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. + * does not move any vertices. This step also removes merges collinear edges where possible. * * 3. Meshing and triangulating the resulting polygons. Firstly, a copy of the PSLG is generated * since this step adds edges and vertices to the PSLG, which may be undesirable for reusability. - * Then, we remove colinear edges which may have been generated as a result of the monotone partitioning. + * Then, we remove collinear 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. @@ -64,7 +64,7 @@ import com.aaaaahhhhhhh.bananapuncher714.mesh.region.RegionRule; * 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). + * The time complexity for the entire algorithm should be roughly O(n^2logn). * * TODO Remove guaranteed checks or add some compile time thing to remove * @@ -72,18 +72,22 @@ import com.aaaaahhhhhhh.bananapuncher714.mesh.region.RegionRule; */ public class Mesh< T extends Region > { public static final double VERTEX_TOLERANCE = 1e-7; - public static final double ANGLE_TOLERANCE = Math.toRadians( 0.0001 ); + public static final double ANGLE_TOLERANCE = Math.sin( 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 > >(); + // Positive interior edges protected Set< HalfEdge > interiorEdges = new HashSet< HalfEdge >(); protected final Supplier< T > defaultRegionSupplier; protected MeshState state = MeshState.TRIANGULATION_READY; + // TODO Remove + public Collection< Chain > chains; + public Mesh( final Supplier< T > defaultRegionSupplier ) { this.defaultRegionSupplier = defaultRegionSupplier; } @@ -145,48 +149,50 @@ public class Mesh< T extends Region > { for ( Vertex vertex : vertices ) { for ( final HalfEdge edge : vertex ) { if ( scanned.add( edge ) ) { - HalfEdge temp = edge; + final Vertex originalVert = edge.getOrigin(); - HalfEdge newEdge = null; - do { - if ( newEdge == null ) { - newEdge = new HalfEdge(); - HalfEdge.splice( newEdge, newEdge.getSym() ); - newEdge.getSym().setOrigin( newEdge.getOrigin() ); - } else { - newEdge = newEdge.split(); - } + HalfEdge newEdge = new HalfEdge(); + // Re-use a previously created vertex if there is one + if ( newVertices.containsKey( originalVert ) ) { + Vertex vert = newVertices.get( originalVert ); - final Vertex tempVert = temp.getOrigin(); + newEdge.setOrigin( vert ); - // 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 ); - } + HalfEdge.splice( newEdge.getPrev(), vert.getEdge() ); + } else { + Vertex vert = newEdge.getOrigin(); + newVertices.put( originalVert, vert ); + + vert.setPosition( originalVert.getPosition() ); - if ( interiorEdges.contains( temp ) ) { - copy.interiorEdges.add( temp ); - } - if ( interiorEdges.contains( temp.getSym() ) ) { - copy.interiorEdges.add( temp.getSym() ); - } + copy.vertices.add( vert ); + } + + final Vertex destinationVert = edge.getDest(); + if ( newVertices.containsKey( destinationVert ) ) { + Vertex vert = newVertices.get( destinationVert ); - copy.rules.put( newEdge, rules.get( temp ) ); - copy.rules.put( newEdge.getSym(), rules.get( temp.getSym() ) ); - } while ( scanned.add( temp = temp.getNext() ) ); + newEdge.getSym().setOrigin( vert ); + + HalfEdge.splice( newEdge.getSym(), vert.getEdge() ); + } else { + Vertex vert = newEdge.getDest(); + newVertices.put( destinationVert, vert ); + + vert.setPosition( destinationVert.getPosition() ); + + copy.vertices.add( vert ); + } + + if ( interiorEdges.contains( edge ) ) { + copy.interiorEdges.add( edge ); + } + if ( interiorEdges.contains( edge.getSym() ) ) { + copy.interiorEdges.add( edge.getSym() ); + } + + copy.rules.put( newEdge, rules.get( edge ) ); + copy.rules.put( newEdge.getSym(), rules.get( edge.getSym() ) ); } } } @@ -454,8 +460,8 @@ public class Mesh< T extends Region > { origin.update(); vertices.add( origin ); - } else if ( intersection instanceof IntersectionColinear ) { - final IntersectionColinear intCol = ( IntersectionColinear ) intersection; + } else if ( intersection instanceof IntersectionCollinear ) { + final IntersectionCollinear intCol = ( IntersectionCollinear ) intersection; final HalfEdge shorter = intCol.getShorterEdge(); HalfEdge longer = intCol.getLongerEdge(); @@ -495,7 +501,7 @@ public class Mesh< T extends Region > { */ 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 + // This assumes that if a and b are collinear, then they _must_ share // the same origin, since we are traversing through the vertices // starting with the smallest. if ( a.getOrigin() == b.getOrigin() ) { @@ -505,14 +511,14 @@ public class Mesh< T extends Region > { final Vector2d s = b.toVector2d().normalize(); final double angle = r.cross( s ); - // Ignore edges that are not colinear + // Ignore edges that are not collinear if ( Math.abs( angle ) <= ANGLE_TOLERANCE ) { // Compare which edge is shorter if ( r.lengthSquared() < s.lengthSquared() ) { - return Optional.of( new IntersectionColinear( a, b ) ); + return Optional.of( new IntersectionCollinear( a, b ) ); } else { - return Optional.of( new IntersectionColinear( b, a ) ); + return Optional.of( new IntersectionCollinear( b, a ) ); } } } @@ -741,8 +747,8 @@ public class Mesh< T extends Region > { interiorBelowEdges.remove( edge ); } - // Merge any colinear edges before returning - mergeColinearEdges( keep, interiorAboveEdges ).forEach( e -> { + // Merge any collinear edges before returning + mergeAdjacentCollinearEdges( keep, interiorAboveEdges ).forEach( e -> { rules.remove( e ); rules.remove( e.getSym() ); } ); @@ -766,25 +772,25 @@ public class Mesh< T extends Region > { 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 ); + // 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 ); - } - } ); - + 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 ); + } + } ); + + // Only need to re-organize the edges if there are 3 or more + if ( edges.size() > 2 ) { for ( int i = 0; i < edges.size(); ++i ) { // Get this edge and the next edge final HalfEdge e1 = edges.get( i ); @@ -796,14 +802,15 @@ public class Mesh< T extends Region > { e2.getSym().setNext( e1 ); } } - - vertex.setEdge( edges.get( 0 ) ); } + + // Set the edge of the vertex to the least edge + vertex.setEdge( edges.get( 0 ) ); return edges; } - private static Collection< HalfEdge > mergeColinearEdges( final Collection< Vertex > vertices, final Collection< HalfEdge > internalEdges ) { + private static Collection< HalfEdge > mergeAdjacentCollinearEdges( final Collection< Vertex > vertices, final Collection< HalfEdge > internalEdges ) { final Collection< HalfEdge > toRemove = new ArrayDeque< HalfEdge >(); final Set< HalfEdge > edges = new HashSet< HalfEdge >(); @@ -819,7 +826,7 @@ public class Mesh< T extends Region > { 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 + // Fast forward to the start of a potential collinear 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(); } @@ -881,6 +888,8 @@ public class Mesh< T extends Region > { return toRemove; } + // TODO Provide triangles + // and preferrably a simple polygon public Collection< EdgePolygon > mesh() { if ( state != MeshState.TRIANGULATION_READY ) { throw new IllegalStateException( "Mesh has not been split into regions!" ); @@ -894,9 +903,6 @@ public class Mesh< T extends Region > { // 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. @@ -926,8 +932,6 @@ public class Mesh< T extends Region > { newEdge.getPrev().setOrigin( vert ); HalfEdge.splice( newEdge.getPrev(), vert.getEdge() ); - - toSort.add( vert ); } else { vert = newEdge.getOrigin(); newVertices.put( tempVert, vert ); @@ -940,15 +944,34 @@ public class Mesh< T extends Region > { } } - 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() ); + // Sort each vertex so that the edge they are pointing to is the least edge + vertices.parallelStream().forEach( v -> sort( v ) ); + + try { + final Collection< Chain > chains = reduceCollinear2( vertices, edges ); + this.chains = new ArrayDeque< Chain >(); + for ( Chain chain : chains ) { + List< HalfEdge > newEdges = new ArrayList< HalfEdge >(); + for ( HalfEdge e : chain.links ) { + HalfEdge edge = new HalfEdge(); + edge.getOrigin().setPosition( e.getOrigin().getPosition() ); + edge.getDest().setPosition( e.getDest().getPosition() ); + newEdges.add( edge ); + } + this.chains.add( new Chain( newEdges ) ); + + } + } catch ( IllegalStateException e ) { + e.printStackTrace(); + } + return partitionMonotone( vertices, edges ).parallelStream() .map( p -> { - mergeColinearEdges( p.getVertices(), p.getEdges() ).forEach( e -> p.getVertices().remove( e.getOrigin() ) ); + mergeAdjacentCollinearEdges( p.getVertices(), p.getEdges() ).forEach( e -> p.getVertices().remove( e.getOrigin() ) ); return p; } ) @@ -957,6 +980,791 @@ public class Mesh< T extends Region > { .collect( Collectors.toSet() ); } + public static class Chain { + List< HalfEdge > links; + + Chain( final Collection< HalfEdge > links ) { + this.links = new ArrayList< HalfEdge >( links ); + } + + public Vertex getOrigin() { + return links.get( 0 ).getOrigin(); + } + + public Vertex getDest() { + return links.get( 2 ).getOrigin(); + } + + public List< HalfEdge > getLinks() { + return links; + } + } + + /* + * This algorithm revolves around first generating chains, then sorting which chains and chain links to keep. + * + * A chain is a collection of chain links. + * A chain link is 3 collinear vertices. + * 2 chain links are considered linked if the first link's 2 last vertices are the same as the second link's 2 first vertices. + * For example, given collinear vertices A, B, C and D, chain links u and v are linked together if + * chain link u has vertices A, B, C, and if chain link v has vertices B, C and D. No more than 2 chain links + * can be linked together at one end. Two chains are considered crossing if a link from both chains intersects each other. + * At most 3 chain links can intersect another chain link, since an intersection only occurs if the segment formed by 2 + * vertices from one chain link intersects another segment formed by 2 vertices from a different chain link. + * + * The complexity comes from analyzing which combinations of chain links provides the greatest reduction of vertices. + */ + private static Collection< Chain > reduceCollinear2( final TreeSet< Vertex > vertices, final Collection< HalfEdge > interior ) { + final Vector2d CROSS = new Vector2d( 1, 0 ); + // Set maximum and minimum cross values that won't occur naturally + final double MAX_CROSS = 1.1; + final double MIN_CROSS = -1.1; + + /** + * A visibility region representing the area that is visible to a vertex. + * Used for determining if a vertex is visible. + * + * To check for collinearity, calculate the new cross product value for + * a new vertex, and see if it would have changed from the previous value. + * If not, then the original vertex, the previous vertex, and the new vertex + * must be collinear. + * + * We use a map for holding collinear vertices, but this is so if the upper + * and lower edges of our visibility region are the same, then they both + * contribute to the same edge list. + * + * Our link map should never have more than 2 entries at any given time, and + * the links should never have more than 3 vertices. + */ + class VisibilityRegion { + double lower; + double upper; + final HalfEdge lowerEdge; + final HalfEdge upperEdge; + + Map< Double, List< HalfEdge > > links = new HashMap< Double, List< HalfEdge > >(); + + VisibilityRegion( final HalfEdge lowerEdge, final HalfEdge upperEdge ) { + // Because we don't scan the vertex for this visibility region since + // it is the start of this region, we need to have two edges to + // represent this vertex, one for the upper and one for the lower + // bounds. + this.lowerEdge = lowerEdge; + this.upperEdge = upperEdge; + + // Default to the minimum and maximum values. + this.lower = MIN_CROSS; + this.upper = MAX_CROSS; + } + } + + // Keep track of all completed chain links that we have + final Collection< Chain > chains = new HashSet< Chain >(); + + /** + * A partition represents a monotone section of a polygon at the current + * scan line. At any given time, a vertical scan line which passes through + * the PSLG can be divided into a series of monotone partitions which are + * bounded by two interior edges. + * + * This can also be used to keep track of visibility regions for vertices, + * since it roughly limits the upper and lower bound that a vertex can see. + * + * Any vertices with more than one right edge signals for the creation of one + * or more partitions, starting from the current lower/upper/right edges. + * + * Upon encountering a left marked vertex(one without any left edges), the + * current partition can be split into two. + * + * Additionally, when given a right marked vertex(one without any right edges), + * the upper and lower partition and all related properties should be merged. + * + * A partition ends when the upper and lower edges have the same destination. + */ + class Partition { + HalfEdge lower; + HalfEdge upper; + + // Keep track of the visibility regions for each vertex. Normally + // each vertex should have one visibility region, but we must be + // able to provide for multiple because two or more partitions + // may be merged into a single partition, and each partition + // may have a visibility region for the same vertex. + Map< Vertex, Collection< VisibilityRegion > > regionMap = new HashMap< Vertex, Collection< VisibilityRegion > >(); + + Partition( final HalfEdge lower, final HalfEdge upper ) { + this.lower = lower; + this.upper = upper; + } + + void incrementUpper( final HalfEdge edge ) { + final Vertex event = edge.getOrigin(); + for ( final Iterator< Entry< Vertex, Collection< VisibilityRegion > > > it = regionMap.entrySet().iterator(); it.hasNext(); ) { + final Entry< Vertex, Collection< VisibilityRegion > > entry = it.next(); + final Vertex vert = entry.getKey(); + final Collection< VisibilityRegion > regions = entry.getValue(); + + // Get the cross value for this vertex to the current event vertex + final Vector2d toEvent = event.getPosition().subtracted( vert.getPosition() ).normalize(); + final double upperCross = CROSS.cross( toEvent ); + final double upperAngle = Math.asin( upperCross ); + + // Check over each visibility region and update the upper cross value + for ( final Iterator< VisibilityRegion > regionIt = regions.iterator(); regionIt.hasNext(); ) { + final VisibilityRegion region = regionIt.next(); + if ( region.upper != MAX_CROSS && Math.abs( upperAngle - Math.asin( region.upper ) ) < ANGLE_TOLERANCE ) { + // Make sure the region upper value has been initialized, before checking + // if this vertex is collinear with the previous vertex + + final List< HalfEdge > links = region.links.get( region.upper ); + + if ( links == null ) { + throw new IllegalStateException( "No links found for scanned vertex!" ); + } else if ( links.size() < 2 ) { + throw new IllegalStateException( "Missing links!" ); + } + + // Only consider links if there are not yet 3 + // Otherwise, do nothing, and ignore any additional vertices + // since they will be captured by another vertex + if ( links.size() < 3 ) { + // Add a vertex because this is the last vertex in the link + // and we don't really care about what comes after + links.add( edge ); + + // TODO Check for intersections + + final Chain chain = new Chain( links ); + chains.add( chain ); + } + + } else if ( upperCross < region.upper ) { + // Is the new cross value less than the previous cross value? + + // Get the angle difference between the region's upper and lower bounds + // but set it to -1 if it the lower region has not been initialized + final double angleDifference = region.lower == MIN_CROSS ? -1 : ( Math.asin( region.lower ) - upperAngle ); + + if ( Math.abs( angleDifference ) <= ANGLE_TOLERANCE ) { + // The event vertex is not collinear with the previous upper + // vertex, but it _is_ collinear with the previous lower vertex! + + // Remove the previous links + region.links.remove( region.upper ); + + // There should be exactly 1 link, the lower region's link + if ( region.links.size() != 1 ) { + throw new IllegalStateException( "Dangling link!" ); + } + + region.upper = region.lower; + + final List< HalfEdge > links = region.links.get( region.upper ); + + if ( links == null ) { + throw new IllegalStateException( "No links found for scanned vertex!" ); + } else if ( links.size() < 2 ) { + throw new IllegalStateException( "Missing links!" ); + } + + // Only consider links if there are not yet 3 + if ( links.size() < 3 ) { + // Add a vertex because this is the last vertex in the link + // and we don't really care about what comes after + links.add( edge ); + + // TODO Check for intersections + + final Chain chain = new Chain( links ); + chains.add( chain ); + + // Remove this vertex since we know the upper and lower + // bounds have already converged, and met at this collinear + // vertex. + regionIt.remove(); + } else { + throw new IllegalStateException( "Region was not previously removed!" ); + } + } else if ( angleDifference > ANGLE_TOLERANCE ) { + // If the upper angle is less than the lower angle, then we know + // that the region is no longer visible to this vertex + regionIt.remove(); + } else { + if ( region.upper != MAX_CROSS ) { + final List< HalfEdge > prevLinks = region.links.get( region.upper ); + if ( prevLinks.size() < 2 ) { + throw new IllegalStateException( "Missing links!" ); + } + + // Remove the previous data since we do not need it anymore. + // Not really necessary to manually remove it, since we will + // never fetch the data again, but we know that it becomes + // useless as of right now, so we might as well as remove it + region.links.remove( region.upper ); + + // As of right now there should only be 1 or 0 links + if ( region.links.size() > 1 ) { + throw new IllegalStateException( "Too many links!" ); + } + } + + // This vertex is not collinear, but it is also not less + // than the lower bound. + region.upper = upperCross; + + // New cross value for this vertex, so create a new queue + // Also add in the original vertex + final List< HalfEdge > links = new ArrayList< HalfEdge >(); + links.add( vert.getEdge() ); + links.add( edge ); + if ( region.links.put( region.upper, links ) != null ) { + throw new IllegalStateException( "Cross value already used!" ); + } + } + + /* + * For each vertex, we only care about collinear chains of exactly + * 3 vertices. We must also know which collinear links intersect + * with our given 3-link. To detect collinear chains is quite + * simple: if the upper or lower cross is within ANGLE_TOLERANCE of + * the previous one. If so, then we should mark it as COLLINEAR, and + * take note of the 3 vertices. Then, once we have found all vertices + * for which the given vertex forms a 3-chain, mark the upper edge + * of this partition with the intersections. + * + * Upon encountering another intersection, we must perform the + * actual intersection determination check. But before that, + * look through all the intersection marks. Two intersections + * have the potential to be intersections if and only if: + * - The earlier intersection's destination be greater than + * the new intersection's origin + * + * + */ + } + } + + if ( regions.isEmpty() ) { + // Remove this vertex if it no longer has any visible regions + it.remove(); + } + } + } + + // More or less similar to incrementUpper, just mirrored + void incrementLower( final HalfEdge edge ) { + final Vertex event = edge.getOrigin(); + for ( final Iterator< Entry< Vertex, Collection< VisibilityRegion > > > it = regionMap.entrySet().iterator(); it.hasNext(); ) { + final Entry< Vertex, Collection< VisibilityRegion > > entry = it.next(); + final Vertex vert = entry.getKey(); + final Collection< VisibilityRegion > regions = entry.getValue(); + + // Get the cross value for this vertex to the current event vertex + final Vector2d toEvent = event.getPosition().subtracted( vert.getPosition() ).normalize(); + final double lowerCross = CROSS.cross( toEvent ); + final double lowerAngle = Math.asin( lowerCross ); + + // Check over each visibility region and update the upper cross value + for ( final Iterator< VisibilityRegion > regionIt = regions.iterator(); regionIt.hasNext(); ) { + final VisibilityRegion region = regionIt.next(); + if ( region.lower != MAX_CROSS && Math.abs( lowerAngle - Math.asin( region.lower ) ) < ANGLE_TOLERANCE ) { + // Make sure the region lower value has been initialized, before checking + // if this vertex is collinear with the previous vertex + + final List< HalfEdge > links = region.links.get( region.lower ); + + if ( links == null ) { + throw new IllegalStateException( "No links found for scanned vertex!" ); + } else if ( links.size() < 2 ) { + throw new IllegalStateException( "Missing links!" ); + } + + // Only consider links if there are not yet 3 + // Otherwise, do nothing, and ignore any additional vertices + // since they will be captured by another vertex + if ( links.size() < 3 ) { + // Add a vertex because this is the last vertex in the link + // and we don't really care about what comes after + links.add( edge ); + + // TODO Check for intersections + + final Chain chain = new Chain( links ); + chains.add( chain ); + } + } else if ( lowerCross > region.lower ) { + // Is the new cross value greater than the previous cross value? + + // Get the angle difference between the region's upper and lower bounds + // but set it to -1 if it the upper region has not been initialized + final double angleDifference = region.upper == MAX_CROSS ? -1 : ( lowerAngle - Math.asin( region.upper ) ); + + if ( Math.abs( angleDifference ) <= ANGLE_TOLERANCE ) { + // The event vertex is not collinear with the previous upper + // vertex, but it _is_ collinear with the previous upper vertex! + + // Remove the previous links + region.links.remove( region.lower ); + + // There should be exactly 1 link, the lower region's link + if ( region.links.size() != 1 ) { + throw new IllegalStateException( "Dangling link!" ); + } + + region.lower = region.upper; + + final List< HalfEdge > links = region.links.get( region.lower ); + + if ( links == null ) { + throw new IllegalStateException( "No links found for scanned vertex!" ); + } else if ( links.size() < 2 ) { + throw new IllegalStateException( "Missing links!" ); + } + + // Only consider links if there are not yet 3 + if ( links.size() < 3 ) { + // Add a vertex because this is the last vertex in the link + // and we don't really care about what comes after + links.add( edge ); + + // TODO Check for intersections + + final Chain chain = new Chain( links ); + chains.add( chain ); + + // Remove this vertex since we know the upper and lower + // bounds have already converged, and met at this collinear + // vertex. + regionIt.remove(); + } else { + throw new IllegalStateException( "Region was not previously removed!" ); + } + } else if ( angleDifference > ANGLE_TOLERANCE ) { + // If the upper angle is less than the lower angle, then we know + // that the region is no longer visible to this vertex + regionIt.remove(); + } else { + if ( region.lower != MIN_CROSS ) { + final List< HalfEdge > prevLinks = region.links.get( region.lower ); + if ( prevLinks.size() < 2 ) { + throw new IllegalStateException( "Missing links!" ); + } + + // Remove the previous data since we do not need it anymore. + // Not really necessary to manually remove it, since we will + // never fetch the data again, but we know that it becomes + // useless as of right now, so we might as well as remove it + region.links.remove( region.lower ); + + // As of right now there should only be 1 or 0 links + if ( region.links.size() > 1 ) { + throw new IllegalStateException( "Too many links!" ); + } + } + + // This vertex is not collinear, but it is also not greater + // than the upper bound. + region.lower = lowerCross; + + // New cross value for this vertex, so create a new queue + // Also add in the original vertex + final List< HalfEdge > links = new ArrayList< HalfEdge >(); + links.add( vert.getEdge() ); + links.add( edge ); + if ( region.links.put( region.lower, links ) != null ) { + throw new IllegalStateException( "Cross value already used!" ); + } + } + } + } + + if ( regions.isEmpty() ) { + // Remove this vertex if it no longer has any visible regions + it.remove(); + } + } + } + + void add( final Vertex vertex, final VisibilityRegion region ) { + Collection< VisibilityRegion > regions; + if ( regionMap.containsKey( vertex ) ) { + regions = regionMap.get( vertex ); + } else { + regions = new ArrayDeque< VisibilityRegion >(); + regionMap.put( vertex, regions ); + } + + regions.add( region ); + } + + void add( final Vertex vertex, final Collection< VisibilityRegion > other ) { + if ( !other.isEmpty() ) { + Collection< VisibilityRegion > regions; + if ( regionMap.containsKey( vertex ) ) { + regions = regionMap.get( vertex ); + } else { + regions = new ArrayDeque< VisibilityRegion >(); + regionMap.put( vertex, regions ); + } + + regions.addAll( other ); + } + } + + void add( final Partition other ) { + for ( final Entry< Vertex, Collection< VisibilityRegion > > entry : other.regionMap.entrySet() ) { + final Vertex vertex = entry.getKey(); + final Collection< VisibilityRegion > otherRegions = entry.getValue(); + + Collection< VisibilityRegion > regions; + if ( regionMap.containsKey( vertex ) ) { + regions = regionMap.get( vertex ); + } else { + regions = new ArrayDeque< VisibilityRegion >(); + regionMap.put( vertex, regions ); + } + + regions.addAll( otherRegions ); + } + } + + @Override + public String toString() { + return String.format( "Partition{lower=%s,upper=%s", lower, upper ); + } + } + + // TODO Use a map for faster searching + // TODO Can keep two maps, a lower and upper map mapping edges to + // a particular partition, which will let us keep constant time + // for partition related operations. + final Collection< Partition > partitions = new HashSet< Partition >(); + + // Keep track of edges from bottom top + final SortedEdgeCollection< HalfEdge > edgeCollection = new SortedEdgeCollection< HalfEdge >( Mesh::greaterThanOrEqualTo ); + + for ( final Vertex event : vertices ) { + final List< HalfEdge > leftGoing = new ArrayList< HalfEdge >(); + final List< HalfEdge > rightGoing = new ArrayList< 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!" ); + } + } + } + + HalfEdge eventEdge = event.getEdge(); + if ( !isPositive( eventEdge ) ) { + eventEdge = eventEdge.getSym(); + } + // Get the lower edge, if there is one + final HalfEdge lower = edgeCollection.searchLower( eventEdge ); + + /* + * How many conditions are there? + * - This vertex only has right edges + * - This vertex only has left edges + * - This vertex has both left and right edges + * - The lower edge is an interior edge + * - The lower edge is not an interior edge + */ + + // Create a collection to store newly created partitions + // Technically we can add new partitions to the main collection + // at any time, but we know that the new partitions will not + // be used in any checks for the remainder of this loop, so + // it would be a waste to add them until after we are done + // with all the partitions. + final Collection< Partition > newPartitions = new HashSet< Partition >(); + + if ( !leftGoing.isEmpty() ) { + // Terminate any partitions + for ( final Iterator< Partition > it = partitions.iterator(); it.hasNext(); ) { + final Partition partition = it.next(); + + // Does both the upper and lower region of this partition end at this vertex? + if ( partition.upper.getDest() == event && partition.lower.getDest() == event ) { + // Increment either the upper or the lower bound... It shouldn't meaningfully make + // a difference which one we do since they all converge at the same location. + partition.incrementUpper( partition.lower.getSym() ); + + it.remove(); + } + } + } else if ( interior.contains( lower ) ) { + // This vertex only has right edges + // and is inside a partition. That means + // the partition containing this vertex must + // be split into two smaller partitions. + + final Optional< Partition > opt = partitions.parallelStream().filter( p -> p.lower == lower ).findAny(); + if ( opt.isPresent() ) { + final Partition toSplit = opt.get(); + + if ( rightGoing.size() < 2 ) { + throw new IllegalStateException( "Vertex has less than 2 right edges!" ); + } + + final HalfEdge newUpper = rightGoing.get( 0 ); + final HalfEdge newLower = rightGoing.get( rightGoing.size() - 1 ); + + final Partition copy = new Partition( lower, newUpper ); + toSplit.lower = newLower; + + /* + * A simple method for splitting and updating the visibility regions + * is to add all the visibility regions from toSplit to the copy, then + * call incrementUpper and incrementLower on both. However, we'd need to + * copy each visibility region, just to potentially remove them again. + * + * Instead, if we do a little manual labor and sort them ourselves, we + * don't need to deal with that problem, and can get it done a bit more + * efficiently at the cost of a little more thinking. + */ + for ( final Iterator< Entry< Vertex, Collection< VisibilityRegion > > > it = toSplit.regionMap.entrySet().iterator(); it.hasNext(); ) { + final Entry< Vertex, Collection< VisibilityRegion > > entry = it.next(); + final Vertex vert = entry.getKey(); + final Collection< VisibilityRegion > regions = entry.getValue(); + + // Get the cross value for this vertex to the current event vertex + final Vector2d toEvent = event.getPosition().subtracted( vert.getPosition() ).normalize(); + final double cross = CROSS.cross( toEvent ); + + final Collection< VisibilityRegion > copyRegions = new HashSet< VisibilityRegion >(); + // Check over each visibility region + for ( final Iterator< VisibilityRegion > regionIt = regions.iterator(); regionIt.hasNext(); ) { + final VisibilityRegion region = regionIt.next(); + + /* + * The region has 3 possible outcomes: + * 1. The lower cross value is greater than the event cross + * 2. The upper cross value is less than the event cross + * 3. The event cross is between the lower and upper cross values + */ + if ( region.lower > cross ) { + // The region belongs to the upper partition, so no change. + } else if ( region.upper < cross ) { + // The region belongs to the lower partition + + copyRegions.add( region ); + regionIt.remove(); + } else { + // Create a new visibility region + final VisibilityRegion copyRegion = new VisibilityRegion( region.lowerEdge, region.upperEdge ); + + // Update the new region's lower links + if ( region.lower != MIN_CROSS ) { + copyRegion.lower = region.lower; + copyRegion.links.put( region.lower, region.links.remove( region.lower ) ); + region.lower = MIN_CROSS; + } + + copyRegions.add( copyRegion ); + } + } + + if ( regions.isEmpty() ) { + // Remove this vertex if it no longer has any visible regions + it.remove(); + } + + // Add all the regions for the copy + copy.add( vert, copyRegions ); + } + + // Update the partitions with the new vertex + toSplit.incrementLower( newLower ); + copy.incrementUpper( newUpper ); + + // Add the current vertex with a visibility region to the top and bottom partitions + toSplit.add( event, new VisibilityRegion( newUpper, newLower ) ); + copy.add( event, new VisibilityRegion( newLower, newUpper ) ); + + newPartitions.add( copy ); + } else { + throw new IllegalStateException( "A partition with an interior edge does not exist!" ); + } + } + + if ( !rightGoing.isEmpty() ) { + // Create new partitions + for ( int i = 0; i + 1 < rightGoing.size(); ) { + final HalfEdge bottom = rightGoing.get( i ); + // Check if the bottom is an interior edge + if ( interior.contains( bottom ) ) { + final HalfEdge top = rightGoing.get( i + 1 ); + + final Partition newPartition = new Partition( bottom, top ); + + // Add a new visibility region for this vertex + newPartition.add( event, new VisibilityRegion( bottom, top ) ); + + // Add our new partition to the new partition collection + newPartitions.add( newPartition ); + + // Increment i by 2, since we are processing two edges + // at the same time. This means the last edge may get skipped, + // but that should be ok, because a partition should already + // exist for that edge. + i += 2; + } else { + // Increment i by 1, since we want to skip this edge + ++i; + } + } + } else if ( interior.contains( lower ) ) { + // This vertex only has left edges + // and is between two partitions. That + // means there exists 2 partitions which + // are converging and need to be merged + // into a single partition. + Partition top = null; + Partition bottom = null; + + if ( leftGoing.size() < 2 ) { + throw new IllegalStateException( "Vertex has less than 2 left edges!" ); + } + + final HalfEdge topLeft = leftGoing.get( 0 ).getSym(); + final HalfEdge bottomLeft = leftGoing.get( leftGoing.size() - 1 ).getSym(); + for ( final Iterator< Partition > it = partitions.iterator(); it.hasNext() && ( top == null || bottom == null ); ) { + final Partition partition = it.next(); + + // TODO Benefit from upper and lower partition mappings + if ( partition.upper == bottomLeft ) { + bottom = partition; + + // Delete the bottom partition + // No particular reason for why the bottom over the top + it.remove(); + } else if ( partition.lower == topLeft ) { + top = partition; + } + } + + if ( top == null ) { + throw new IllegalStateException( "Upper partition not found!" ); + } else if ( bottom == null ) { + throw new IllegalStateException( "Lower partition not found!" ); + } + + // Merge the top and bottom partition + top.lower = bottom.lower; + + // Update all visibility regions for the top and bottom partitions, separately + top.incrementLower( topLeft.getSym() ); + bottom.incrementUpper( bottomLeft.getSym() ); + + // Merge the two partitions + top.add( bottom ); + + // Add a new visibility region + top.add( event, new VisibilityRegion( bottomLeft.getSym(), topLeft.getSym() ) ); + } + + // This vertex has both left and right going edges + // That means this vertex may have a partition at the top, + // and a partition at the bottom, depending on the interior + // status of the lower and upper edges. + if ( !leftGoing.isEmpty() && !rightGoing.isEmpty() ) { + // Use the lower edge if it is not null, otherwise use the event edge + final HalfEdge upper = edgeCollection.searchUpper( lower == null ? eventEdge : lower ); + + if ( interior.contains( lower ) ) { + // There exists a partition below this vertex + final HalfEdge left = leftGoing.get( leftGoing.size() - 1 ).getSym(); + final HalfEdge right = rightGoing.get( 0 ); + + // TODO Benefit from a constant time lower partition mapping + final Optional< Partition > opt = partitions.parallelStream().filter( p -> p.lower == lower ).findAny(); + + if ( opt.isPresent() ) { + final Partition partition = opt.get(); + + if ( partition.upper != left ) { + throw new IllegalStateException( "Partition upper edge is inconsistent!" ); + } + + // Update the partition's upper edge to the new right going edge + partition.upper = right; + + // Update all visibility regions + partition.incrementUpper( right ); + + // Add a new region for this vertex + partition.add( event, new VisibilityRegion( left.getSym(), right ) ); + } else { + throw new IllegalStateException( "Interior edge does not have a partition!" ); + } + } + + if ( upper != null && interior.contains( upper.getSym() ) ) { + // There exists a partition above this vertex + final HalfEdge left = leftGoing.get( 0 ).getSym(); + final HalfEdge right = rightGoing.get( rightGoing.size() - 1 ); + + // TODO Benefit from an upper partition mapping + final Optional< Partition > opt = partitions.parallelStream().filter( p -> p.upper == upper ).findAny(); + + if ( opt.isPresent() ) { + final Partition partition = opt.get(); + + if ( partition.lower != left ) { + throw new IllegalStateException( "Partition lower edge is inconsistent!" ); + } + + partition.lower = right; + + // Update all visibility regions + partition.incrementLower( right ); + + // Add a new region for this vertex + partition.add( event, new VisibilityRegion( left.getSym(), right ) ); + } else { + throw new IllegalStateException( "Interior edge does not have a partition!" ); + } + } + } + + partitions.addAll( newPartitions ); + + for ( final HalfEdge edge : rightGoing ) { + edgeCollection.insert( edge ); + } + } + + if ( !partitions.isEmpty() ) { + throw new IllegalStateException( "Dangling partitions!" ); + } + + System.out.println( "Found links: " + chains.size() ); + Map< Vertex, Collection< Vertex > > mappings = new HashMap< Vertex, Collection< Vertex > >(); + for ( final Chain c : chains ) { + System.out.println( c.getOrigin() + "\t to \t" + c.getDest() ); + + Collection< Vertex > destinations; + if ( mappings.containsKey( c.getOrigin() ) ) { + destinations = mappings.get( c.getOrigin() ); + } else { + destinations = new HashSet< Vertex >(); + mappings.put( c.getOrigin(), destinations ); + } + + if ( !destinations.add( c.getDest() ) ) { + System.out.println( "DUPLICATE!" ); + } + } + return chains; + + // Given a chain link, select the links which intersect the least, and remove any that it intersects with from the + // queue. Essentially, a greedy method, which is more or less guaranteed optimal results. + } + 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 @@ -1324,10 +2132,10 @@ public class Mesh< T extends Region > { final HalfEdge next = stack.peek(); final Vector2d diagonal = next.getOrigin().getPosition().subtracted( edge.getOrigin().getPosition() ); - final double cross = diagonal.normalize().cross( leftEdge.toVector2d().normalize() ); + final double cross = diagonal.cross( leftEdge.toVector2d() ); // Check if the diagonal is inside the polygon - final boolean isInside = isPositive ? cross > ANGLE_TOLERANCE : cross < - ANGLE_TOLERANCE; + final boolean isInside = isPositive ? cross > 0 : cross < 0; if ( isInside ) { final HalfEdge newEdge = new HalfEdge(); @@ -1501,13 +2309,13 @@ public class Mesh< T extends Region > { } /* - * When 2 edges are colinear, and do not share the same endpoint + * When 2 edges are collinear, and do not share the same endpoint */ - private static class IntersectionColinear extends Intersection { + private static class IntersectionCollinear extends Intersection { private final HalfEdge shorter; private final HalfEdge longer; - IntersectionColinear( HalfEdge shorter, HalfEdge longer ) { + IntersectionCollinear( HalfEdge shorter, HalfEdge longer ) { this.shorter = shorter; this.longer = longer; } diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/MeshingTest2.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/MeshingTest2.java index 4d3a9bc..cd735f6 100644 --- a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/MeshingTest2.java +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/MeshingTest2.java @@ -4,8 +4,6 @@ import java.awt.Color; import java.awt.Font; import java.awt.Graphics; import java.awt.MouseInfo; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.event.MouseAdapter; @@ -26,13 +24,13 @@ import java.util.Random; import javax.swing.JFrame; import javax.swing.JPanel; import javax.swing.SwingUtilities; -import javax.swing.Timer; import org.bukkit.util.Vector; import com.aaaaahhhhhhh.bananapuncher714.mesh.EdgeSet; import com.aaaaahhhhhhh.bananapuncher714.mesh.HalfEdge; import com.aaaaahhhhhhh.bananapuncher714.mesh.Mesh; +import com.aaaaahhhhhhh.bananapuncher714.mesh.Mesh.Chain; import com.aaaaahhhhhhh.bananapuncher714.mesh.Mesh.EdgePolygon; import com.aaaaahhhhhhh.bananapuncher714.mesh.Point; import com.aaaaahhhhhhh.bananapuncher714.mesh.Polygon; @@ -56,7 +54,7 @@ public class MeshingTest2 extends JPanel { // private int windowHeight = 1000; private int windowWidth = 1200; - private int windowHeight = 1400; + private int windowHeight = 800; // private int centerX = 600; // private int centerY = 400; @@ -72,11 +70,14 @@ public class MeshingTest2 extends JPanel { private Collection< Vertex > data; private Collection< EdgePolygon > polygons; + // Temporary... Remove at some point + private static Collection< Chain > chains; + public static void main( String[] args ) { if ( CHUNK_DIR.exists() && CHUNK_DIR.isDirectory() ) { System.out.println( "Found " + CHUNK_DIR.list().length + " files" ); for ( File file : CHUNK_DIR.listFiles() ) { - Collection< Plane > planes = mesh( file ); + List< Plane > planes = mesh( file ); Plane draw = null; @@ -119,7 +120,7 @@ public class MeshingTest2 extends JPanel { } } - private static Collection< Plane > mesh( File file ) { + private static List< Plane > mesh( File file ) { String name = file.getName(); String[] split = name.split( "," ); ChunkLocation location = new ChunkLocation( split[ 0 ], Integer.parseInt( split[ 1 ] ), Integer.parseInt( split[ 2 ] ) ); @@ -187,7 +188,7 @@ public class MeshingTest2 extends JPanel { long start = System.currentTimeMillis(); mesh.simplify(); mesh.generateRegions(); - final Collection< EdgePolygon > polys = mesh.mesh(); + final Collection< EdgePolygon > polys = mesh.copyOf().mesh(); long end = System.currentTimeMillis(); System.out.println( plane.polygons.size() + " to " + polys.size() + ":\t " + ( end - start ) + "ms" ); } catch ( IllegalStateException e ) { @@ -263,6 +264,7 @@ public class MeshingTest2 extends JPanel { mesh = mesh.copyOf(); final Collection< EdgePolygon > polys = mesh.mesh(); + chains = mesh.chains; long end = System.currentTimeMillis(); System.out.println( "Took " + ( end - start ) + "ms" ); @@ -431,9 +433,9 @@ public class MeshingTest2 extends JPanel { @Override public void mouseWheelMoved( MouseWheelEvent e ) { if ( e.getWheelRotation() < 0 ) { - scroll += 0.5; + scroll += e.isControlDown() ? 0.1 : 0.5; } else { - scroll -= 0.5; + scroll -= e.isControlDown() ? 0.1 : 0.5; } scale = Math.exp( scroll ); @@ -554,11 +556,27 @@ public class MeshingTest2 extends JPanel { } } + if ( chains != null ) { + for ( Chain chain : chains ) { + Point p1 = chain.getOrigin().getPosition(); + Point p2 = chain.getDest().getPosition(); + g.setColor( Color.RED ); + g.drawLine( + ( int ) ( ( centerX + p1.getX() + 40 ) * scale ) + offsetX, + ( int ) ( ( centerY - p1.getY() ) * scale ) + offsetY, + ( int ) ( ( centerX + p2.getX() + 40 ) * scale ) + offsetX, + ( int ) ( ( centerY - p2.getY() ) * scale ) + offsetY + ); + } + } + java.awt.Point p = MouseInfo.getPointerInfo().getLocation(); - p = new java.awt.Point( p.x - getLocation().x, p.y - getLocation().y ); + java.awt.Point componentLocation = getLocation(); + SwingUtilities.convertPointToScreen( componentLocation, this ); + p = new java.awt.Point( p.x - componentLocation.x, p.y - componentLocation.y ); g.setColor( Color.RED ); g.setFont( new Font( Font.MONOSPACED, Font.BOLD, 30 ) ); g.drawString( "X: " + ( ( p.x - offsetX ) / scale - centerX ), 10, 30 ); - g.drawString( "Y: " + ( ( p.y - offsetY ) / scale - centerY ), 10, 60 ); + g.drawString( "Y: " + ( - ( ( p.y - offsetY ) / scale - centerY ) ), 10, 60 ); } } \ No newline at end of file