diff --git a/pom.xml b/pom.xml index b217204..a84d398 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ org.spigotmc spigot - 1.19.4-R0.1-SNAPSHOT + 1.21.5-R0.1-SNAPSHOT provided diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/EdgeSet.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/EdgeSet.java new file mode 100644 index 0000000..18ac793 --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/EdgeSet.java @@ -0,0 +1,51 @@ +package com.aaaaahhhhhhh.bananapuncher714.mesh; + +import java.util.HashSet; + +/** + * A set for easily checking if a set contains/does not contain a half-edge or its sym. + * + * @author BananaPuncher714 + */ +public class EdgeSet extends HashSet< HalfEdge > { + /** + * + */ + private static final long serialVersionUID = 4042266887750818558L; + + @Override + public boolean add( HalfEdge edge ) { + if ( !contains( edge ) ) { + return super.add( edge ); + } else { + return false; + } + } + + @Override + public boolean remove( Object object ) { + if ( object instanceof HalfEdge ) { + HalfEdge edge = ( HalfEdge ) object; + final boolean r1 = super.remove( edge ); + final boolean r2 = super.remove( edge.getSym() ); + + if ( r1 && r2 ) { + throw new IllegalStateException( "Removed edge was added twice!" ); + } + + return r1 || r2; + } else { + return false; + } + } + + @Override + public boolean contains( Object object ) { + if ( object instanceof HalfEdge ) { + HalfEdge edge = ( HalfEdge ) object; + return super.contains( edge ) || super.contains( edge.getSym() ); + } else { + return false; + } + } +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/HalfEdge.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/HalfEdge.java new file mode 100644 index 0000000..3a81591 --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/HalfEdge.java @@ -0,0 +1,201 @@ +package com.aaaaahhhhhhh.bananapuncher714.mesh; + +/** + * A half-edge class used to represent a DCEL(doubly connected edge list): https://en.wikipedia.org/wiki/Doubly_connected_edge_list + * + * In this case, it is intended to be used for a 2d PSLG(planar straight-line graph), but can probably be repurposed + * for some other nefarious use case. + * + * An edge is a line segment connecting an origin to a destination. A half-edge is a partial line segment containing only + * the origin, but not the destination. + * + * Each half-edge structure contains: + * - A reference to the origin + * - A reference to the symmetrical, or opposite half-edge which is attached to the destination. + * - A reference to the next clockwise half-edge of the opposite half edge. This can be used to + * traverse counter-clockwise through the half-edges of a polygon. + * - A reference to the previous edge, which is the next counter-clockwise edge of this half edge. + * This can be used to traverse all the edges of a vertex, in counter clockwise order. + * + * For a given half-edge attached to two points that contain no other edges, the previous edge is + * itself, and the next edge is the same as the opposite edge. + * + * There are 2 main operations that are used when manipulating half-edges to attach/detach them from + * a vertex: + * - splicing + * - splitting + * + * Splicing is the most important of the 2 operations, by a large margin. Given two half-edges, A and B, + * swap the previous edges, and set the next edge for both of the previous edges to the opposite half edge. + * This has the desired effect of either detaching two edges if they are connected to the same vertex, or + * attaching two separate half-edges together to the same vertex. + * + * Splitting is adding an edge between the opposite of an edge and attaching it to the destination. It + * splits a single edge into two edges with a vertex in the middle. + * + * @author BananaPuncher714 + */ +public class HalfEdge { + /** + * The origin + */ + protected Vertex origin; + + /** + * The symmetrical half-edge + */ + protected HalfEdge sym; + /** + * The half-edge counter-clockwise on the same origin + */ + protected HalfEdge prev; + /** + * The half-edge clockwise from the symmetrical half-edge + */ + protected HalfEdge next; + + // TODO Consider adding a constructor with a origin and destination + + public HalfEdge() { + init(); + + // Whenever we create a new half-edge, we need to make + // sure that it has a symmetrical half-edge. + new HalfEdge( this ); + } + + private HalfEdge( HalfEdge o ) { + init(); + + sym = o; + o.sym = this; + + next = o; + o.next = this; + } + + private void init() { + origin = new Vertex( this ); + prev = this; + } + + public Vertex getOrigin() { + return origin; + } + + /** + * Set the origin. Does not affect any additional + * + * @param vert + * @return + */ + public HalfEdge setOrigin( Vertex vert ) { + origin = vert; + return this; + } + + public HalfEdge getSym() { + return sym; + } + + public HalfEdge getPrev() { + return prev; + } + + /** + * Set the previous half-edge. Does not update additional properties. + * + * @param edge The half-edge to set as the previous half-edge. + * @return This half-edge + */ + public HalfEdge setPrev( HalfEdge edge ) { + this.prev = edge; + return this; + } + + public HalfEdge getNext() { + return next; + } + + /** + * Set the next half-edge. Does not update additional properties. + * + * @param edge The half-edge to set as the next half-edge. + * @return This half-edge + */ + public HalfEdge setNext( HalfEdge edge ) { + this.next = edge; + return this; + } + + public Vertex getDest() { + return sym.origin; + } + + /** + * Get the difference between the destination position and the origin position + * + * @return A vector representing the destination minus the origin + */ + public Vector2d toVector2d() { + return getDest().getPosition().subtracted( getOrigin().getPosition() ); + } + + /** + * Check if the origin and destination are the same + * + * @return If the origin is equal to the destination + */ + public boolean isZero() { + return getOrigin().equals( getDest() ); + } + + /** + * Split this half-edge in half and return the newly created + * half-edge, as the next half-edge of this half-edge. + * + * @return A new half-edge with a default initialized origin. + */ + public HalfEdge split() { + final HalfEdge edge = new HalfEdge(); + + splice( edge, getNext() ); + + splice( getSym(), getNext() ); + splice( getSym(), edge.getSym() ); + + edge.setOrigin( getDest() ); + edge.getOrigin().setEdge( edge ); + + getSym().setOrigin( edge.getDest() ); + getDest().setEdge( getSym() ); + + edge.getOrigin().setEdge( edge ); + + return edge.getSym(); + } + + /** + * Splice two half-edges together. A commutative operation. + * + * This does _not_ modify any vertices. + * + * @param a The first half-edge + * @param b The second half-edge + */ + public static void splice( HalfEdge a, HalfEdge b ) { + final HalfEdge ap = a.prev; + final HalfEdge bp = b.prev; + + a.prev = bp; + b.prev = ap; + + ap.sym.next = b; + bp.sym.next = a; + } + + @Override + public String toString() { + return String.format( "HalfEdge{orig=%1$s,dest=%2$s}", getOrigin(), getDest() ); + } +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/Mesh.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/Mesh.java new file mode 100644 index 0000000..8ebc952 --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/Mesh.java @@ -0,0 +1,1324 @@ +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.TreeSet; +import java.util.function.Supplier; + +import com.aaaaahhhhhhh.bananapuncher714.mesh.region.Region; +import com.aaaaahhhhhhh.bananapuncher714.mesh.region.RegionRule; +import com.aaaaahhhhhhh.bananapuncher714.mesh.region.simple.RegionRuleWinding; +import com.aaaaahhhhhhh.bananapuncher714.mesh.region.simple.RegionSimple; +import com.aaaaahhhhhhh.bananapuncher714.mesh.region.simple.RegionSimple.GluWindingRule; + +/** + * A general mesh class containing polygons + * + * @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.01 ); + + // 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.SIMPLIFIED; + + public static void main( String[] args ) { + Mesh< RegionSimple > mesh = new Mesh< RegionSimple >( () -> { return new RegionSimple( GluWindingRule.ODD ); } ); + + mesh.addPolygon( new Polygon( Arrays.asList( + new Point( -1, -1 ), + new Point( 1, -1 ), + new Point( 1, 1 ), + new Point( -1, 1 ) + ) ), RegionRuleWinding.CLOCKWISE ); + mesh.addPolygon( new Polygon( Arrays.asList( + new Point( 1, -1 ), + new Point( 3, -1 ), + new Point( 3, 1 ), + new Point( 1, 1 ) + ) ), RegionRuleWinding.CLOCKWISE ); + +// mesh.addPolygon( new Polygon( Arrays.asList( +// new Point( -1, -1 ), +// new Point( 1, -1 ), +// new Point( 1, 1 ), +// new Point( -1, 1 ) +// ) ), RegionRuleWinding.CLOCKWISE ); +// mesh.addPolygon( new Polygon( Arrays.asList( +// new Point( -1, 1 ), +// new Point( 1, 1 ), +// new Point( 1, 3 ), +// new Point( -1, 3 ) +// ) ), RegionRuleWinding.CLOCKWISE ); +// mesh.addPolygon( new Polygon( Arrays.asList( +// new Point( -2, -1 ), +// new Point( -1, -1 ), +// new Point( -1, 2 ), +// new Point( -2, 2 ) +// ) ), RegionRuleWinding.CLOCKWISE ); +// mesh.addPolygon( new Polygon( Arrays.asList( +// new Point( -4, -2 ), +// new Point( -1, -2 ), +// new Point( -1, 1.5 ), +// new Point( -4, 1.5 ) +// ) ), RegionRuleWinding.CLOCKWISE ); +// mesh.addPolygon( new Polygon( Arrays.asList( +// new Point( -3, 0 ), +// new Point( 3, 0 ), +// new Point( -1.5, -3 ), +// new Point( 0, 1.5 ), +// new Point( 1.5, -3 ) +// ) ), RegionRuleWinding.CLOCKWISE ); +// mesh.addPolygon( new Polygon( Arrays.asList( +// new Point( -1, 0 ), +// new Point( 0, 1.5 ), +// new Point( 1, 0 ) +// ) ), RegionRuleWinding.CLOCKWISE ); +// mesh.addPolygon( new Polygon( Arrays.asList( +// new Point( 1, 0 ), +// new Point( 0, -2 ), +// new Point( -1, 0 ) +// ) ), RegionRuleWinding.CLOCKWISE ); + + System.out.println( "Vertices: " + mesh.vertices.size() ); + System.out.println( "Edges: " + ( mesh.rules.size() / 2 ) ); + mesh.simplify(); + System.out.println( "Vertices: " + mesh.vertices.size() ); + System.out.println( "Edges: " + ( mesh.rules.size() / 2 ) ); + mesh.generateRegions(); + System.out.println( "Vertices: " + mesh.vertices.size() ); + System.out.println( "Edges: " + ( mesh.rules.size() / 2 ) ); + System.out.println( "Partitions: " + mesh.mesh().size() ); + } + + public Mesh( final Supplier< T > defaultRegionSupplier ) { + this.defaultRegionSupplier = defaultRegionSupplier; + } + + public void addPolygon( final Polygon poly, final RegionRule< T > rule ) { + HalfEdge edge = null; + for ( Point point : poly.getPoints() ) { + if ( edge == null ) { + edge = new HalfEdge(); + HalfEdge.splice( edge, edge.getSym() ); + edge.getOrigin().update(); + } 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() { + vertices.clear(); + rules.clear(); + } + + /** + * 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. + */ + 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.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 ); + + 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 = e1.toVector2d().cross( e2.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 void 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; + + // 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 ); + + rules.remove( next ); + rules.remove( next.getSym() ); + + toRemove.add( next ); + } else { + edges.add( edge ); + edge = edge.getNext(); + } + } + } + + internalEdges.removeAll( 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 >(); + + 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() ); + } 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 ); + + HalfEdge.splice( newEdge.getPrev(), vert.getEdge() ); + + toSort.add( vert ); + } else { + vert = newEdge.getOrigin(); + newVertices.put( tempVert, vert ); + + vert.setPosition( tempVert.getPosition() ); + } + vert.update(); + + edges.add( newEdge ); + } while ( scanned.add( temp = temp.getNext() ) ); + } + } + + for ( final Vertex v : toSort ) { + sort( v ); + } + + // Have a single set of edges + final TreeSet< Vertex > vertices = new TreeSet< Vertex >( Mesh::compare ); + vertices.addAll( newVertices.values() ); + + final Collection< EdgePolygon > polygons = partitionMonotone( vertices, edges ); + + return polygons; + } + + 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 ) ) { + // 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, prev.getEdge() ); + + leftGoing.add( edge ); + supportEdges.add( edge ); + + toSort.add( event ); + } 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( lower, edge ); + 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( upper.getSym().getNext(), edge ); + 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.getSym(), vertex.getEdge() ); + HalfEdge.splice( edge, prev.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() ); + } + + // 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 + Collection< EdgePolygon > newPolygons = new HashSet< 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 ) { + 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 void triangulate() { + + } + + /* + * Assumes region a and b overlap somewhere on the x axis + * Assume both wings do not intersect, and are nonzero and positive + * Assume that a and b cannot both be vertical and not 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 + */ + private static boolean isPositive( HalfEdge edge ) { + return compare( edge.getOrigin(), edge.getDest() ) <= 0; + } + + private static int compare( final Vertex a, final Vertex b ) { + 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 TreeSet< Vertex > vertices = new TreeSet< Vertex >( Mesh::compare ); + + private void addEdge( HalfEdge edge ) { + edges.add( edge ); + vertices.add( edge.getOrigin() ); + } + + public Set< HalfEdge > getEdges() { + return edges; + } + + public TreeSet< Vertex > getVertices() { + return vertices; + } + } + + protected enum MeshState { + DIRTY, + SIMPLIFIED, + TRIANGULATION_READY + } +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Point.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/Point.java similarity index 68% rename from src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Point.java rename to src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/Point.java index 71a4cb7..bdf8d2d 100644 --- a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Point.java +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/Point.java @@ -1,7 +1,12 @@ -package com.aaaaahhhhhhh.bananapuncher714.tess4j; +package com.aaaaahhhhhhh.bananapuncher714.mesh; +/** + * A 2d cartesian coordinate + * + * @author BananaPuncher714 + */ public class Point { - double x, y; + protected double x, y; public Point( double x, double y ) { this.x = x; @@ -15,6 +20,16 @@ public class Point { public double getY() { return y; } + + public Point setX( double x ) { + this.x = x; + return this; + } + + public Point setY( double y ) { + this.y = y; + return this; + } @Override public int hashCode() { @@ -43,4 +58,9 @@ public class Point { return false; return true; } + + @Override + public String toString() { + return String.format( "Point{x=%f,y=%f}", x, y ); + } } diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/Polygon.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/Polygon.java new file mode 100644 index 0000000..8c3e782 --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/Polygon.java @@ -0,0 +1,31 @@ +package com.aaaaahhhhhhh.bananapuncher714.mesh; + +import java.util.List; + +/** + * A list of points denoting the shape of the polygon. May contain self intersections, + * duplicate vertices, and overlapping edges. + * + * @author BananaPuncher714 + */ +public class Polygon { + protected List< Point > points; + + public Polygon( List< Point > points ) { + this.points = points; + } + + public List< Point > getPoints() { + return points; + } + + public double area() { + double area = 0; + for ( int i = 0; i < points.size(); ++i ) { + final Vector2d a = new Vector2d( points.get( i ) ); + final Vector2d b = new Vector2d( points.get( ( i + 1 ) % points.size() ) ); + area += a.cross( b ); + } + return Math.abs( area ) / 2.0; + } +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/SortedEdgeCollection.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/SortedEdgeCollection.java new file mode 100644 index 0000000..a43e773 --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/SortedEdgeCollection.java @@ -0,0 +1,141 @@ +package com.aaaaahhhhhhh.bananapuncher714.mesh; + +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiFunction; + +public class SortedEdgeCollection< T > implements Iterable< T > { + Map< T, Node > quickMap = new ConcurrentHashMap< T, Node >(); + Node head = new Node( null ); + + BiFunction< T, T, Boolean > isGreaterThan; + + public SortedEdgeCollection( BiFunction< T, T, Boolean > compare ) { + isGreaterThan = compare; + } + + public T bottom() { + return head.upper == head ? null : head.upper.value; + } + + public T upper( T val ) { + Node n = quickMap.get( val ); + return n == null ? null : n.upper.value; + } + + public T lower( T val ) { + Node n = quickMap.get( val ); + return n == null ? null : n.lower.value; + } + + public boolean remove( T val ) { + Node n = quickMap.remove( val ); + if ( n != null ) { + n.lower.upper = n.upper; + n.upper.lower = n.lower; + return true; + } else { + return false; + } + } + + public boolean contains( T val ) { + return quickMap.containsKey( val ); + } + + public void insert( T val ) { + if ( !quickMap.containsKey( val ) ) { + Node insertBefore = head.upper; + // Find the first node that the value is NOT greater than + // Otherwise, break and insert before + while ( insertBefore.value != null && isGreaterThan.apply( val, insertBefore.value ) ) { + insertBefore = insertBefore.upper; + } + + Node newNode = new Node( val ); + + newNode.lower = insertBefore.lower; + newNode.lower.upper = newNode; + newNode.upper = insertBefore; + + insertBefore.lower = newNode; + + quickMap.put( val, newNode ); + } else { + // Should not really happen + throw new IllegalStateException( "Tried to insert value twice" ); + } + } + + // Find the first value that is greater than the one provided + public T searchUpper( T val ) { + Node n = quickMap.get( val ); + if ( n == null ) { + Node greater = head.upper; + while ( greater.value != null && isGreaterThan.apply( val, greater.value ) ) { + greater = greater.upper; + } + return greater.value; + } else { + return n.upper.value; + } + } + + // Find the highest value that is lower than the one provided + public T searchLower( T val ) { + Node n = quickMap.get( val ); + if ( n == null ) { + Node lower = head.upper; + while ( lower.value != null && isGreaterThan.apply( val, lower.value ) ) { + lower = lower.upper; + } + return lower.lower.value; + } else { + return n.lower.value; + } + } + + public void clear() { + head = new Node( null ); + quickMap.clear(); + } + + protected class Node { + Node lower; + Node upper; + T value; + + Node( T val ) { + lower = this; + upper = this; + + this.value = val; + } + } + + @Override + public Iterator< T > iterator() { + return new Iterator< T >() { + Node current = head; + + @Override + public boolean hasNext() { + return current.upper != head; + } + + @Override + public T next() { + current = current.upper; + return current.value; + } + + @Override + public void remove() { + T val = current.value; + current = current.lower; + SortedEdgeCollection.this.remove( val ); + } + }; + } +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/Test.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/Test.java new file mode 100644 index 0000000..dc6e157 --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/Test.java @@ -0,0 +1,42 @@ +package com.aaaaahhhhhhh.bananapuncher714.mesh; + +public class Test { + public static void main( String[] args ) { + final Vector2d a = new Vector2d( -1, -1 ); + final Vector2d b = new Vector2d( 10, 10 ); + final Vector2d c = new Vector2d( 0, -1 ); + final Vector2d d = new Vector2d( 10, 11 ); + + test( a, b, c, d ); + } + + public static void test( final Vector2d a, final Vector2d b, final Vector2d c, final Vector2d d ) { + final Vector2d ab = b.subtracted( a ); + final Vector2d cd = d.subtracted( c ); + + final Vector2d ac = c.subtracted( a ); + + final double acab = ac.cross( ab ); + final double abcd = ab.cross( cd ); + + System.out.println( "Parallel value: " + abcd ); + if ( abcd != 0 ) { + final double s = ac.cross( cd ); + final double t = s / abcd; + final double u = acab / abcd; + + System.out.println( "S: " + s ); + System.out.println( "T: " + t ); + System.out.println( "U: " + u ); + + System.out.println( "AB Length: " + ( t * ab.length() ) ); + System.out.println( "CD Length: " + ( u * cd.length() ) ); + + // Gives the intersection correctly + System.out.println( "AB int: " + ab.multiplied( t ).add( a ) ); + System.out.println( "CD int: " + cd.multiplied( u ).add( c ) ); + } else { + + } + } +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Vector2d.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/Vector2d.java similarity index 67% rename from src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Vector2d.java rename to src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/Vector2d.java index 32bab8b..314e999 100644 --- a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Vector2d.java +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/Vector2d.java @@ -1,41 +1,28 @@ -package com.aaaaahhhhhhh.bananapuncher714.tess4j; +package com.aaaaahhhhhhh.bananapuncher714.mesh; -public class Vector2d { - double x; - double y; - +/** + * A basic 2d vector implementation. + * + * @author BananaPuncher714 + */ +public class Vector2d extends Point { public Vector2d() { this( 0, 0 ); } public Vector2d( double x, double y ) { - this.x = x; - this.y = y; + super( x, y ); } - public Vector2d( Vector2d o ) { + public Vector2d( Point o ) { this( o.x, o.y ); } - public double getX() { - return x; - } - - public double getY() { - return y; - } - - public Vector2d setX( double x ) { - this.x = x; - return this; - } - - public Vector2d setY( double y ) { - this.y = y; - return this; + public double angle( Point o ) { + return Math.atan2( cross( o ), dot( o ) ); } - public double cross( Vector2d o ) { + public double cross( Point o ) { return ( x * o.y ) - ( y * o.x ); } @@ -47,15 +34,15 @@ public class Vector2d { return Math.sqrt( lengthSquared() ); } - public double distanceSquared( Vector2d o ) { + public double distanceSquared( Point o ) { return distanceSquared( this, o ); } - public double distance( Vector2d o ) { + public double distance( Point o ) { return distance( this, o ); } - public double dot( Vector2d o ) { + public double dot( Point o ) { return dot( this, o ); } @@ -69,13 +56,13 @@ public class Vector2d { return new Vector2d( x + v, y + v ); } - public Vector2d add( Vector2d o ) { + public Vector2d add( Point o ) { x += o.x; y += o.y; return this; } - public Vector2d added( Vector2d o ) { + public Vector2d added( Point o ) { return add( this, o ); } @@ -89,13 +76,13 @@ public class Vector2d { return new Vector2d( x - v, y - v ); } - public Vector2d subtract( Vector2d o ) { + public Vector2d subtract( Point o ) { x -= o.x; y -= o.y; return this; } - public Vector2d subtracted( Vector2d o ) { + public Vector2d subtracted( Point o ) { return new Vector2d( x - o.x, y - o.y ); } @@ -109,13 +96,13 @@ public class Vector2d { return new Vector2d( x * v, y * v ); } - public Vector2d multiply( Vector2d o ) { + public Vector2d multiply( Point o ) { x *= o.x; y *= o.y; return this; } - public Vector2d multiplied( Vector2d o ) { + public Vector2d multiplied( Point o ) { return multiply( this, o ); } @@ -129,13 +116,13 @@ public class Vector2d { return new Vector2d( x / v, y / v ); } - public Vector2d divide( Vector2d o ) { + public Vector2d divide( Point o ) { x /= o.x; y /= o.y; return this; } - public Vector2d divided( Vector2d o ) { + public Vector2d divided( Point o ) { return new Vector2d( x / o.x, y / o.y ); } @@ -151,23 +138,23 @@ public class Vector2d { return x == 0 && y == 0; } - public static double dot( Vector2d a, Vector2d b ) { + public static double dot( Point a, Point b ) { return a.x * b.x + a.y * b.y; } - public static Vector2d add( Vector2d a, Vector2d b ) { + public static Vector2d add( Point a, Point b ) { return new Vector2d( a.x + b.x, a.y + b.y ); } - public static Vector2d multiply( Vector2d a, Vector2d b ) { + public static Vector2d multiply( Point a, Point b ) { return new Vector2d( a.x * b.x, a.y * b.y ); } - public static double distanceSquared( Vector2d a, Vector2d b ) { + public static double distanceSquared( Vector2d a, Point b ) { return a.subtracted( b ).lengthSquared(); } - public static double distance( Vector2d a, Vector2d b ) { + public static double distance( Vector2d a, Point b ) { return Math.sqrt( distanceSquared( a, b ) ); } @@ -201,6 +188,6 @@ public class Vector2d { @Override public String toString() { - return "Vector2d{x=" + x + ",y=" + y + "}"; + return String.format( "Vector2d{x=%f,y=%f}", x, y ); } } diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/Vertex.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/Vertex.java new file mode 100644 index 0000000..5decf33 --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/Vertex.java @@ -0,0 +1,121 @@ +package com.aaaaahhhhhhh.bananapuncher714.mesh; + +import java.util.Iterator; + +/** + * The vertex class is used to represent a point in space. For our use cases, it is going to be 2d. + * + * A vertex contains two pieces of information: + * - The position + * - A half-edge which is attached to this vertex + * + * To traverse over all the edges which are connected to this vertex, + * loop over the previous edge of the half edge until all of the edges + * have been reached at least once. + * + * Whenever performing a splicing operation on an edge, it is a good + * idea to update all edges attached to this vertex to point to this + * vertex, in case if they are pointing at a different one. + * + * @author BananaPuncher714 + */ +public class Vertex implements Iterable< HalfEdge > { + protected HalfEdge edge; + protected Vector2d position; + + public Vertex( final HalfEdge edge ) { + this( edge, new Vector2d() ); + } + + public Vertex( final HalfEdge edge, final Vector2d position ) { + setEdge( edge ); + setPosition( position ); + } + + public HalfEdge getEdge() { + return edge; + } + + public Vertex setEdge( HalfEdge edge ) { + this.edge = edge; + return this; + } + + public Vector2d getPosition() { + return position; + } + + public Vertex setPosition( Vector2d vec ) { + this.position = vec; + return this; + } + + /** + * O(n) + */ + public int size() { + int size = 0; + for ( final Iterator< HalfEdge > it = iterator(); it.hasNext(); it.next() ) { + ++size; + } + return size; + } + + /** + * Update all attached half-edges to point to this vertex + * as the origin + */ + public void update() { + HalfEdge edge = this.edge; + do { + edge.setOrigin( this ); + } while ( ( edge = edge.getPrev() ) != this.edge ); + } + + @Override + public Iterator< HalfEdge > iterator() { + return new Iterator< HalfEdge >() { + private final HalfEdge start = edge; + private HalfEdge index = null; + + @Override + public boolean hasNext() { + return index != start; + } + + @Override + public HalfEdge next() { + if ( index == null ) { + index = start.getPrev(); + return start; + } else { + final HalfEdge curr = index; + index = index.getPrev(); + return curr; + } + } + }; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Vertex other = (Vertex) obj; + if (position == null) { + if (other.position != null) + return false; + } else if (!position.equals(other.position)) + return false; + return true; + } + + @Override + public String toString() { + return "Vertex{x=" + position.getX() + ",y=" + position.getY() + "}"; + } +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/region/CompositeRegionRule.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/region/CompositeRegionRule.java new file mode 100644 index 0000000..18d7c3e --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/region/CompositeRegionRule.java @@ -0,0 +1,57 @@ +package com.aaaaahhhhhhh.bananapuncher714.mesh.region; + +import java.util.ArrayList; +import java.util.List; +import java.util.ListIterator; + +public class CompositeRegionRule< T extends Region > implements RegionRule< T > { + final protected List< RegionRule< T > > rules; + + public CompositeRegionRule() { + this.rules = new ArrayList< RegionRule< T > >(); + } + + public CompositeRegionRule( final List< RegionRule< T > > rules ) { + this.rules = new ArrayList< RegionRule< T > >( rules ); + } + + public CompositeRegionRule< T > addRule( final RegionRule< T > rule ) { + rules.add( rule ); + return this; + } + + public List< RegionRule< T > > getRules() { + return rules; + } + + @Override + public T apply( T region ) { + for ( final RegionRule< T > rule : rules ) { + region = rule.apply( region ); + } + return region; + } + + @Override + public RegionRule< T > inverse() { + CompositeRegionRule< T > rule = new CompositeRegionRule< T >(); + for ( ListIterator< RegionRule< T > > it = rules.listIterator(); it.hasPrevious(); ) { + rule.rules.add( it.previous().inverse() ); + } + return rule; + } + + @Override + public RegionRule< T > apply( RegionRule< T > rule ) { + final CompositeRegionRule< T > copy = new CompositeRegionRule< T >( rules ); + if ( rule instanceof CompositeRegionRule ) { + final CompositeRegionRule< T > other = ( CompositeRegionRule< T > ) rule; + for ( RegionRule< T > r : other.getRules() ) { + copy.addRule( r ); + } + } else { + copy.addRule( rule ); + } + return copy; + } +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/region/Region.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/region/Region.java new file mode 100644 index 0000000..366d6b7 --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/region/Region.java @@ -0,0 +1,14 @@ +package com.aaaaahhhhhhh.bananapuncher714.mesh.region; + +/** + * A continuous portion in space that can be clearly defined as "inside" or "outside". + * + * The equality check is mainly used to guarantee winding rule consistency. + * + * @author BananaPuncher714 + */ +public abstract class Region { + // Represents the area under a wing + public abstract boolean isInterior(); + public abstract boolean equals( Object other ); +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/region/RegionRule.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/region/RegionRule.java new file mode 100644 index 0000000..40414a1 --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/region/RegionRule.java @@ -0,0 +1,28 @@ +package com.aaaaahhhhhhh.bananapuncher714.mesh.region; + +/** + * A region rule describes how a region is modified, particularly when crossing a border + * between a known region to an unknown region. The inverse region rule must provide a + * symmetric change such that applying the region rule then the inverse, the resulting region + * must be equivalent to the original region. Normally, the same rule is applied to all polygons + * that need to be tessellated, such as the odd number rule, but if your polygons conform to different or + * more complex interior/exterior critera, then you can easily set different rules per polygon. + * An example might be if you have two sets of polygons that you would like to tessellate + * together, and distinctly mark regions where they might overlap. You can use a combination + * of two odd number rules to properly mark the boundaries. + * + * For more information: + * - https://en.wikipedia.org/wiki/Nonzero-rule + * + * @author BananaPuncher714 + * + * @param A region type that can be modified by this rule + */ +public interface RegionRule< T extends Region > { + // Apply this rule to the region, and return a new region + T apply( T region ); + // Get the inverse of this rule + RegionRule< T > inverse(); + // Apply this rule to the given rule, and return a new rule + RegionRule< T > apply( RegionRule< T > rule ); +} \ No newline at end of file diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/region/simple/RegionRuleWinding.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/region/simple/RegionRuleWinding.java new file mode 100644 index 0000000..cbc3eb8 --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/region/simple/RegionRuleWinding.java @@ -0,0 +1,61 @@ +package com.aaaaahhhhhhh.bananapuncher714.mesh.region.simple; + +import com.aaaaahhhhhhh.bananapuncher714.mesh.region.CompositeRegionRule; +import com.aaaaahhhhhhh.bananapuncher714.mesh.region.RegionRule; + +/** + * A simple winding number rule. + * + * Changes the winding number for a given region. + * + * @author BananaPuncher714 + * + */ +public class RegionRuleWinding implements RegionRule< RegionSimple > { + public static final RegionRuleWinding CLOCKWISE = new RegionRuleWinding( 1 ); + public static final RegionRuleWinding COUNTER_CLOCKWISE = CLOCKWISE.inverse(); + + private final int amount; + private RegionRuleWinding inverse; + + private RegionRuleWinding( final int amount ) { + this.amount = amount; + } + + public int getAmount() { + return amount; + } + + @Override + public RegionSimple apply( final RegionSimple region ) { + RegionSimple copy = new RegionSimple( region ); + copy.increment( amount ); + return copy; + } + + @Override + public RegionRuleWinding inverse() { + if ( inverse == null ) { + inverse = new RegionRuleWinding( -amount ); + inverse.inverse = this; + } + return inverse; + } + + @Override + public RegionRule< RegionSimple > apply( final RegionRule< RegionSimple > rule ) { + if ( rule instanceof RegionRuleWinding ) { + final RegionRuleWinding other = ( RegionRuleWinding ) rule; + return new RegionRuleWinding( amount + other.amount ); + } else { + final CompositeRegionRule< RegionSimple > composite = new CompositeRegionRule< RegionSimple >(); + composite.addRule( this ).addRule( rule ); + return composite; + } + } + + @Override + public String toString() { + return String.format( "RegionRuleWinding{amount=%1$d}", amount ); + } +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/region/simple/RegionSimple.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/region/simple/RegionSimple.java new file mode 100644 index 0000000..b276102 --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/region/simple/RegionSimple.java @@ -0,0 +1,86 @@ +package com.aaaaahhhhhhh.bananapuncher714.mesh.region.simple; + +import com.aaaaahhhhhhh.bananapuncher714.mesh.region.Region; + +/** + * A region with a winding number. The interior-ness of + * this region is determined by the GLU winding rule: + * + * https://www.songho.ca/opengl/gl_tessellation.html#winding + * + * @author BananaPuncher714 + */ +public class RegionSimple extends Region { + protected int windingNumber; + protected GluWindingRule rule; + + public RegionSimple( RegionSimple o ) { + windingNumber = o.windingNumber; + rule = o.rule; + } + + public RegionSimple( GluWindingRule rule ) { + this.windingNumber = 0; + this.rule = rule; + } + + @Override + public boolean isInterior() { + switch ( rule ) { + case ODD: + return ( windingNumber & 1 ) == 1; + case NONZERO: + return windingNumber != 0; + case POSITIVE: + return windingNumber > 0; + case NEGATIVE: + return windingNumber < 0; + case ABS_GEQ_TWO: + return Math.abs( windingNumber ) >= 2; + default: + return false; + } + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + RegionSimple other = (RegionSimple) obj; + if (rule != other.rule) + return false; + if (windingNumber != other.windingNumber) + return false; + return true; + } + + @Override + public String toString() { + return String.format( "Region{number=%1$d,rule=%2$s}", windingNumber, rule ); + } + + public int getWindingNumber() { + return windingNumber; + } + + public RegionSimple setWindingNumber( int number ) { + this.windingNumber = number; + return this; + } + + public int increment( final int amount ) { + return windingNumber += amount; + } + + public enum GluWindingRule { + ODD, + NONZERO, + POSITIVE, + NEGATIVE, + ABS_GEQ_TWO + } +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/MeshingTest.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/MeshingTest.java index 3f5cf02..ff4ca7e 100644 --- a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/MeshingTest.java +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/MeshingTest.java @@ -8,7 +8,6 @@ import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import javax.swing.JFrame; @@ -17,16 +16,14 @@ import javax.swing.SwingUtilities; import org.bukkit.util.Vector; -import com.aaaaahhhhhhh.bananapuncher714.minietest.MeshTest.AABB; +import com.aaaaahhhhhhh.bananapuncher714.mesh.Point; +import com.aaaaahhhhhhh.bananapuncher714.mesh.Polygon; +import com.aaaaahhhhhhh.bananapuncher714.mesh.Vector2d; import com.aaaaahhhhhhh.bananapuncher714.minietest.objects.ChunkLocation; import com.aaaaahhhhhhh.bananapuncher714.minietest.objects.mesh.Facet; import com.aaaaahhhhhhh.bananapuncher714.minietest.objects.mesh.MeshBuilder; import com.aaaaahhhhhhh.bananapuncher714.minietest.objects.mesh.Plane; -import com.aaaaahhhhhhh.bananapuncher714.tess4j.Point; -import com.aaaaahhhhhhh.bananapuncher714.tess4j.Polygon; import com.aaaaahhhhhhh.bananapuncher714.tess4j.Tess4j; -import com.aaaaahhhhhhh.bananapuncher714.tess4j.Vector2d; -import com.aaaaahhhhhhh.bananapuncher714.tess4j.Vector2dComparator; import com.aaaaahhhhhhh.bananapuncher714.tess4j.region.RegionSimple; import com.aaaaahhhhhhh.bananapuncher714.tess4j.region.RegionSimple.GluWindingRule; import com.aaaaahhhhhhh.bananapuncher714.tess4j.region.WindingRuleSimple; diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/MeshingTest2.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/MeshingTest2.java new file mode 100644 index 0000000..6c07a72 --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/MeshingTest2.java @@ -0,0 +1,485 @@ +package com.aaaaahhhhhhh.bananapuncher714.minietest; + +import java.awt.Color; +import java.awt.Graphics; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.SwingUtilities; + +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.EdgePolygon; +import com.aaaaahhhhhhh.bananapuncher714.mesh.Point; +import com.aaaaahhhhhhh.bananapuncher714.mesh.Polygon; +import com.aaaaahhhhhhh.bananapuncher714.mesh.Vector2d; +import com.aaaaahhhhhhh.bananapuncher714.mesh.Vertex; +import com.aaaaahhhhhhh.bananapuncher714.mesh.region.simple.RegionRuleWinding; +import com.aaaaahhhhhhh.bananapuncher714.mesh.region.simple.RegionSimple; +import com.aaaaahhhhhhh.bananapuncher714.mesh.region.simple.RegionSimple.GluWindingRule; +import com.aaaaahhhhhhh.bananapuncher714.minietest.objects.ChunkLocation; +import com.aaaaahhhhhhh.bananapuncher714.minietest.objects.mesh.Facet; +import com.aaaaahhhhhhh.bananapuncher714.minietest.objects.mesh.MeshBuilder; +import com.aaaaahhhhhhh.bananapuncher714.minietest.objects.mesh.Plane; + +public class MeshingTest2 extends JPanel { + private static final File BASE = new File( System.getProperty( "user.dir" ) ); + private static final File CHUNK_DIR = new File( BASE, "chunks" ); + + JFrame f; + + private int windowWidth = 1200; + private int windowHeight = 1000; + +// private int centerX = 600; +// private int centerY = 400; + + private int centerX = 400; + private int centerY = 1200; + + private Graphics g; + + private int scale = 8; + + private Collection< Vertex > data; + private Collection< EdgePolygon > polygons; + + 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 ); + + Plane draw = null; + + System.out.println( "Planes: " + planes.size() ); + for ( Plane plane : planes ) { + if ( plane.polygons.size() > 100 ) { + draw = plane; + break; + } + } + +// draw = planes.get( new Random().nextInt( planes.size() ) ); + +// test( planes ); + + final Collection< Vertex > data = process( draw ); + final Collection< EdgePolygon > polys = triangulate( draw ); +// final Collection< EdgePolygon > polys = test(); + + SwingUtilities.invokeLater( new Runnable() { + @Override + public void run() { + new MeshingTest2( data, polys ); + } + } ); + break; + } + } else { + System.err.println( "No such directory exists: " + CHUNK_DIR ); + } + } + + private static Collection< 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 ] ) ); + + System.out.println( "Parsing chunk " + location ); + + // First parse the file to get a list of all bounding boxes that we can use + List< AABB > boxes = new ArrayList< AABB >(); + try ( BufferedReader reader = new BufferedReader( new FileReader( file ) ) ) { + String line; + while ( ( line = reader.readLine() ) != null ) { + if ( !line.isEmpty() ) { + String[] values = line.split( "," ); + double minX = Double.parseDouble( values[ 0 ] ); + double minY = Double.parseDouble( values[ 1 ] ); + double minZ = Double.parseDouble( values[ 2 ] ); + double maxX = Double.parseDouble( values[ 3 ] ); + double maxY = Double.parseDouble( values[ 4 ] ); + double maxZ = Double.parseDouble( values[ 5 ] ); + boxes.add( new AABB( minX, minY, minZ, maxX, maxY, maxZ ) ); + } + } + } catch ( FileNotFoundException e ) { + e.printStackTrace(); + return null; + } catch ( IOException e ) { + e.printStackTrace(); + return null; + } + + System.out.println( "Found " + boxes.size() + " boxes" ); + // Now that we have a bunch of bounding boxes, do whatever + + MeshBuilder builder = new MeshBuilder(); + Plane draw = null; + for ( AABB box : boxes ) { + for ( Facet facet : generateFacetsFor( box ) ) { + builder.addFacet( facet ); + } + } + + return builder.planes; + } + + private static void test( final Collection< Plane > planes ) { + final long processAllStart = System.currentTimeMillis(); + planes.parallelStream().forEach( plane -> { + try { + Mesh< RegionSimple > mesh = new Mesh< RegionSimple >( () -> { return new RegionSimple( GluWindingRule.ODD ); } ); + + for ( Polygon poly : plane.polygons ) { + mesh.addPolygon( poly, RegionRuleWinding.CLOCKWISE ); + } + long start = System.currentTimeMillis(); + mesh.simplify(); + mesh.generateRegions(); + long end = System.currentTimeMillis(); + System.out.println( plane.polygons.size() + ":\t " + ( end - start ) + "ms" ); + } catch ( IllegalStateException e ) { + e.printStackTrace(); + } + } ); + final long processAllEnd = System.currentTimeMillis(); + System.out.println( "Took " + ( processAllEnd - processAllStart ) + "ms to process " + planes.size() + " planes" ); + } + + private static Collection< Vertex > process( final Plane plane ) { + if ( plane != null ) { + System.out.println( "Norm:\t" + plane.normal ); + System.out.println( "Ref:\t" + plane.point ); + System.out.println( "Size:\t" + plane.polygons.size() ); + + Mesh< RegionSimple > mesh = new Mesh< RegionSimple >( () -> { return new RegionSimple( GluWindingRule.ODD ); } ); + for ( Polygon poly : plane.polygons ) { + mesh.addPolygon( poly, RegionRuleWinding.CLOCKWISE ); + } + + System.out.println( "Initial vertex count: " + mesh.getVertices().size() ); + System.out.println( "Initial edge count: " + ( mesh.getRuleSize() / 2 ) ); + + long start = System.currentTimeMillis(); + mesh.simplify(); + + System.out.println( "After simplify vertex count: " + mesh.getVertices().size() ); + System.out.println( "After simplify edge count: " + ( mesh.getRuleSize() / 2 ) ); + + mesh.generateRegions(); + long end = System.currentTimeMillis(); + + System.out.println( "After generating regions vertex count: " + mesh.getVertices().size() ); + System.out.println( "After generating regions edge count: " + ( mesh.getRuleSize() / 2 ) ); + + + System.out.println( "Took " + ( end - start ) + "ms" ); + return mesh.getVertices(); + } else { + System.out.println( "No data!" ); + return Collections.emptySet(); + } + } + + private static Collection< EdgePolygon > triangulate( final Plane plane ) { + if ( plane != null ) { + System.out.println( "Norm:\t" + plane.normal ); + System.out.println( "Ref:\t" + plane.point ); + System.out.println( "Size:\t" + plane.polygons.size() ); + + Mesh< RegionSimple > mesh = new Mesh< RegionSimple >( () -> { return new RegionSimple( GluWindingRule.ODD ); } ); + for ( Polygon poly : plane.polygons ) { + mesh.addPolygon( poly, RegionRuleWinding.CLOCKWISE ); + } + + System.out.println( "Initial vertex count: " + mesh.getVertices().size() ); + System.out.println( "Initial edge count: " + ( mesh.getRuleSize() / 2 ) ); + + long start = System.currentTimeMillis(); + mesh.simplify(); + + System.out.println( "After simplify vertex count: " + mesh.getVertices().size() ); + System.out.println( "After simplify edge count: " + ( mesh.getRuleSize() / 2 ) ); + + mesh.generateRegions(); + + System.out.println( "After generating regions vertex count: " + mesh.getVertices().size() ); + System.out.println( "After generating regions edge count: " + ( mesh.getRuleSize() / 2 ) ); + + final Collection< EdgePolygon > polys = mesh.mesh(); + long end = System.currentTimeMillis(); + System.out.println( "Took " + ( end - start ) + "ms" ); + + return polys; + } else { + System.out.println( "No data!" ); + return Collections.emptySet(); + } + } + + private static Collection< EdgePolygon > test() { + Mesh< RegionSimple > mesh = new Mesh< RegionSimple >( () -> { return new RegionSimple( GluWindingRule.ODD ); } ); + + mesh.addPolygon( new Polygon( Arrays.asList( + new Point( -1, -1 ), + new Point( 1, -1 ), + new Point( 1, 1 ), + new Point( -1, 1 ) + ) ), RegionRuleWinding.CLOCKWISE ); + + mesh.simplify(); + + mesh.generateRegions(); + + return mesh.mesh(); + } + + private static List< Facet > generateFacetsFor( AABB box ) { + List< Facet > facets = new ArrayList< Facet >(); + + Vector p1 = new Vector( box.xmin, box.ymin, box.zmin ); + Vector p2 = new Vector( box.xmin, box.ymin, box.zmax ); + Vector p3 = new Vector( box.xmin, box.ymax, box.zmin ); + Vector p4 = new Vector( box.xmin, box.ymax, box.zmax ); + Vector p5 = new Vector( box.xmax, box.ymin, box.zmin ); + Vector p6 = new Vector( box.xmax, box.ymin, box.zmax ); + Vector p7 = new Vector( box.xmax, box.ymax, box.zmin ); + Vector p8 = new Vector( box.xmax, box.ymax, box.zmax ); + + { + Facet facet = new Facet(); + facet.points.add( p1 ); + facet.points.add( p2 ); + facet.points.add( p4 ); + facet.points.add( p3 ); + facet.normal = new Vector( -1, 0, 0 ); + facets.add( facet ); + } + + { + Facet facet = new Facet(); + facet.points.add( p5 ); + facet.points.add( p6 ); + facet.points.add( p8 ); + facet.points.add( p7 ); + facet.normal = new Vector( 1, 0, 0 ); + facets.add( facet ); + } + + { + Facet facet = new Facet(); + facet.points.add( p1 ); + facet.points.add( p2 ); + facet.points.add( p6 ); + facet.points.add( p5 ); + facet.normal = new Vector( 0, -1, 0 ); + facets.add( facet ); + } + + { + Facet facet = new Facet(); + facet.points.add( p3 ); + facet.points.add( p4 ); + facet.points.add( p8 ); + facet.points.add( p7 ); + facet.normal = new Vector( 0, 1, 0 ); + facets.add( facet ); + } + + { + Facet facet = new Facet(); + facet.points.add( p1 ); + facet.points.add( p3 ); + facet.points.add( p7 ); + facet.points.add( p5 ); + facet.normal = new Vector( 0, 0, -1 ); + facets.add( facet ); + } + + { + Facet facet = new Facet(); + facet.points.add( p2 ); + facet.points.add( p4 ); + facet.points.add( p8 ); + facet.points.add( p6 ); + facet.normal = new Vector( 0, 0, 1 ); + facets.add( facet ); + } + + return facets; + } + + static class AABB { + double xmin, ymin, zmin; + double xmax, ymax, zmax; + + AABB( double xmin, double ymin, double zmin, double xmax, double ymax, double zmax ) { + this.xmin = xmin; + this.ymin = ymin; + this.zmin = zmin; + + this.xmax = xmax; + this.ymax = ymax; + this.zmax = zmax; + } + + double getVolume() { + return ( xmax - xmin ) * ( ymax - ymin ) * ( zmax - zmin ); + } + + @Override + public String toString() { + return String.format( "(%f, %f, %f) -> (%f, %f, %f)", xmin, ymin, zmin, xmax, ymax, zmax ); + } + } + + public MeshingTest2( Collection< Vertex > polys, Collection< EdgePolygon > polygons ) { + data = polys; + this.polygons = polygons; + + f = new JFrame( "Drawing Board" ); + f.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); + + f.add( this ); + + f.setSize( windowWidth, windowHeight ); + f.setVisible( true ); + f.setResizable( true ); + + f.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); + } + + public void drawPoint( double x, double y ) { + g.fillRect( ( int ) x * scale, ( int ) y * scale, scale, scale ); + } + + private static Vector2d rotate( Vector2d a, double angle ) { + double cos = Math.cos( angle ); + double sin = Math.sin( angle ); + double ax = a.getX() * cos - a.getY() * sin; + double ay = a.getX() * sin + a.getY() * cos; + return new Vector2d( ax, ay ); + } + + @Override + public void paintComponent( Graphics g ) { + super.paintComponent( g ); + this.g = g; + + g.setColor( new Color( 200, 200, 200 ) ); + g.drawLine( centerX, 0, centerX, 2000 ); + g.drawLine( 0, centerY, 2000, centerY ); + g.setColor( Color.BLACK ); + + if ( data != null ) { + Collection< Vertex > polygons = data; + + EdgeSet edges = new EdgeSet(); + for ( Vertex vert : polygons ) { + for ( HalfEdge edge : vert ) { + if ( edges.add( edge ) ) { + Point p1 = edge.getOrigin().getPosition(); + Point p2 = edge.getDest().getPosition(); + g.drawLine( + ( int ) ( p1.getX() * scale ) + centerX, + ( int ) ( p1.getY() * scale ) + centerY, + ( int ) ( p2.getX() * scale ) + centerX, + ( int ) ( p2.getY() * scale ) + centerY + ); + } + } + + final double diff = scale * 0.3; + final Point point = vert.getPosition(); + g.drawRect( ( int ) ( point.getX() * scale ) + centerX - ( int ) diff, ( int ) ( point.getY() * scale ) + centerY - ( int ) diff, ( int ) ( diff * 2 ), ( int ) ( diff * 2 ) ); + } + } + + if ( polygons != null ) { + System.out.println( "Polygon count: " + polygons.size() ); + g.setColor( Color.BLACK ); + for ( final EdgePolygon p : polygons ) { + final Collection< HalfEdge > edges = p.getEdges(); + + for ( HalfEdge edge : edges ) { + Point p1 = edge.getOrigin().getPosition(); + Point p2 = edge.getDest().getPosition(); + g.drawLine( + ( int ) ( p1.getX() * scale ) + centerX + 400, + ( int ) ( p1.getY() * scale ) + centerY, + ( int ) ( p2.getX() * scale ) + centerX + 400, + ( int ) ( p2.getY() * scale ) + centerY + ); + } + +// int size = edges.size(); +// int[] xPoints = new int[ size ]; +// int[] yPoints = new int[ size ]; +// +// HalfEdge edge = edges.iterator().next(); +// for ( int i = 0; i < edges.size(); ++i ) { +// final Point point = edge.getOrigin().getPosition(); +// xPoints[ i ] = ( int ) ( point.getX() * scale ) + centerX; +// yPoints[ i ] = ( int ) ( point.getY() * scale ) + centerY; +// } +// +// g.setColor( new Color( ( int ) ( Math.random() * 0xFFFFFF ) ) ); +// g.fillPolygon( xPoints, yPoints, size ); + } + + g.setColor( Color.BLACK ); + for ( final EdgePolygon p : polygons ) { + for ( final Vertex v : p.getVertices() ) { + final Point point = v.getPosition(); + + double diff = scale * .3; + g.drawRect( 400 + ( int ) ( point.getX() * scale ) + centerX - ( int ) diff, ( int ) ( point.getY() * scale ) + centerY - ( int ) diff, ( int ) ( diff * 2 ), ( int ) ( diff * 2 ) ); + } + } + } + +// Point[] points = { +// new Point( 0, -2 ), +// new Point( 1.5, -3.0 ), +// new Point( 0.0, -2.0 ), +// new Point( 0.9545454545454546, -1.3636363636363638 ), +// new Point( -0.8333333333333334, -1.0 ), +// new Point( 0.8333333333333334, -1.0 ), +// new Point( -0.5, 0.0 ), +// new Point( 0.5, 0.0 ), +// new Point( -0.16666666666666669, 1.0 ), +// new Point( 0.16666666666666669, 1.0 ), +// new Point( -0.16666666666666669, 1.0 ), +// new Point( 1, 1 ), +// new Point( -0.16666666666666669, 1.0 ), +// new Point( 0, 1.5 ), +// new Point( 0, 1.5 ), +// new Point( 0.16666666666666669, 1.0 ), +// new Point( -1, 2 ), +// new Point( 1, 2 ), +// }; +// +// int size = points.length >> 1; +// for ( int i = 0; i < size;i++ ) { +// Point a = points[ i << 1 ]; +// Point b = points[ ( i << 1 ) + 1 ]; +// +// g.drawLine( ( int ) ( a.x * scale ) + centerX, ( int ) ( a.y * scale ) + centerY, ( int ) ( b.x * scale ) + centerX, ( int ) ( b.y * scale ) + centerY ); +// } + } +} \ No newline at end of file diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/objects/mesh/MeshBuilder.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/objects/mesh/MeshBuilder.java index 5939a31..dc7faa3 100644 --- a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/objects/mesh/MeshBuilder.java +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/objects/mesh/MeshBuilder.java @@ -22,12 +22,6 @@ public class MeshBuilder { Optional< Vector > ref = facet.points.stream().filter( v -> v.lengthSquared() > 0.001 && Math.abs( v.clone().normalize().dot( facet.normal ) ) < .999999 ).findAny(); if ( ref.isPresent() ) { plane.point = ref.get(); - } else { - System.out.println( "Normal " + facet.normal ); - for ( Vector point : facet.points ) { - System.out.println( point ); - } - System.exit( 1 ); } plane.addShape( facet ); planes.add( plane ); diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/objects/mesh/Plane.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/objects/mesh/Plane.java index 578c584..191cbff 100644 --- a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/objects/mesh/Plane.java +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/objects/mesh/Plane.java @@ -7,8 +7,8 @@ import org.bukkit.util.Vector; import org.joml.Matrix3d; import org.joml.Vector3d; -import com.aaaaahhhhhhh.bananapuncher714.tess4j.Point; -import com.aaaaahhhhhhh.bananapuncher714.tess4j.Polygon; +import com.aaaaahhhhhhh.bananapuncher714.mesh.Point; +import com.aaaaahhhhhhh.bananapuncher714.mesh.Polygon; public class Plane { public Vector normal; @@ -17,30 +17,35 @@ public class Plane { public List< Polygon > polygons = new ArrayList< Polygon >(); public void addShape( Facet facet ) { - // TODO Flatten the polygons into 2d points - // Get the basis of this plane - Vector basis1 = normal.clone().multiply( point.clone().multiply( -1 ).dot( normal ) ).add( point ).normalize(); - Vector3d b1 = new Vector3d( basis1.getX(), basis1.getY(), basis1.getZ() ); - Vector3d b3 = new Vector3d( normal.getX(), normal.getY(), normal.getZ() ); - Vector3d b2 = new Vector3d( b1 ).cross( b3 ).normalize(); - Matrix3d mat = new Matrix3d( b1, b2, b3 ).transpose(); + Matrix3d mat = getProjectionMatrix(); List< Point > polygon = new ArrayList< Point >(); for ( Vector p : facet.points ) { double t = point.clone().subtract( p ).dot( normal ); + // TODO Subtract point or to not subtract point?!? Vector i = normal.clone().multiply( t ).add( p ).subtract( point ); Vector3d v = new Vector3d( i.getX(), i.getY(), i.getZ() ); Vector3d r = v.mul( mat ); - + polygon.add( new Point( r.x(), r.y() ) ); } - + polygons.add( new Polygon( polygon ) ); } public boolean matches( Vector normal, Vector point ) { return Math.abs( this.normal.dot( normal ) ) > 0.99 && Math.abs( this.point.clone().subtract( point ).dot( normal ) ) < 0.01; } + + private Matrix3d getProjectionMatrix() { + final Vector basis1 = normal.clone().multiply( point.clone().multiply( -1 ).dot( normal ) ).add( point ).normalize(); + final Vector3d b1 = new Vector3d( basis1.getX(), basis1.getY(), basis1.getZ() ); + final Vector3d b3 = new Vector3d( normal.getX(), normal.getY(), normal.getZ() ); + final Vector3d b2 = new Vector3d( b1 ).cross( b3 ).normalize(); + final Matrix3d mat = new Matrix3d( b1, b2, b3 ).transpose(); + + return mat; + } } diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/HalfWing.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/HalfWing.java index 63d3b29..3dfc455 100644 --- a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/HalfWing.java +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/HalfWing.java @@ -1,5 +1,7 @@ package com.aaaaahhhhhhh.bananapuncher714.tess4j; +import com.aaaaahhhhhhh.bananapuncher714.mesh.Vector2d; + public class HalfWing { protected HalfWing sym; protected HalfWing prev; diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Mesh.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Mesh.java deleted file mode 100644 index 9e460ff..0000000 --- a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Mesh.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.aaaaahhhhhhh.bananapuncher714.tess4j; - -import java.util.List; - -public class Mesh { - List< HalfEdge > edges; - - -} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Polygon.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Polygon.java deleted file mode 100644 index d502890..0000000 --- a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Polygon.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.aaaaahhhhhhh.bananapuncher714.tess4j; - -import java.util.List; - -public class Polygon { - List< Point > points; - - public Polygon( List< Point > points ) { - this.points = points; - } - - public List< Point > getPoints() { - return points; - } -} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Scratch.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Scratch.java index 3f13ddb..30a5e24 100644 --- a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Scratch.java +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Scratch.java @@ -1,55 +1,104 @@ package com.aaaaahhhhhhh.bananapuncher714.tess4j; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.WeakHashMap; + +import com.aaaaahhhhhhh.bananapuncher714.mesh.Vector2d; + public class Scratch { + public static class Test { + int value; + + Test( int v ) { + value = v; + } + + @Override + public String toString() { + return "Test(" + value + ")"; + } + } + public static void main( String[] args ) { + Map< Test, String > weakMap = new WeakHashMap< Test, String >(); { - String t = "Vertex{x=4.25,y=0.5}\r\n" + - "Vertex{x=2.75,y=-2.5}\r\n" + - "Vertex{x=2.2045454545454546,y=-0.8636363636363638}\r\n" + - "Vertex{x=1.75,y=0.5}\r\n" + - "Vertex{x=1.25,y=-1.5}\r\n" + - "Vertex{x=1.25,y=2.0}\r\n" + - "Vertex{x=1.0,y=-1.0}\r\n" + - "Vertex{x=1.0,y=0.0}\r\n" + - "Vertex{x=1.0,y=0.5}\r\n" + - "Vertex{x=1.0,y=1.0}\r\n" + - "Vertex{x=1.0,y=1.25}\r\n" + - "Vertex{x=1.0,y=3.0}\r\n" + - "Vertex{x=0.9166666666666666,y=1.0}\r\n" + - "Vertex{x=0.75,y=0.5}\r\n" + - "Vertex{x=0.7222222222222222,y=0.41666666666666674}\r\n" + - "Vertex{x=0.6666666666666667,y=0.5}\r\n" + - "Vertex{x=0.5833333333333333,y=0.0}\r\n" + - "Vertex{x=0.5,y=-1.0}\r\n" + - "Vertex{x=0.33333333333333337,y=1.0}\r\n" + - "Vertex{x=0.2954545454545454,y=-0.8636363636363636}\r\n" + - "Vertex{x=0.24999999999999994,y=-1.0}\r\n" + - "Vertex{x=0.0,y=-2.0}\r\n" + - "Vertex{x=0.0,y=1.5}\r\n" + - "Vertex{x=-0.050000000000000024,y=-1.9}\r\n" + - "Vertex{x=-0.25,y=-2.5}\r\n" + - "Vertex{x=-0.33333333333333337,y=1.0}\r\n" + - "Vertex{x=-0.5,y=-1.0}\r\n" + - "Vertex{x=-0.6666666666666667,y=0.5}\r\n" + - "Vertex{x=-0.9999999999999999,y=-2.220446049250313E-16}\r\n" + - "Vertex{x=-1.0,y=-1.0}\r\n" + - "Vertex{x=-1.0,y=0.0}\r\n" + - "Vertex{x=-1.0,y=0.5}\r\n" + - "Vertex{x=-1.0,y=1.0}\r\n" + - "Vertex{x=-1.0,y=2.0}\r\n" + - "Vertex{x=-1.0,y=3.0}\r\n" + - "Vertex{x=-1.75,y=0.5}\r\n" + - "Vertex{x=-2.0,y=-1.0}\r\n" + - "Vertex{x=-2.0,y=0.0}\r\n" + - "Vertex{x=-2.0,y=1.0}\r\n" + - "Vertex{x=-2.0,y=2.0}\r\n" + - "Vertex{x=-5.0,y=0.0}\r\n" + - "Vertex{x=-5.0,y=1.0}"; - String[] split = t.split( "\r\n" ); - for ( int i = split.length - 1; i >= 0; i-- ) { - System.out.println( split[ i ] ); + List< Test > values = new ArrayList< Test >(); + { + for ( int i = 0; i < 10000; ++i ) { + Test test = new Test(i); + weakMap.put( test, test.toString() ); + values.add( test ); + } + } + + System.out.println( "Size: " + weakMap.size() ); + for ( int i = 45; i < 55; ++i ) { + values.set( i, null ); + } + System.out.println( "Size: " + weakMap.size() ); + for ( int i = 0; i < 100; i++ ) { + weakMap.size(); + weakMap.isEmpty(); + } + values = null; + try { + Thread.sleep( 5000 ); + } catch ( InterruptedException e ) { + e.printStackTrace(); } } + System.out.println( "Size: " + weakMap.size() ); + +// { +// String t = "Vertex{x=4.25,y=0.5}\r\n" + +// "Vertex{x=2.75,y=-2.5}\r\n" + +// "Vertex{x=2.2045454545454546,y=-0.8636363636363638}\r\n" + +// "Vertex{x=1.75,y=0.5}\r\n" + +// "Vertex{x=1.25,y=-1.5}\r\n" + +// "Vertex{x=1.25,y=2.0}\r\n" + +// "Vertex{x=1.0,y=-1.0}\r\n" + +// "Vertex{x=1.0,y=0.0}\r\n" + +// "Vertex{x=1.0,y=0.5}\r\n" + +// "Vertex{x=1.0,y=1.0}\r\n" + +// "Vertex{x=1.0,y=1.25}\r\n" + +// "Vertex{x=1.0,y=3.0}\r\n" + +// "Vertex{x=0.9166666666666666,y=1.0}\r\n" + +// "Vertex{x=0.75,y=0.5}\r\n" + +// "Vertex{x=0.7222222222222222,y=0.41666666666666674}\r\n" + +// "Vertex{x=0.6666666666666667,y=0.5}\r\n" + +// "Vertex{x=0.5833333333333333,y=0.0}\r\n" + +// "Vertex{x=0.5,y=-1.0}\r\n" + +// "Vertex{x=0.33333333333333337,y=1.0}\r\n" + +// "Vertex{x=0.2954545454545454,y=-0.8636363636363636}\r\n" + +// "Vertex{x=0.24999999999999994,y=-1.0}\r\n" + +// "Vertex{x=0.0,y=-2.0}\r\n" + +// "Vertex{x=0.0,y=1.5}\r\n" + +// "Vertex{x=-0.050000000000000024,y=-1.9}\r\n" + +// "Vertex{x=-0.25,y=-2.5}\r\n" + +// "Vertex{x=-0.33333333333333337,y=1.0}\r\n" + +// "Vertex{x=-0.5,y=-1.0}\r\n" + +// "Vertex{x=-0.6666666666666667,y=0.5}\r\n" + +// "Vertex{x=-0.9999999999999999,y=-2.220446049250313E-16}\r\n" + +// "Vertex{x=-1.0,y=-1.0}\r\n" + +// "Vertex{x=-1.0,y=0.0}\r\n" + +// "Vertex{x=-1.0,y=0.5}\r\n" + +// "Vertex{x=-1.0,y=1.0}\r\n" + +// "Vertex{x=-1.0,y=2.0}\r\n" + +// "Vertex{x=-1.0,y=3.0}\r\n" + +// "Vertex{x=-1.75,y=0.5}\r\n" + +// "Vertex{x=-2.0,y=-1.0}\r\n" + +// "Vertex{x=-2.0,y=0.0}\r\n" + +// "Vertex{x=-2.0,y=1.0}\r\n" + +// "Vertex{x=-2.0,y=2.0}\r\n" + +// "Vertex{x=-5.0,y=0.0}\r\n" + +// "Vertex{x=-5.0,y=1.0}"; +// String[] split = t.split( "\r\n" ); +// for ( int i = split.length - 1; i >= 0; i-- ) { +// System.out.println( split[ i ] ); +// } +// } // { // System.out.println( new Vector2d( 1, 0 ).cross( new Vector2d( 1, 1 ) ) ); // @@ -299,14 +348,14 @@ public class Scratch { if ( a1.equals( b1 ) ) { return b2.subtracted( b1 ).cross( a2.subtracted( a1 ) ) >= 0; - } else if ( a1.x < b1.x ) { + } else if ( a1.getX() < b1.getX() ) { System.out.println( "LESS A" ); return b1.subtracted( a1 ).cross( a2.subtracted( a1 ) ) >= 0; - } else if ( a1.x > b1.x ) { + } else if ( a1.getX() > b1.getX() ) { System.out.println( "MORAY" ); return b2.subtracted( b1 ).cross( a1.subtracted( b1 ) ) >= 0; } else { - return a1.y > b1.y; + return a1.getY() > b1.getY(); } } diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Tess4j.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Tess4j.java index 3ab0258..611b9b5 100644 --- a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Tess4j.java +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Tess4j.java @@ -19,6 +19,9 @@ import java.util.TreeSet; import java.util.WeakHashMap; import java.util.function.Supplier; +import com.aaaaahhhhhhh.bananapuncher714.mesh.Point; +import com.aaaaahhhhhhh.bananapuncher714.mesh.Polygon; +import com.aaaaahhhhhhh.bananapuncher714.mesh.Vector2d; import com.aaaaahhhhhhh.bananapuncher714.tess4j.region.RegionSimple; import com.aaaaahhhhhhh.bananapuncher714.tess4j.region.RegionSimple.GluWindingRule; import com.aaaaahhhhhhh.bananapuncher714.tess4j.region.WindingRuleSimple; @@ -39,12 +42,12 @@ public class Tess4j< T extends Region > { private Vector2dComparator comparator = new Vector2dComparator() { @Override public double compareX( Vector2d a, Vector2d b ) { - return a.x - b.x; + return a.getX() - b.getX(); } @Override public double compareY( Vector2d a, Vector2d b ) { - return a.y - b.y; + return a.getY() - b.getY(); } }; @@ -69,7 +72,7 @@ public class Tess4j< T extends Region > { // Populate the vertex queue // TODO Ensure that the polygon has at least 3 points? HalfWing wing = null; - for ( Point point : polygon.points ) { + for ( Point point : polygon.getPoints() ) { if ( wing == null ) { // Make a self loop with one edge and one vertex wing = new HalfWing(); @@ -81,7 +84,7 @@ public class Tess4j< T extends Region > { } // Set the origin - wing.getOrigin().setPosition( new Vector2d( point.x, point.y ) ); + wing.getOrigin().setPosition( new Vector2d( point ) ); // TODO Temporary angle check // final double angle = 128; @@ -100,11 +103,19 @@ public class Tess4j< T extends Region > { } public void tessellate() { + StagePreTess stage = tessStage.get(); + Queue< Vertex > queue = stage.vertexQueue; + System.out.println( "Initial vertex count: " + queue.size() ); + System.out.println( "Initial vertices: " + ( stage.windingMap.size() / 2 ) ); + /* * Given a heap of vertices and edges, remove any conflicting intersections and form interior regions */ generateRegions(); + System.out.println( "After vertex count: " + queue.size() ); + System.out.println( "After vertices: " + ( stage.windingMap.size() / 2 ) ); + /* * Merge all adjacent collinear edges */ @@ -1478,27 +1489,14 @@ public class Tess4j< T extends Region > { Tess4j< RegionSimple > tess4j = new Tess4j< RegionSimple >( () -> { return new RegionSimple( GluWindingRule.ODD ); }, new Vector2dComparator() { @Override public double compareX( Vector2d a, Vector2d b ) { - return a.x - b.x; + return a.getX() - b.getX(); } @Override public double compareY( Vector2d a, Vector2d b ) { - return a.y - b.y; + return a.getY() - b.getY(); } } ); - tess4j.addPolygon( new Polygon( Arrays.asList( - new Point( -1, -1 ), - new Point( 1, -1 ), - new Point( 1, 1 ), - new Point( -1, 1 ) - ) ), new WindingRuleSimple() ); - tess4j.addPolygon( new Polygon( Arrays.asList( - new Point( -1, -1 ), - new Point( 1, -1 ), - new Point( 1, 1 ), - new Point( -1, 1 ) - ) ), new WindingRuleSimple() ); - // tess4j.addPolygon( new Polygon( Arrays.asList( // new Point( -1, -1 ), // new Point( 1, -1 ), @@ -1506,42 +1504,55 @@ public class Tess4j< T extends Region > { // new Point( -1, 1 ) // ) ), new WindingRuleSimple() ); // tess4j.addPolygon( new Polygon( Arrays.asList( -// new Point( -1, 1 ), -// new Point( 1, 1 ), -// new Point( 1, 3 ), -// new Point( -1, 3 ) -// ) ), new WindingRuleSimple() ); -// tess4j.addPolygon( new Polygon( Arrays.asList( -// new Point( -2, -1 ), // new Point( -1, -1 ), -// new Point( -1, 2 ), -// new Point( -2, 2 ) +// new Point( 1, -1 ), +// new Point( 1, 1 ), +// new Point( -1, 1 ) // ) ), new WindingRuleSimple() ); -// tess4j.addPolygon( new Polygon( Arrays.asList( -// new Point( -4, -2 ), -// new Point( -1, -2 ), -// new Point( -1, 1.5 ), -// new Point( -4, 1.5 ) -// ) ), new WindingRuleSimple() ); -// tess4j.addPolygon( new Polygon( Arrays.asList( -// new Point( -3, 0 ), -// new Point( 3, 0 ), -// new Point( -1.5, -3 ), -// new Point( 0, 1.5 ), -// new Point( 1.5, -3 ) -// ) ), new WindingRuleSimple() ); -// tess4j.addPolygon( new Polygon( Arrays.asList( -// new Point( -1, 0 ), -// new Point( 0, 1.5 ), -// new Point( 1, 0 ) -// ) ), new WindingRuleSimple() ); -// tess4j.addPolygon( new Polygon( Arrays.asList( -// new Point( 1, 0 ), -// new Point( 0, -2 ), -// new Point( -1, 0 ) -// ) ), new WindingRuleSimple() ); - tess4j.tessellate(); + tess4j.addPolygon( new Polygon( Arrays.asList( + new Point( -1, -1 ), + new Point( 1, -1 ), + new Point( 1, 1 ), + new Point( -1, 1 ) + ) ), new WindingRuleSimple() ); + tess4j.addPolygon( new Polygon( Arrays.asList( + new Point( -1, 1 ), + new Point( 1, 1 ), + new Point( 1, 3 ), + new Point( -1, 3 ) + ) ), new WindingRuleSimple() ); + tess4j.addPolygon( new Polygon( Arrays.asList( + new Point( -2, -1 ), + new Point( -1, -1 ), + new Point( -1, 2 ), + new Point( -2, 2 ) + ) ), new WindingRuleSimple() ); + tess4j.addPolygon( new Polygon( Arrays.asList( + new Point( -4, -2 ), + new Point( -1, -2 ), + new Point( -1, 1.5 ), + new Point( -4, 1.5 ) + ) ), new WindingRuleSimple() ); + tess4j.addPolygon( new Polygon( Arrays.asList( + new Point( -3, 0 ), + new Point( 3, 0 ), + new Point( -1.5, -3 ), + new Point( 0, 1.5 ), + new Point( 1.5, -3 ) + ) ), new WindingRuleSimple() ); + tess4j.addPolygon( new Polygon( Arrays.asList( + new Point( -1, 0 ), + new Point( 0, 1.5 ), + new Point( 1, 0 ) + ) ), new WindingRuleSimple() ); + tess4j.addPolygon( new Polygon( Arrays.asList( + new Point( 1, 0 ), + new Point( 0, -2 ), + new Point( -1, 0 ) + ) ), new WindingRuleSimple() ); + + tess4j.tessellate(); } /* @@ -1763,7 +1774,7 @@ public class Tess4j< T extends Region > { for ( HalfWing wing : poly ) { Vector2d v = wing.getOrigin().getPosition(); - points.add( new Point( v.x, v.y ) ); + points.add( v ); } filledPolygons.add( new Polygon( points ) ); diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Tess4jTest.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Tess4jTest.java index 7c0bb9a..70e02a7 100644 --- a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Tess4jTest.java +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Tess4jTest.java @@ -9,6 +9,9 @@ import javax.swing.JFrame; import javax.swing.JPanel; import javax.swing.SwingUtilities; +import com.aaaaahhhhhhh.bananapuncher714.mesh.Point; +import com.aaaaahhhhhhh.bananapuncher714.mesh.Polygon; +import com.aaaaahhhhhhh.bananapuncher714.mesh.Vector2d; import com.aaaaahhhhhhh.bananapuncher714.tess4j.region.RegionSimple; import com.aaaaahhhhhhh.bananapuncher714.tess4j.region.RegionSimple.GluWindingRule; import com.aaaaahhhhhhh.bananapuncher714.tess4j.region.WindingRuleSimple; @@ -114,14 +117,14 @@ public class Tess4jTest extends JPanel { public double compareX( Vector2d a, Vector2d b ) { a = rotate( a, 128 ); b = rotate( b, 128 ); - return a.x - b.x; + return a.getX() - b.getX(); } @Override public double compareY(Vector2d a, Vector2d b ) { a = rotate( a, 128 ); b = rotate( b, 128 ); - return b.y - a.y; + return b.getY() - a.getY(); } } ); // tess4j.addPolygon( new Polygon( Arrays.asList( @@ -195,21 +198,21 @@ public class Tess4jTest extends JPanel { System.out.println( "Polygon count" ); System.out.println( polygons.size() ); for ( Polygon p : polygons ) { - System.out.println( p.points.size() ); - int size = p.points.size(); + System.out.println( p.getPoints().size() ); + int size = p.getPoints().size(); int[] xPoints = new int[ size ]; int[] yPoints = new int[ size ]; - for ( int i = 0; i < p.points.size(); i++ ) { - Point point = p.points.get( i ); + for ( int i = 0; i < p.getPoints().size(); i++ ) { + Point point = p.getPoints().get( i ); - System.out.println( point.x + ", " + point.y ); - Vector2d pVec = new Vector2d( point.x, point.y ); + System.out.println( point.getX() + ", " + point.getY() ); + Vector2d pVec = new Vector2d( point.getX(), point.getY() ); pVec = rotate( pVec, 0 ); point = new Point( pVec.getX(), pVec.getY() ); - xPoints[ i ] = ( int ) ( point.x * scale ) + centerX; - yPoints[ i ] = 100 - ( int ) ( point.y * scale ) + centerY; + xPoints[ i ] = ( int ) ( point.getX() * scale ) + centerX; + yPoints[ i ] = 100 - ( int ) ( point.getY() * scale ) + centerY; } g.setColor( new Color( ( int ) ( Math.random() * 0xFFFFFF ) ) ); @@ -218,13 +221,13 @@ public class Tess4jTest extends JPanel { g.setColor( Color.BLACK ); for ( Polygon p : polygons ) { - for ( Point point : p.points ) { - Vector2d pVec = new Vector2d( point.x, point.y ); + for ( Point point : p.getPoints() ) { + Vector2d pVec = new Vector2d( point.getX(), point.getY() ); pVec = rotate( pVec, 0 ); point = new Point( pVec.getX(), pVec.getY() ); double diff = scale * .05; - g.drawRect( ( int ) ( point.x * scale ) + centerX - ( int ) diff, 100 - ( int ) ( point.y * scale ) + centerY - ( int ) diff, ( int ) ( diff * 2 ), ( int ) ( diff * 2 ) ); + g.drawRect( ( int ) ( point.getX() * scale ) + centerX - ( int ) diff, 100 - ( int ) ( point.getY() * scale ) + centerY - ( int ) diff, ( int ) ( diff * 2 ), ( int ) ( diff * 2 ) ); } } diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Tessellator.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Tessellator.java index 5edfc7a..bc6f83e 100644 --- a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Tessellator.java +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Tessellator.java @@ -5,7 +5,6 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; -import java.util.Optional; import java.util.PriorityQueue; import java.util.Queue; import java.util.Set; @@ -13,6 +12,9 @@ import java.util.WeakHashMap; import org.joml.Math; +import com.aaaaahhhhhhh.bananapuncher714.mesh.Point; +import com.aaaaahhhhhhh.bananapuncher714.mesh.Polygon; + public class Tessellator { // TODO Insecure private static VertexOld vertex; @@ -43,7 +45,7 @@ public class Tessellator { Set< VertexOld > vertexCollection = Collections.newSetFromMap( new WeakHashMap< VertexOld, Boolean >() ); for ( Polygon polygon : shapes ) { HalfEdge edge = null; - for ( Point point : polygon.points ) { + for ( Point point : polygon.getPoints() ) { if ( edge == null ) { // Make a self loop with one edge and one vertex edge = HalfEdge.makeEdge(); @@ -53,17 +55,17 @@ public class Tessellator { edge = edge.nextLeft; } - edge.origin.x = point.x; - edge.origin.y = point.y; + edge.origin.x = point.getX(); + edge.origin.y = point.getY(); // For now, assume reverse contours is false edge.winding = 1; edge.sym().winding = -1; - minX = Math.min( minX, point.x ); - minY = Math.min( minY, point.y ); - maxX = Math.max( maxX, point.x ); - maxY = Math.max( maxY, point.y ); + minX = Math.min( minX, point.getX() ); + minY = Math.min( minY, point.getY() ); + maxX = Math.max( maxX, point.getX() ); + maxY = Math.max( maxY, point.getY() ); vertexCollection.add( edge.origin ); vertexCollection.add( edge.sym().origin ); @@ -450,7 +452,7 @@ public class Tessellator { // Compare points if ( !y1.lessThanOrEqualTo( x2 ) ) { - point.x = ( y1.x + x2.x ) / 2; + point.setX( ( y1.x + x2.x ) / 2 ); } else if ( x2.lessThanOrEqualTo( y2 ) ) { double z1 = y1.verticalDistance( x1, x2 ); double z2 = x2.verticalDistance( y1, y2 ); @@ -458,7 +460,7 @@ public class Tessellator { z1 *= -1; z2 *= -1; } - point.x = interpolate( z1, y1.x, z2, x2.x ); + point.setX( interpolate( z1, y1.x, z2, x2.x ) ); } else { double z1 = y1.compareTo( x1, x2 ); double z2 = - y2.compareTo( x1, x2 ); @@ -466,7 +468,7 @@ public class Tessellator { z1 *= -1; z2 *= -1; } - point.x = interpolate( z1, y1.x, z2, y2.x ); + point.setX( interpolate( z1, y1.x, z2, y2.x ) ); } return point; } diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Util.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Util.java index 195e80e..ece49cb 100644 --- a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Util.java +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Util.java @@ -9,6 +9,8 @@ import java.util.Optional; import org.joml.Math; +import com.aaaaahhhhhhh.bananapuncher714.mesh.Point; + public class Util { protected static String toString( Object obj ) { StringBuilder builder = new StringBuilder(); @@ -142,8 +144,8 @@ public class Util { if ( qpr == 0 ) { // Do not need to calculate the distance between the two parallel lines since it is 0 final Vector2dOld midpoint = r.multiply( midt ).add( p ); - point.x = midpoint.x; - point.y = midpoint.y; + point.setX( midpoint.x ); + point.setY( midpoint.y ); } else { // Calculate the midpoint along pr final Vector2dOld midp = r.multiply( midt ).add( p ); @@ -152,8 +154,8 @@ public class Util { final Vector2dOld z = perp.multiply( qp.dot( perp ) / rr ); // Sum the components final Vector2dOld midpoint = midp.add( z.divide( 2.0 ) ); - point.x = midpoint.x; - point.y = midpoint.y; + point.setX( midpoint.x ); + point.setY( midpoint.y ); } } else { // The lines aren't really anywhere close to each other, so just find the midpoint between the 2 closest points @@ -162,8 +164,8 @@ public class Util { // Same as the first edge but the second edge may be inverted final Vector2dOld qv = ( inverted ^ t0 > 1 ) ? q : v; final Vector2dOld midpoint = pu.add( qv ).divide( 2.0 ); - point.x = midpoint.x; - point.y = midpoint.y; + point.setX( midpoint.x ); + point.setY( midpoint.y ); } } else { // Calculate t and u @@ -174,8 +176,8 @@ public class Util { if ( t >= 0 && t <= 1 && u >= 0 && u <= 1 ) { // Calculate the point of intersection final Vector2dOld intersection = r.multiply( t ).add( p ); - point.x = intersection.x; - point.y = intersection.y; + point.setX( intersection.x ); + point.setY( intersection.y ); } else { // No intersection, so calculate the closest point between the two segments // We have t and u, which we can use to find the closest endpoints @@ -185,8 +187,8 @@ public class Util { final Vector2dOld nearP = r.multiply( nearT ).add( p ); final Vector2dOld nearQ = s.multiply( nearU ).add( q ); - point.x = ( nearP.x + nearQ.x ) / 2.0; - point.y = ( nearP.y + nearQ.y ) / 2.0; + point.setX( ( nearP.x + nearQ.x ) / 2.0 ); + point.setY( ( nearP.y + nearQ.y ) / 2.0 ); } } return point; diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Vector2dComparator.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Vector2dComparator.java index b278563..7ba7c21 100644 --- a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Vector2dComparator.java +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Vector2dComparator.java @@ -2,6 +2,8 @@ package com.aaaaahhhhhhh.bananapuncher714.tess4j; import java.util.Comparator; +import com.aaaaahhhhhhh.bananapuncher714.mesh.Vector2d; + public abstract class Vector2dComparator implements Comparator< Vector2d > { public abstract double compareX( Vector2d a, Vector2d b ); public abstract double compareY( Vector2d a, Vector2d b ); diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Vector2dOld.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Vector2dOld.java index ee78593..a2da2b5 100644 --- a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Vector2dOld.java +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Vector2dOld.java @@ -1,5 +1,7 @@ package com.aaaaahhhhhhh.bananapuncher714.tess4j; +import com.aaaaahhhhhhh.bananapuncher714.mesh.Point; + public class Vector2dOld { final double x; final double y; diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Vertex.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Vertex.java index b9c4afa..c3d7730 100644 --- a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Vertex.java +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Vertex.java @@ -1,5 +1,7 @@ package com.aaaaahhhhhhh.bananapuncher714.tess4j; +import com.aaaaahhhhhhh.bananapuncher714.mesh.Vector2d; + public class Vertex { private HalfWing wing; private Vector2d position;