From 29a0c04fae0b69c965be7e34a6ea0f9e5c975e4b Mon Sep 17 00:00:00 2001 From: BananaPuncher714 Date: Fri, 23 May 2025 00:35:39 -0400 Subject: [PATCH] Fix inconsistencies Use linked hash set for reproducibility when required Split chain generation into three smaller methods Fixed error when certain chains skip certain vertices, specifically after splitting a partition Hide internal classes Add conversion to external classes when returning data Update the polygon viewer Update chain intersections in parallel Sort edges cleaner Resolve some TODOs --- .../bananapuncher714/mesh/Chain.java | 26 ++ .../bananapuncher714/mesh/Mesh.java | 377 ++++++++++++------ .../bananapuncher714/mesh/Point.java | 4 + .../bananapuncher714/mesh/Segment.java | 19 + .../minietest/MeshingTest2.java | 332 ++++++++------- 5 files changed, 480 insertions(+), 278 deletions(-) create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/Chain.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/Segment.java diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/Chain.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/Chain.java new file mode 100644 index 0000000..c863b16 --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/Chain.java @@ -0,0 +1,26 @@ +package com.aaaaahhhhhhh.bananapuncher714.mesh; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class Chain { + protected List< Point > points = new ArrayList< Point >(); + protected List< Chain > intersections = new ArrayList< Chain >(); + + public Point getStart() { + return points.get( 0 ); + } + + public Point getEnd() { + return points.get( points.size() - 1 ); + } + + public List< Point > getPoints() { + return Collections.unmodifiableList( points ); + } + + public List< Chain > getIntersections() { + return Collections.unmodifiableList( intersections ); + } +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/Mesh.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/Mesh.java index 11f5c20..09573b9 100644 --- a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/Mesh.java +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/Mesh.java @@ -8,6 +8,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; +import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -18,6 +19,7 @@ import java.util.Queue; import java.util.Set; import java.util.Stack; import java.util.TreeSet; +import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -67,6 +69,7 @@ import com.aaaaahhhhhhh.bananapuncher714.mesh.region.RegionRule; * The time complexity for the entire algorithm should be roughly O(n^2logn). * * TODO Remove guaranteed checks or add some compile time thing to remove + * TODO Update this description * * @author BananaPuncher714 */ @@ -86,8 +89,7 @@ public class Mesh< T extends Region > { protected MeshState state = MeshState.TRIANGULATION_READY; - // TODO Remove - public Collection< Chain > chains; + protected Collection< MeshEventHandler > handlers = new LinkedHashSet< MeshEventHandler >(); public Mesh( final Supplier< T > defaultRegionSupplier ) { this.defaultRegionSupplier = defaultRegionSupplier; @@ -120,11 +122,36 @@ public class Mesh< T extends Region > { } } - public Collection< Vertex > getVertices() { - return vertices; + public boolean addHandler( final MeshEventHandler handler ) { + return handlers.add( handler ); } - public int getRuleSize() { + public boolean removeHandler( final MeshEventHandler handler ) { + return handlers.remove( handler ); + } + + public Collection< MeshEventHandler > getHandlers() { + return Collections.unmodifiableCollection( handlers ); + } + + public Set< Point > getVertices() { + return vertices.parallelStream().map( v -> { return new Point( v.getPosition() ); } ).collect( Collectors.toSet() ); + } + + public Set< Segment > getEdges() { + final EdgeSet set = new EdgeSet(); + set.addAll( rules.keySet() ); + + final Map< Vertex, Point > pointMap = vertices.parallelStream().collect( Collectors.toMap( Function.identity(), v -> new Point( v.getPosition() ) ) ); + + return set.parallelStream().map( e -> new Segment( pointMap.get( e.getOrigin() ), pointMap.get( e.getDest() ) ) ).collect( Collectors.toSet() ); + } + + public int getVertexCount() { + return vertices.size(); + } + + public int getEdgeCount() { return rules.size(); } @@ -764,6 +791,7 @@ public class Mesh< T extends Region > { // Sort the edges around this vertex. private static List< HalfEdge > sort( Vertex vertex ) { + final Vector2d UP = new Vector2d( 0, 1 ); final List< HalfEdge > edges = new ArrayList< HalfEdge >(); for ( final HalfEdge edge : vertex ) { @@ -773,23 +801,22 @@ public class Mesh< T extends Region > { edges.add( edge ); } - // TODO Can really just figure this out based on the absolute angle? - // relative to the (0, 1) vector... // Sort the edges in a counter clockwise direction, // where 11:59 is the least, and 12:00 is the greatest Collections.sort( edges, ( e1, e2 ) -> { // This assumes that no edge has length of 0 - final boolean e1p = isPositive( e1 ); - final boolean e2p = isPositive( e2 ); - - if ( e1p ^ e2p ) { - return e1p ? 1 : -1; - } else if ( e1.getDest() == e2.getDest() ) { - return 0; - } else { - final double cross = e2.toVector2d().cross( e1.toVector2d() ); - return Double.compare( cross, 0 ); + final Vector2d vec1 = e1.toVector2d().normalize(); + final Vector2d vec2 = e2.toVector2d().normalize(); + double cross1 = vec1.angle( UP ); + if ( cross1 < 0 ) { + cross1 += 10; } + double cross2 = vec2.angle( UP ); + if ( cross2 < 0 ) { + cross2 += 10; + } + + return Double.compare( cross2, cross1 ); } ); // Only need to re-organize the edges if there are 3 or more @@ -891,9 +918,7 @@ public class Mesh< T extends Region > { return toRemove; } - // TODO Provide triangles - // and preferrably a simple polygon - public Collection< EdgePolygon > mesh() { + public Collection< Polygon > mesh() { if ( state != MeshState.TRIANGULATION_READY ) { throw new IllegalStateException( "Mesh has not been split into regions!" ); } @@ -904,7 +929,7 @@ public class Mesh< T extends Region > { final Map< Vertex, Vertex > newVertices = new HashMap< Vertex, Vertex >(); // Keep track of all newly added interior edges - final Collection< HalfEdge > edges = new HashSet< HalfEdge >(); + final Collection< HalfEdge > edges = new LinkedHashSet< HalfEdge >(); /* * We now want to create a completely separate PSLG so that we don't modify @@ -954,53 +979,67 @@ public class Mesh< T extends Region > { // Sort each vertex so that the edge they are pointing to is the least edge vertices.parallelStream().forEach( v -> sort( v ) ); - final Collection< Chain > chains = reduceCollinear2( vertices, edges ); - this.chains = new ArrayDeque< Chain >(); - for ( Chain chain : chains ) { - List< HalfEdge > newEdges = new ArrayList< HalfEdge >(); - for ( HalfEdge e : chain.links ) { - HalfEdge edge = new HalfEdge(); - edge.getOrigin().setPosition( e.getOrigin().getPosition() ); - edge.getDest().setPosition( e.getDest().getPosition() ); - newEdges.add( edge ); + final Collection< Link > allChains = findChains( vertices, edges ); + final Collection< Link > chains = maximizeChains( allChains ); + if ( !handlers.isEmpty() ) { + final Collection< Chain > newChains = new HashSet< Chain >(); + final Collection< Link > seen = new HashSet< Link >(); + + for ( final Link link : allChains ) { + if ( seen.add( link ) ) { + Link head = link; + while ( head.previous != null ) { + head = head.previous; + } + + final Chain chain = new Chain(); + Link previous; + do { + chain.points.add( new Point( head.getOrigin().getPosition() ) ); + seen.add( head ); + previous = head; + } while ( ( head = head.next ) != null ); + chain.points.add( new Point( previous.getMidpoint().getPosition() ) ); + chain.points.add( new Point( previous.getDest().getPosition() ) ); + newChains.add( chain ); + } } - this.chains.add( new Chain( newEdges ) ); + + handlers.forEach( h -> h.onChainGenerationEvent( newChains ) ); + + final Collection< Link > selectedSeen = new HashSet< Link >(); + final Collection< Chain > selectedChains = new HashSet< Chain >(); + for ( final Link link : chains ) { + if ( selectedSeen.add( link ) ) { + Link head = link; + while ( chains.contains( head.previous ) ) { + head = head.previous; + } + + final Chain chain = new Chain(); + Link previous; + do { + chain.points.add( new Point( head.getOrigin().getPosition() ) ); + selectedSeen.add( head ); + previous = head; + } while ( ( head = head.next ) != null && chains.contains( head ) ); + chain.points.add( new Point( previous.getMidpoint().getPosition() ) ); + chain.points.add( new Point( previous.getDest().getPosition() ) ); + selectedChains.add( chain ); + } + } + + handlers.forEach( h -> h.onPreChainMergeEvent( selectedChains ) ); } + mergeChains( chains, edges ); + return partitionMonotone( vertices, edges ).parallelStream() .map( Mesh::triangulate ) .flatMap( p -> p.parallelStream() ) .collect( Collectors.toSet() ); } - // TODO Make not static - public static class Chain { - List< HalfEdge > links; - Set< Chain > intersections = new HashSet< Chain >(); - Chain previous; - Chain next; - - Chain( final Collection< HalfEdge > links ) { - this.links = new ArrayList< HalfEdge >( links ); - } - - public Vertex getOrigin() { - return links.get( 0 ).getOrigin(); - } - - public Vertex getMidpoint() { - return links.get( 1 ).getOrigin(); - } - - public Vertex getDest() { - return links.get( 2 ).getOrigin(); - } - - public List< HalfEdge > getLinks() { - return links; - } - } - /* * This algorithm revolves around first generating chains, then sorting which chains and chain links to keep. * @@ -1015,7 +1054,7 @@ public class Mesh< T extends Region > { * * The complexity comes from analyzing which combinations of chain links provides the greatest reduction of vertices. */ - private static Collection< Chain > reduceCollinear2( final TreeSet< Vertex > vertices, final Collection< HalfEdge > interior ) { + private static Collection< Link > findChains( final TreeSet< Vertex > vertices, final Collection< HalfEdge > interior ) { final Vector2d CROSS = new Vector2d( 1, 0 ); // Set maximum and minimum cross values that won't occur naturally final double MAX_CROSS = 1.1; @@ -1092,7 +1131,7 @@ public class Mesh< T extends Region > { // Keep track of all chains that were formed in this current partition. // For all new chains, see what previous chains it may intersect with - Collection< Chain > chains = new HashSet< Chain >(); + Collection< Link > chains = new LinkedHashSet< Link >(); Partition( final HalfEdge lower, final HalfEdge upper ) { this.lower = lower; @@ -1133,7 +1172,7 @@ public class Mesh< T extends Region > { // and we don't really care about what comes after links.add( edge ); - final Chain chain = new Chain( links ); + final Link chain = new Link( links ); addChain( chain ); } @@ -1174,7 +1213,7 @@ public class Mesh< T extends Region > { // and we don't really care about what comes after links.add( edge ); - final Chain chain = new Chain( links ); + final Link chain = new Link( links ); addChain( chain ); } @@ -1263,7 +1302,7 @@ public class Mesh< T extends Region > { // and we don't really care about what comes after links.add( edge ); - final Chain chain = new Chain( links ); + final Link chain = new Link( links ); addChain( chain ); } } else if ( lowerCross > region.lower ) { @@ -1302,7 +1341,7 @@ public class Mesh< T extends Region > { // and we don't really care about what comes after links.add( edge ); - final Chain chain = new Chain( links ); + final Link chain = new Link( links ); addChain( chain ); } @@ -1360,30 +1399,34 @@ public class Mesh< T extends Region > { * Check all previous chains for intersections and stuff. This method * is unfortunately polynomial time(O(n^2)) and there's not much we can do about it. * - * Maybe parallelize the intersection checking though? We'd need to ensure that the - * intersection collection for each chain is synchronized, but otherwise entirely possible. - * - * TODO Good idea, now someone needs to implement that. - * * Also, just because the origin or destination may be the midpoint of another chain * does not necessarily mean they coincide with each other... such as if the origin * or endpoint are connected to the midpoint for one chain, but not the other */ - void addChain( final Chain chain ) { + void addChain( final Link chain ) { final Vertex chainOrigin = chain.getOrigin(); final Vertex chainMidpoint = chain.getMidpoint(); final Vertex chainDestination = chain.getDest(); - for ( final Chain other : chains ) { + for ( final Link other : chains ) { if ( other.getOrigin() == chainOrigin && other.getDest() == chainDestination ) { // Already added this chain, so break out early return; } } + // Synchronize the intersections for this chain so that we can check each + // other chain for intersection in parallel. The order of the intersections + // doesn't affect the reproducibility either, so it's fine. + final Collection< Link > intersections = Collections.synchronizedCollection( chain.intersections ); + + final HalfEdge firstEdge = chain.links.get( 0 ); + final HalfEdge secondEdge = chain.links.get( 1 ); + final Vector2d secondEdgeVec = secondEdge.toVector2d(); + final Vector2d p = chainOrigin.getPosition(); final Vector2d r = chainDestination.getPosition().subtracted( p ); - for ( final Chain other : chains ) { + chains.parallelStream().forEach( other -> { if ( other.getMidpoint() == chainOrigin && other.getDest() == chainMidpoint ) { // The new chain is a continuation of this chain other.next = chain; @@ -1391,8 +1434,56 @@ public class Mesh< T extends Region > { } else if ( compare( other.getDest(), chainOrigin ) > 0 ) { // Do the chains even overlap? if ( chainOrigin == other.getMidpoint() ) { - chain.intersections.add( other ); - other.intersections.add( chain ); + // In the case that the new chain's origin is another chain's midpoint, + // we need to check which direction the other chain is facing. + // This is because the new chain only counts as intersecting with the other + // chain if and only if the creation of this chain would prevent the other + // chain from being linked correctly. + + final Vector2d otherDirection = other.getDest().getPosition().subtracted( other.getOrigin().getPosition() ); + final double cross = otherDirection.cross( r ); + + boolean intersects = true; + final HalfEdge firstLink = other.links.get( 0 ); + final HalfEdge secondLink = other.links.get( 1 ); + if ( firstLink.getDest() == other.getMidpoint() ) { + // Is the first link a direct connection? + intersects = interior.contains( firstLink ) ^ cross < 0; + } else if ( secondLink.getDest() == other.getDest() ) { + // Is the second link a direct connection? + intersects = interior.contains( secondLink ) ^ cross < 0; + } else { + // The chain is comprised of only vertices... + intersects = otherDirection.cross( secondLink.toVector2d() ) < 0 ^ cross < 0; + } + + if ( intersects ) { + synchronized ( intersections ) { + intersections.add( other ); + } + other.intersections.add( chain ); + } + } else if ( other.getDest() == chainMidpoint ) { + // Likewise, the new chain's midpoint may be another existing chain's + // destination. We need to check for that case too. + final Vector2d otherDirection = other.getOrigin().getPosition().subtracted( other.getDest().getPosition() ).normalize(); + final double cross = r.cross( otherDirection ); + + boolean intersects = true; + if ( firstEdge.getDest() == chainMidpoint ) { + intersects = interior.contains( firstEdge ) ^ cross < 0; + } else if ( secondEdge.getDest() == chainDestination ) { + intersects = interior.contains( secondEdge ) ^ cross < 0; + } else { + intersects = r.cross( secondEdgeVec ) < 0 ^ cross < 0; + } + + if ( intersects ) { + synchronized ( intersections ) { + intersections.add( other ); + } + other.intersections.add( chain ); + } } else if ( other.getOrigin() != chainOrigin && other.getDest() != chainDestination ) { // Make sure the chains don't share the same origin or destination @@ -1411,20 +1502,23 @@ public class Mesh< T extends Region > { 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 // Allow for overlap - if ( t >= 0 && t <= 1 && u > -uTol && u < ( 1 + uTol ) ) { + if ( t > -tTol && t < ( 1 + tTol ) && u > -uTol && u < ( 1 + uTol ) ) { // The chains intersect! We don't care about where. - chain.intersections.add( other ); + synchronized ( intersections ) { + intersections.add( other ); + } other.intersections.add( chain ); } } } } - } + } ); chains.add( chain ); } @@ -1487,7 +1581,7 @@ public class Mesh< T extends Region > { final Map< HalfEdge, Partition > lowerEdgeMap = new HashMap< HalfEdge, Partition >(); // Keep track of all completed chain links that we have - final Collection< Chain > chains = new HashSet< Chain >(); + final Collection< Link > chains = new LinkedHashSet< Link >(); // Keep track of edges from bottom top final SortedEdgeCollection< HalfEdge > edgeCollection = new SortedEdgeCollection< HalfEdge >( Mesh::greaterThanOrEqualTo ); @@ -1620,30 +1714,21 @@ public class Mesh< T extends Region > { // Create a new visibility region final VisibilityRegion copyRegion = new VisibilityRegion( region.lowerEdge, region.upperEdge ); - // Update the new region's lower links - if ( region.lower != MIN_CROSS ) { - copyRegion.lower = region.lower; - copyRegion.links.put( region.lower, region.links.remove( region.lower ) ); - - // Obviously, whenever we remove a region from the links, we need to check - // if the region even has any more links... otherwise it's clearly dead and - // needs to be removed entirely. - if ( region.links.isEmpty() ) { - regionIt.remove(); - } - - region.lower = MIN_CROSS; - } - - // Inherit the upper region's links // This may cause duplication of chains, but // we should be able to check them when we add them // TODO Can this be simplified? // As in, can we prove that this chain will connect, while // the upper region's chain will not, or vice versa? + + // Copy the old region's lower and upper links + if ( region.lower != MIN_CROSS ) { + copyRegion.lower = region.lower; + copyRegion.links.put( region.lower, region.links.get( region.lower ) ); + } + + // Inherit the upper region's links if ( region.upper != MAX_CROSS ) { copyRegion.upper = region.upper; - copyRegion.links.put( region.upper, region.links.get( region.upper ) ); } @@ -1822,19 +1907,17 @@ public class Mesh< T extends Region > { throw new IllegalStateException( "Dangling partitions!" ); } - // Given a chain link, select the links which intersect the least, and remove any that it intersects with from the - // queue. Essentially, a greedy method, which is more or less guaranteed optimal results. - // This is also unfortunately an O(n^2) time algorithm that can't be simplified. - - // Keep track of all chains that we want - final Collection< Chain > selectedChains = new HashSet< Chain >(); - // Keep track of all remaining chains that needs to be scanned - final Collection< Chain > remainingChains = new HashSet< Chain >( chains ); + return chains; + } + + private static Collection< Link > maximizeChains( final Collection< Link > chains ) { + final Collection< Link > remainingChains = new LinkedHashSet< Link >( chains ); + final Collection< Link > selectedChains = new HashSet< Link >(); // Do a quick scan and remove any chains that have 0 intersections, since we can get // this done in linear time and all at once - for ( final Iterator< Chain > it = remainingChains.iterator(); it.hasNext(); ) { - final Chain chain = it.next(); + for ( final Iterator< Link > it = remainingChains.iterator(); it.hasNext(); ) { + final Link chain = it.next(); if ( chain.intersections.size() == 0 ) { selectedChains.add( chain ); @@ -1845,7 +1928,7 @@ public class Mesh< T extends Region > { // Chain links while ( !remainingChains.isEmpty() ) { // Find the chain with the least amount of intersections - final Chain least = remainingChains.parallelStream().min( ( c1, c2 ) -> { + final Link least = remainingChains.parallelStream().min( ( c1, c2 ) -> { return Integer.compare( c1.intersections.size(), c2.intersections.size() ); } ).get(); @@ -1861,14 +1944,17 @@ public class Mesh< T extends Region > { selectedChains.add( least ); } - final Collection< Chain > cs = new HashSet< Chain >( selectedChains ); - - while ( !selectedChains.isEmpty() ) { + return selectedChains; + } + + private static void mergeChains( final Collection< Link > selected, final Collection< HalfEdge > interior ) { + final Collection< Link > chains = new HashSet< Link >( selected ); + while ( !chains.isEmpty() ) { // Get a chain - Chain chain = selectedChains.iterator().next(); + Link chain = chains.iterator().next(); // Get the head of the chain - while ( selectedChains.contains( chain.previous ) ) { + while ( chains.contains( chain.previous ) ) { chain = chain.previous; } @@ -1893,9 +1979,9 @@ public class Mesh< T extends Region > { interior.add( right.getSym() ); } - Chain previousChain; + Link previousChain; do { - selectedChains.remove( chain ); + chains.remove( chain ); final HalfEdge head = chain.links.get( 0 ); final Vertex origin = chain.getOrigin(); @@ -2045,7 +2131,7 @@ public class Mesh< T extends Region > { } previousChain = chain; - } while ( ( chain = chain.next ) != null && selectedChains.contains( chain ) ); + } while ( ( chain = chain.next ) != null && chains.contains( chain ) ); // Terminate the chain final Vertex destination = previousChain.getDest(); @@ -2087,10 +2173,8 @@ public class Mesh< T extends Region > { left.getSym().setOrigin( destination ); } } - - return cs; } - + // Use the vertex's edge as the hint, basically because we have no better edge to start from private static HalfEdge findPrevious( final Vector2d vector, final Vertex vertex ) { return findPrevious( vector, vertex, vertex.getEdge() ); @@ -2111,6 +2195,8 @@ public class Mesh< T extends Region > { final Vector2d normalized = vector.normalized(); + int size = vertex.size(); + // This is a fairly simple use case, so compare absolute angles HalfEdge edge = hint; while ( true ) { @@ -2127,6 +2213,10 @@ public class Mesh< T extends Region > { lowest = angle; } edge = edge.getPrev(); + + if ( --size < 0 ) { + throw new IllegalStateException( "Not good vertex! With only " + vertex.size() ); + } } } @@ -2445,7 +2535,7 @@ public class Mesh< T extends Region > { return newPolygons; } - private static Collection< EdgePolygon > triangulate( final EdgePolygon polygon ) { + private static Collection< Polygon > triangulate( final EdgePolygon polygon ) { // Go down each monotone chain and connect the vertices where possible. // Implements the O(n) triangulation of a polygon as described in // Computation Geometry Algorithms and Applications 3rd Ed. @@ -2454,7 +2544,7 @@ public class Mesh< T extends Region > { if ( polygon.getEdges().size() < 3 || polygon.getVertices().size() < 3 ) { throw new IllegalStateException( "Invalid amount of verts/edges!" ); } else if ( polygon.getEdges().size() == 3 ) { - return Arrays.asList( polygon ); + return Arrays.asList( new Polygon( polygon.getVertices().stream().map( v -> new Point( v.getPosition() ) ).toList() ) ); } final Queue< HalfEdge > edges = new PriorityQueue< HalfEdge >( ( a, b ) -> { @@ -2552,23 +2642,22 @@ public class Mesh< T extends Region > { // Lazy solution, just sort the vertices // Eventually we will remove this toSort.parallelStream().forEach( v -> sort( v ) ); - - // TODO Convert to triangles or something? - final Collection< EdgePolygon > polygons = new ArrayDeque< EdgePolygon >(); + + final Collection< Polygon > polygons = new ArrayDeque< Polygon >(); final Set< HalfEdge > scanned = new HashSet< HalfEdge >(); for ( final HalfEdge edge : polygon.getEdges() ) { if ( scanned.add( edge ) ) { HalfEdge temp = edge; - EdgePolygon poly = new EdgePolygon(); + final List< Point > points = new ArrayList< Point >(); do { - poly.addEdge( temp ); + points.add( new Point( temp.getOrigin().getPosition() ) ); } while ( scanned.add( temp = temp.getNext() ) ); - if ( poly.getVertices().size() != 3 ) { - throw new IllegalStateException( "Not a triangle! " + poly.getVertices().size() ); + if ( points.size() != 3 ) { + throw new IllegalStateException( "Not a triangle: " + points.size() ); } - polygons.add( poly ); + polygons.add( new Polygon( points ) ); } } @@ -2702,7 +2791,7 @@ public class Mesh< T extends Region > { } } - public static class EdgePolygon { + private static class EdgePolygon { private Set< HalfEdge > edges = new HashSet< HalfEdge >(); private Set< Vertex > vertices = new HashSet< Vertex >(); @@ -2720,6 +2809,36 @@ public class Mesh< T extends Region > { } } + private static class Link { + List< HalfEdge > links; + Set< Link > intersections = new LinkedHashSet< Link >(); + Link previous; + Link next; + + Link( final Collection< HalfEdge > links ) { + this.links = new ArrayList< HalfEdge >( links ); + } + + Vertex getOrigin() { + return links.get( 0 ).getOrigin(); + } + + Vertex getMidpoint() { + return links.get( 1 ).getOrigin(); + } + + Vertex getDest() { + return links.get( 2 ).getOrigin(); + } + } + + + public static interface MeshEventHandler { + default void onChainGenerationEvent( final Collection< Chain > chains ) {}; + default void onPreChainMergeEvent( final Collection< Chain > chains ) {}; + default void onPartitionEvent( final Collection< Polygon > polygons ) {}; + } + protected enum MeshState { DIRTY, SIMPLIFIED, diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/Point.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/Point.java index bdf8d2d..c3f88b1 100644 --- a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/Point.java +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/Point.java @@ -8,6 +8,10 @@ package com.aaaaahhhhhhh.bananapuncher714.mesh; public class Point { protected double x, y; + public Point( final Point other ) { + this( other.x, other.y ); + } + public Point( double x, double y ) { this.x = x; this.y = y; diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/Segment.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/Segment.java new file mode 100644 index 0000000..7c483f9 --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/Segment.java @@ -0,0 +1,19 @@ +package com.aaaaahhhhhhh.bananapuncher714.mesh; + +public class Segment { + private final Point start; + private final Point end; + + public Segment( Point start, Point end ) { + this.start = start; + this.end = end; + } + + public Point getStart() { + return start; + } + + public Point getEnd() { + return end; + } +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/MeshingTest2.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/MeshingTest2.java index 35458b8..f92e825 100644 --- a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/MeshingTest2.java +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/MeshingTest2.java @@ -1,6 +1,8 @@ package com.aaaaahhhhhhh.bananapuncher714.minietest; +import java.awt.BorderLayout; import java.awt.Color; +import java.awt.FlowLayout; import java.awt.Font; import java.awt.Graphics; import java.awt.MouseInfo; @@ -22,20 +24,22 @@ import java.util.HashSet; import java.util.List; import java.util.Random; import java.util.Set; +import java.util.function.Function; +import javax.swing.JButton; 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.Chain; import com.aaaaahhhhhhh.bananapuncher714.mesh.HalfEdge; import com.aaaaahhhhhhh.bananapuncher714.mesh.Mesh; -import com.aaaaahhhhhhh.bananapuncher714.mesh.Mesh.Chain; -import com.aaaaahhhhhhh.bananapuncher714.mesh.Mesh.EdgePolygon; +import com.aaaaahhhhhhh.bananapuncher714.mesh.Mesh.MeshEventHandler; import com.aaaaahhhhhhh.bananapuncher714.mesh.Point; import com.aaaaahhhhhhh.bananapuncher714.mesh.Polygon; +import com.aaaaahhhhhhh.bananapuncher714.mesh.Segment; import com.aaaaahhhhhhh.bananapuncher714.mesh.Vector2d; import com.aaaaahhhhhhh.bananapuncher714.mesh.Vertex; import com.aaaaahhhhhhh.bananapuncher714.mesh.region.simple.RegionRuleWinding; @@ -69,11 +73,10 @@ public class MeshingTest2 extends JPanel { private double scale = 8; - private Collection< Vertex > data; - private Collection< EdgePolygon > polygons; + private PlaneData data; - // Temporary... Remove at some point - private static Collection< Chain > chains; + private int stringBoardX = -100; + private int stringBoardY = 20; public static void main( String[] args ) { if ( CHUNK_DIR.exists() && CHUNK_DIR.isDirectory() ) { @@ -91,28 +94,31 @@ public class MeshingTest2 extends JPanel { } } +// Collections.sort( planes, ( a, b ) -> Integer.compare( b.polygons.size(), a.polygons.size() ) ); +// draw = planes.get( 4 ); + // Select a random plane to draw // draw = planes.get( new Random().nextInt( planes.size() ) ); System.out.println( "Draw is " + draw ); // Attempt to mesh all planes - try { - test( planes ); - } catch ( PolygonException e ) { - draw = e.getPlane(); - System.out.println( "Draw is now " + draw ); - } +// try { +// test( planes ); +// } catch ( PolygonException e ) { +// draw = e.getPlane(); +// System.out.println( "Draw is now " + draw ); +// } - final Collection< Vertex > data = process( draw ); - final Collection< EdgePolygon > polys = triangulate( draw ); -// final Collection< EdgePolygon > polys = test(); + final PlaneData data = process( draw ); + final Collection< Polygon > polys = triangulate( draw ); +// final Collection< Polygon > polys = test(); System.out.println( "Polygon count: " + polys.size() ); SwingUtilities.invokeLater( new Runnable() { @Override public void run() { - new MeshingTest2( data, polys ); + new MeshingTest2( data ); } } ); break; @@ -190,7 +196,7 @@ public class MeshingTest2 extends JPanel { long start = System.currentTimeMillis(); mesh.simplify(); mesh.generateRegions(); - final Collection< EdgePolygon > polys = mesh.copyOf().mesh(); + final Collection< Polygon > polys = mesh.copyOf().mesh(); long end = System.currentTimeMillis(); System.out.println( plane.polygons.size() + " to " + polys.size() + ":\t " + ( end - start ) + "ms" ); } catch ( IllegalStateException e ) { @@ -202,8 +208,9 @@ public class MeshingTest2 extends JPanel { System.out.println( "Took " + ( processAllEnd - processAllStart ) + "ms to process " + planes.size() + " planes" ); } - private static Collection< Vertex > process( final Plane plane ) { + private static PlaneData process( final Plane plane ) { if ( plane != null ) { + final PlaneData data = new PlaneData(); System.out.println( "Norm:\t" + plane.normal ); System.out.println( "Ref:\t" + plane.point ); System.out.println( "Size:\t" + plane.polygons.size() ); @@ -214,30 +221,46 @@ public class MeshingTest2 extends JPanel { } System.out.println( "Initial vertex count: " + mesh.getVertices().size() ); - System.out.println( "Initial edge count: " + ( mesh.getRuleSize() / 2 ) ); + System.out.println( "Initial edge count: " + ( mesh.getEdgeCount() / 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 ) ); + System.out.println( "After simplify edge count: " + ( mesh.getEdgeCount() / 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 ) ); + data.vertices = mesh.getVertices(); + data.edges = mesh.getEdges(); + System.out.println( "After generating regions vertex count: " + mesh.getVertices().size() ); + System.out.println( "After generating regions edge count: " + ( mesh.getEdgeCount() / 2 ) ); + + mesh.addHandler( new MeshEventHandler() { + @Override + public void onChainGenerationEvent( Collection< Chain > chains ) { + data.chains = chains; + } + + @Override + public void onPreChainMergeEvent( Collection< Chain > chains ) { + data.selectedChains = chains; + } + } ); + + data.polygons = mesh.mesh(); System.out.println( "Took " + ( end - start ) + "ms" ); - return mesh.getVertices(); + return data; } else { System.out.println( "No data!" ); - return Collections.emptySet(); + return null; } } - private static Collection< EdgePolygon > triangulate( final Plane plane ) { + private static Collection< Polygon > triangulate( final Plane plane ) { if ( plane != null ) { System.out.println( "Norm:\t" + plane.normal ); System.out.println( "Ref:\t" + plane.point ); @@ -249,32 +272,28 @@ public class MeshingTest2 extends JPanel { } System.out.println( "Initial vertex count: " + mesh.getVertices().size() ); - System.out.println( "Initial edge count: " + ( mesh.getRuleSize() / 2 ) ); + System.out.println( "Initial edge count: " + ( mesh.getEdgeCount() / 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 ) ); + System.out.println( "After simplify edge count: " + ( mesh.getEdgeCount() / 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 ) ); + System.out.println( "After generating regions edge count: " + ( mesh.getEdgeCount() / 2 ) ); // Make sure this works mesh = mesh.copyOf(); - Collection< EdgePolygon > polys; + Collection< Polygon > polys = new HashSet< Polygon >(); try { polys = mesh.mesh(); - chains = mesh.chains; } catch ( IllegalStateException e ) { e.printStackTrace(); - - polys = mesh.mesh( ); - chains = mesh.chains; } long end = System.currentTimeMillis(); @@ -287,7 +306,7 @@ public class MeshingTest2 extends JPanel { } } - private static Collection< EdgePolygon > test() { + private static Collection< Polygon > test() { Mesh< RegionSimple > mesh = new Mesh< RegionSimple >( () -> { return new RegionSimple( GluWindingRule.ODD ); } ); // mesh.addPolygon( new Polygon( Arrays.asList( @@ -409,20 +428,37 @@ public class MeshingTest2 extends JPanel { // new Point( 2.8151429293905887, -2.747294403063162 ) // ) ), RegionRuleWinding.CLOCKWISE ); + mesh.addPolygon( new Polygon( Arrays.asList( + new Point( 0, 0 ), + new Point( 1, 0 ), + new Point( 1, 1 ), + new Point( 0, 1 ) + ) ), RegionRuleWinding.CLOCKWISE ); + mesh.addPolygon( new Polygon( Arrays.asList( + new Point( 0, 2 ), + new Point( 1, 2 ), + new Point( 1, 3 ), + new Point( 0, 3 ) + ) ), RegionRuleWinding.CLOCKWISE ); + mesh.addPolygon( new Polygon( Arrays.asList( + new Point( -1, -1 ), + new Point( 2, -1 ), + new Point( 2, 4 ), + new Point( -1, 4 ) + ) ), RegionRuleWinding.CLOCKWISE ); + mesh.simplify(); mesh.generateRegions(); try { - final Collection< EdgePolygon > polys = mesh.mesh(); - chains = mesh.chains; + final Collection< Polygon > polys = mesh.mesh(); return polys; } catch ( Exception e ) { e.printStackTrace(); } - final Collection< EdgePolygon > polys = mesh.mesh(); - chains = mesh.chains; + final Collection< Polygon > polys = mesh.mesh(); return polys; } @@ -526,9 +562,8 @@ public class MeshingTest2 extends JPanel { } } - public MeshingTest2( Collection< Vertex > polys, Collection< EdgePolygon > polygons ) { - data = polys; - this.polygons = polygons; + public MeshingTest2( PlaneData data ) { + this.data = data; f = new JFrame( "Drawing Board" ); f.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); @@ -596,6 +631,14 @@ public class MeshingTest2 extends JPanel { }; } ); +// final JPanel taskbar = new JPanel(); +// taskbar.setLayout( new FlowLayout() ); +// +// taskbar.add( new JButton( "Previous" ) ); +// taskbar.add( new JButton( "Next" ) ); +// +// f.add( taskbar, BorderLayout.NORTH ); + f.add( this ); f.setSize( windowWidth, windowHeight ); @@ -613,6 +656,11 @@ public class MeshingTest2 extends JPanel { return new Vector2d( ax, ay ); } + private void drawOnBoard( Graphics g, String str, int x, int y ) { + g.setFont( new Font( Font.MONOSPACED, Font.BOLD, ( int ) scale * 5 ) ); + g.drawString( str, ( int ) ( ( centerX + x + stringBoardX ) * scale ) + offsetX, ( int ) ( ( centerY - y + stringBoardY ) * scale ) + offsetY ); + } + @Override public void paintComponent( Graphics g ) { super.paintComponent( g ); @@ -624,131 +672,117 @@ public class MeshingTest2 extends JPanel { int highestX = 40; if ( data != null ) { - Collection< Vertex > polygons = data; - - highestX = ( int ) ( polygons.parallelStream() - .max( ( a, b ) -> { return Double.compare( a.getPosition().getX(), b.getPosition().getX() ); } ).get().getPosition().getX() + 40.5 ); - - 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 ) ( ( centerX + p1.getX() + highestX ) * scale ) + offsetX, - ( int ) ( ( centerY - p1.getY() ) * scale ) + offsetY, - ( int ) ( ( centerX + p2.getX() + highestX ) * scale ) + offsetX, - ( int ) ( ( centerY - p2.getY() ) * scale ) + offsetY - ); - } - } + if ( !data.edges.isEmpty() ) { + Collection< Point > points = data.vertices; - final double diff = scale * 0.15; - final Point point = vert.getPosition(); + highestX = ( int ) ( points.parallelStream() + .max( ( a, b ) -> { return Double.compare( a.getX(), b.getX() ); } ).get().getX() + 40.5 ); + g.setColor( Color.BLACK ); - g.drawRect( ( int ) ( ( centerX + point.getX() + highestX ) * scale ) - ( int ) diff + offsetX, ( int ) ( ( centerY - point.getY() ) * scale ) - ( int ) diff + offsetY, ( int ) ( diff * 2 ), ( int ) ( diff * 2 ) ); - } - } - - if ( polygons != null ) { - g.setColor( Color.BLACK ); - int count = 2; - Random random = new Random( hashCode() ); + for ( final Segment segment : data.edges ) { + Point p1 = segment.getStart(); + Point p2 = segment.getEnd(); + g.drawLine( + ( int ) ( ( centerX + p1.getX() + highestX ) * scale ) + offsetX, + ( int ) ( ( centerY - p1.getY() ) * scale ) + offsetY, + ( int ) ( ( centerX + p2.getX() + highestX ) * scale ) + offsetX, + ( int ) ( ( centerY - p2.getY() ) * scale ) + offsetY + ); + } + + for ( final Point point : points ) { + final double diff = scale * 0.15; + g.drawRect( ( int ) ( ( centerX + point.getX() + highestX ) * scale ) - ( int ) diff + offsetX, ( int ) ( ( centerY - point.getY() ) * scale ) - ( int ) diff + offsetY, ( int ) ( diff * 2 ), ( int ) ( diff * 2 ) ); + } + + drawOnBoard( g, "Edge count: " + data.edges.size(), 0, 0 ); + drawOnBoard( g, "Vertex count: " + points.size(), 0, -5 ); + } - final int spacing = 25; - for ( final EdgePolygon p : polygons ) { - final Collection< HalfEdge > edges = p.getEdges(); - - final Set< Vertex > verts = new HashSet< Vertex >(); - g.setColor( Color.BLACK ); - for ( HalfEdge edge : edges ) { - Point p1 = edge.getOrigin().getPosition(); - Point p2 = edge.getDest().getPosition(); - g.drawLine( - ( int ) ( ( centerX + p1.getX() + count * spacing ) * scale ) + offsetX, - ( int ) ( ( centerY - p1.getY() - 150 ) * scale ) + offsetY, - ( int ) ( ( centerX + p2.getX() + count * spacing ) * scale ) + offsetX, - ( int ) ( ( centerY - p2.getY() - 150 ) * scale ) + offsetY - ); - - g.drawLine( - ( int ) ( ( centerX + p1.getX() ) * scale ) + offsetX, - ( int ) ( ( centerY - p1.getY() - 150 ) * scale ) + offsetY, - ( int ) ( ( centerX + p2.getX() ) * scale ) + offsetX, - ( int ) ( ( centerY - p2.getY() - 150 ) * scale ) + offsetY - ); - - final double diff = scale * 0.15; - if ( !verts.add( edge.getOrigin() ) ) { - g.drawRect( ( int ) ( ( centerX + p1.getX() + count * spacing ) * scale ) - ( int ) diff + offsetX, ( int ) ( ( centerY - p1.getY() - 150 ) * scale ) - ( int ) diff + offsetY, ( int ) ( diff * 2 ), ( int ) ( diff * 2 ) ); + if ( data.polygons != null ) { + int count = 2; + Random random = new Random( hashCode() ); + + final int spacing = 25; + + for ( final Polygon p : data.polygons ) { + final List< Point > points = p.getPoints(); + final int[] x = new int[ points.size() ]; + final int[] y = new int[ points.size() ]; + final int[] xOffset = new int[ points.size() ]; + final int[] yOffset = new int[ points.size() ]; + final int[] filledX = new int[ points.size() ]; + final int[] filledY = new int[ points.size() ]; + for ( int i = 0; i < points.size(); ++i ) { + final Point point = points.get( i ); + x[ i ] = ( int ) ( ( centerX + point.getX() ) * scale ) + offsetX; + y[ i ] = ( int ) ( ( centerY - point.getY() - 150 ) * scale ) + offsetY; + xOffset[ i ] = ( int ) ( ( centerX + point.getX() + count * spacing ) * scale ) + offsetX; + yOffset[ i ] = ( int ) ( ( centerY - point.getY() - 150 ) * scale ) + offsetY; + filledX[ i ] = ( int ) ( ( centerX + point.getX() ) * scale ) + offsetX; + filledY[ i ] = ( int ) ( ( centerY - point.getY() ) * scale ) + offsetY; } - } - count++; - - int size = edges.size(); - int[] xPoints = new int[ size ]; - int[] yPoints = new int[ size ]; - - final Set< Vertex > verts2 = new HashSet< Vertex >(); - - HalfEdge edge = edges.iterator().next(); - for ( int i = 0; i < edges.size(); ++i ) { - final Point point = edge.getOrigin().getPosition(); - xPoints[ i ] = ( int ) ( ( centerX + point.getX() ) * scale ) + offsetX; - yPoints[ i ] = ( int ) ( ( centerY - point.getY() ) * scale ) + offsetY; - final double diff = scale * 0.15; - if ( !verts2.add( edge.getOrigin() ) ) { - g.setColor( Color.ORANGE ); - } else { - g.setColor( Color.BLACK ); - } - g.drawRect( ( int ) ( ( centerX + point.getX() + highestX ) * scale ) - ( int ) diff + offsetX, ( int ) ( ( centerY - point.getY() ) * scale ) - ( int ) diff + offsetY, ( int ) ( diff * 2 ), ( int ) ( diff * 2 ) ); - - edge = edge.getNext(); + g.setColor( Color.BLACK ); + g.drawPolygon( x, y, points.size() ); + g.drawPolygon( xOffset, yOffset, points.size() ); + g.setColor( new Color( random.nextInt( 0x1000000 ) ) ); + g.fillPolygon( filledX, filledY, points.size() ); + ++count; } - g.setColor( new Color( random.nextInt( 0x1000000 ) ) ); - g.fillPolygon( xPoints, yPoints, size ); + g.setColor( Color.MAGENTA ); + drawOnBoard( g, "Polygon count: " + data.polygons.size(), 0, -10 ); } - } - - if ( chains != null ) { - int i = 0; - for ( Chain chain : chains ) { - Point p1 = chain.getOrigin().getPosition(); - Point p2 = chain.getDest().getPosition(); + + if ( data.chains != null ) { g.setColor( Color.RED ); - g.drawLine( - ( int ) ( ( centerX + p1.getX() + highestX ) * scale ) + offsetX + i, - ( int ) ( ( centerY - p1.getY() ) * scale ) + offsetY + i, - ( int ) ( ( centerX + p2.getX() + highestX ) * scale ) + offsetX + i, - ( int ) ( ( centerY - p2.getY() ) * scale ) + offsetY + i - ); + for ( final Chain chain : data.chains ) { + final Point p1 = chain.getStart(); + final Point p2 = chain.getEnd(); + + g.drawLine( + ( int ) ( ( centerX + p1.getX() + highestX ) * scale ) + offsetX, + ( int ) ( ( centerY - p1.getY() ) * scale ) + offsetY, + ( int ) ( ( centerX + p2.getX() + highestX ) * scale ) + offsetX, + ( int ) ( ( centerY - p2.getY() ) * scale ) + offsetY + ); + } -// for ( HalfEdge e : chain.getLinks() ) { -// Point e1 = e.getOrigin().getPosition(); -// Point e2 = e.getSym().getOrigin().getPosition(); -// g.setColor( Color.BLUE ); -// g.drawLine( -// ( int ) ( ( centerX + e1.getX() + 40 ) * scale ) + offsetX + i + 1, -// ( int ) ( ( centerY - e1.getY() ) * scale ) + offsetY + i + 1, -// ( int ) ( ( centerX + e2.getX() + 40 ) * scale ) + offsetX + i + 1, -// ( int ) ( ( centerY - e2.getY() ) * scale ) + offsetY + i + 1 -// ); -// } -// i += 2; + drawOnBoard( g, "Chain count: " + data.chains.size(), 0, -15 ); + } + + if ( data.selectedChains != null ) { + g.setColor( Color.BLUE ); + for ( final Chain chain : data.selectedChains ) { + final Point p1 = chain.getStart(); + final Point p2 = chain.getEnd(); + + g.drawLine( + ( int ) ( ( centerX + p1.getX() + highestX ) * scale ) + offsetX, + ( int ) ( ( centerY - p1.getY() ) * scale ) + offsetY, + ( int ) ( ( centerX + p2.getX() + highestX ) * scale ) + offsetX, + ( int ) ( ( centerY - p2.getY() ) * scale ) + offsetY + ); + } + drawOnBoard( g, "Minimum chain count: " + data.selectedChains.size(), 0, -20 ); } } java.awt.Point p = MouseInfo.getPointerInfo().getLocation(); - java.awt.Point componentLocation = getLocation(); - SwingUtilities.convertPointToScreen( componentLocation, this ); + java.awt.Point componentLocation = getLocationOnScreen(); p = new java.awt.Point( p.x - componentLocation.x, p.y - componentLocation.y ); g.setColor( Color.RED ); g.setFont( new Font( Font.MONOSPACED, Font.BOLD, 30 ) ); g.drawString( "X: " + ( ( p.x - offsetX ) / scale - centerX ), 10, 30 ); g.drawString( "Y: " + ( - ( ( p.y - offsetY ) / scale - centerY ) ), 10, 60 ); } + + private static class PlaneData { + Collection< Point > vertices; + Collection< Segment > edges; + Collection< Chain > chains; + Collection< Chain > selectedChains; + Collection< Polygon > polygons; + } } \ No newline at end of file