From a1899a52b74926e09ebda3572f3f02b2406b00e9 Mon Sep 17 00:00:00 2001 From: BananaPuncher714 Date: Fri, 2 May 2025 00:29:08 -0400 Subject: [PATCH] Initial commit for historical purposes --- .gitignore | 6 + pom.xml | 73 + .../bananapuncher714/minietest/MeshTest.java | 194 ++ .../minietest/MeshingTest.java | 421 ++++ .../minietest/MiniePlugin.java | 768 +++++++ .../bananapuncher714/minietest/MinieTest.java | 37 + .../minietest/command/CommandBase.java | 58 + .../minietest/command/CommandOption.java | 25 + .../minietest/command/CommandParameters.java | 65 + .../minietest/command/CommandResult.java | 39 + .../minietest/command/SplitCommand.java | 27 + .../minietest/command/SubCommand.java | 231 +++ .../command/executor/CommandExecutable.java | 9 + .../executor/CommandExecutableMessage.java | 18 + .../command/executor/package-info.java | 8 + .../minietest/command/package-info.java | 6 + .../command/validator/InputValidator.java | 14 + .../validator/InputValidatorArguments.java | 37 + .../validator/InputValidatorBoolean.java | 47 + .../validator/InputValidatorChain.java | 44 + .../command/validator/InputValidatorInt.java | 38 + .../validator/InputValidatorPattern.java | 28 + .../validator/InputValidatorPlayer.java | 31 + .../validator/InputValidatorString.java | 38 + .../command/validator/package-info.java | 8 + .../validator/sender/SenderValidator.java | 7 + .../sender/SenderValidatorNotPlayer.java | 11 + .../sender/SenderValidatorPermission.java | 27 + .../sender/SenderValidatorPlayer.java | 11 + .../validator/sender/package-info.java | 8 + .../minietest/objects/ChunkLocation.java | 286 +++ .../minietest/objects/Component.java | 12 + .../minietest/objects/mesh/Facet.java | 11 + .../minietest/objects/mesh/LineSegment.java | 45 + .../minietest/objects/mesh/MeshBuilder.java | 36 + .../minietest/objects/mesh/Plane.java | 46 + .../minietest/util/BukkitUtil.java | 183 ++ .../minietest/util/FileUtil.java | 150 ++ .../tess4j/EdgeCollection.java | 143 ++ .../bananapuncher714/tess4j/EdgeDict.java | 128 ++ .../bananapuncher714/tess4j/Face.java | 19 + .../bananapuncher714/tess4j/HalfEdge.java | 249 +++ .../bananapuncher714/tess4j/HalfWing.java | 105 + .../bananapuncher714/tess4j/Mesh.java | 9 + .../bananapuncher714/tess4j/Point.java | 46 + .../bananapuncher714/tess4j/Polygon.java | 15 + .../bananapuncher714/tess4j/Region.java | 7 + .../bananapuncher714/tess4j/RegionOld.java | 18 + .../bananapuncher714/tess4j/Scratch.java | 367 ++++ .../bananapuncher714/tess4j/Tess4j.java | 1842 +++++++++++++++++ .../bananapuncher714/tess4j/Tess4jTest.java | 263 +++ .../bananapuncher714/tess4j/Tessellator.java | 506 +++++ .../bananapuncher714/tess4j/Util.java | 194 ++ .../bananapuncher714/tess4j/Vector2d.java | 206 ++ .../tess4j/Vector2dComparator.java | 17 + .../bananapuncher714/tess4j/Vector2dOld.java | 84 + .../bananapuncher714/tess4j/Vertex.java | 66 + .../bananapuncher714/tess4j/VertexOld.java | 104 + .../bananapuncher714/tess4j/WindingRule.java | 23 + .../tess4j/region/RegionSimple.java | 82 + .../tess4j/region/WindingRuleSimple.java | 36 + .../tess4j/vertex_reduction.txt | 41 + src/main/resources/README.md | 67 + src/main/resources/plugin.yml | 14 + 64 files changed, 7754 insertions(+) create mode 100644 .gitignore create mode 100644 pom.xml create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/MeshTest.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/MeshingTest.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/MiniePlugin.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/MinieTest.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/CommandBase.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/CommandOption.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/CommandParameters.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/CommandResult.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/SplitCommand.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/SubCommand.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/executor/CommandExecutable.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/executor/CommandExecutableMessage.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/executor/package-info.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/package-info.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/validator/InputValidator.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/validator/InputValidatorArguments.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/validator/InputValidatorBoolean.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/validator/InputValidatorChain.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/validator/InputValidatorInt.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/validator/InputValidatorPattern.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/validator/InputValidatorPlayer.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/validator/InputValidatorString.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/validator/package-info.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/validator/sender/SenderValidator.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/validator/sender/SenderValidatorNotPlayer.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/validator/sender/SenderValidatorPermission.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/validator/sender/SenderValidatorPlayer.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/validator/sender/package-info.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/objects/ChunkLocation.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/objects/Component.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/objects/mesh/Facet.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/objects/mesh/LineSegment.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/objects/mesh/MeshBuilder.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/objects/mesh/Plane.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/util/BukkitUtil.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/util/FileUtil.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/EdgeCollection.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/EdgeDict.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Face.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/HalfEdge.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/HalfWing.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Mesh.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Point.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Polygon.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Region.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/RegionOld.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Scratch.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Tess4j.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Tess4jTest.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Tessellator.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Util.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Vector2d.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Vector2dComparator.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Vector2dOld.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Vertex.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/VertexOld.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/WindingRule.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/region/RegionSimple.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/region/WindingRuleSimple.java create mode 100644 src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/vertex_reduction.txt create mode 100644 src/main/resources/README.md create mode 100644 src/main/resources/plugin.yml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e8c9928 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.classpath +.project +target +.settings +chunks +new_chunks diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..b217204 --- /dev/null +++ b/pom.xml @@ -0,0 +1,73 @@ + + 4.0.0 + com.aaaaahhhhhhh.bananapuncher714 + MinieTest + 0.0.1-SNAPSHOT + + + + org.spigotmc + spigot + 1.19.4-R0.1-SNAPSHOT + provided + + + com.github.stephengold + Minie + 9.0.1 + compile + + + org.scijava + native-lib-loader + 2.4.0 + compile + + + io.github.earcut4j + earcut4j + 2.2.2 + compile + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 2.3.2 + + 1.8 + 1.8 + + + + org.apache.maven.plugins + maven-assembly-plugin + + + package + + single + + + + + + com.aaaaahhhhhhh.bananapuncher714.minietest.MinieTest + + + + + jar-with-dependencies + + + + + + + + \ No newline at end of file diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/MeshTest.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/MeshTest.java new file mode 100644 index 0000000..881fd5d --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/MeshTest.java @@ -0,0 +1,194 @@ +package com.aaaaahhhhhhh.bananapuncher714.minietest; + +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.List; + +import org.bukkit.util.Vector; + +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 MeshTest { + private static final File BASE = new File( System.getProperty( "user.dir" ) ); + private static final File CHUNK_DIR = new File( BASE, "chunks" ); + + 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() ) { + doStuff( file ); + break; + } + } else { + System.err.println( "No such directory exists: " + CHUNK_DIR ); + } + } + + private static void doStuff( 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; + } catch ( IOException e ) { + e.printStackTrace(); + return; + } + + System.out.println( "Found " + boxes.size() + " boxes" ); + // Now that we have a bunch of bounding boxes, do whatever + + MeshBuilder builder = new MeshBuilder(); + int i = 0; + for ( AABB box : boxes ) { + for ( Facet facet : generateFacetsFor( box ) ) { + builder.addFacet( facet ); + } + } + + System.out.println( "Planes: " + builder.planes.size() ); + for ( Plane plane : builder.planes ) { + System.out.println( "Plane" ); + System.out.println( "Norm:\t" + plane.normal ); + System.out.println( "Ref:\t" + plane.point ); + System.out.println( "Size:\t" + plane.polygons.size() ); + } + + /* + * The general plan is as follows: + * - Create a new mesh object + * - Insert faces one by one until we have all the shapes we want + * - In this case, it happens to be perfect AABB + * - Process the faces + * - Sort them into their correct plane of existence, along with their normal + * - In this case, their unique plane can be defined by their normal, and a point + * - Use the first point of the polygon to compare + * - Use the smallest point we can find as the reference point + * - O(1) operation per face, I suppose + * - Need to check point and normal with tolerance which is why + * - Now that we have faces each in their assorted group, we can start to tessellate each one + * - Generate even/odd regions and remove any colinear edges + * - Save this as the pre-mesh + * - Now trianglate each group and put that into a mesh collision shape or gimpact shape or whatnot + * - Use an IndexedMesh with the two array constructor + * - Put into game and test + */ + } + + 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( p7 ); + facet.points.add( p8 ); + 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.ymax = ymin; + this.zmin = zmin; + + this.xmax = xmax; + this.ymax = ymax; + this.zmax = zmax; + } + } +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/MeshingTest.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/MeshingTest.java new file mode 100644 index 0000000..3f5cf02 --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/MeshingTest.java @@ -0,0 +1,421 @@ +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.List; + +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.SwingUtilities; + +import org.bukkit.util.Vector; + +import com.aaaaahhhhhhh.bananapuncher714.minietest.MeshTest.AABB; +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; + +public class MeshingTest 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 = 400; // 600 + private int centerY = 1000; // 400 + + private Graphics g; + + private int scale = 10; + + private List< Polygon > data; + + 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() ) { + List< Polygon > polys = mesh( file ); + SwingUtilities.invokeLater( new Runnable() { + @Override + public void run() { + new MeshingTest( polys ); + } + } ); + break; + } + } else { + System.err.println( "No such directory exists: " + CHUNK_DIR ); + } + } + + private static List< Polygon > 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 ); + } + } + + System.out.println( "Planes: " + builder.planes.size() ); + for ( Plane plane : builder.planes ) { + if ( plane.polygons.size() > 100 ) { + draw = plane; + break; + } + } + + for ( Plane plane : builder.planes ) { + try { + Tess4j< RegionSimple > tess4j = new Tess4j< RegionSimple >( () -> { return new RegionSimple( GluWindingRule.ODD ); } ); + for ( Polygon poly : plane.polygons ) { + tess4j.addPolygon( poly, new WindingRuleSimple() ); + } + tess4j.tessellate(); + } catch ( IllegalStateException e ) { + e.printStackTrace(); + + draw = plane; +// Tess4j< RegionSimple > tess4j = new Tess4j< RegionSimple >( () -> { return new RegionSimple( GluWindingRule.ODD ); } ); +// tess4j.setDebug( true ); +// System.out.println( "Polygon count: " + plane.polygons.size() ); +// for ( Polygon poly : plane.polygons ) { +// tess4j.addPolygon( poly, new WindingRuleSimple() ); +// } +// tess4j.tessellate(); + } + } + + if ( draw != null ) { + System.out.println( "Norm:\t" + draw.normal ); + System.out.println( "Ref:\t" + draw.point ); + System.out.println( "Size:\t" + draw.polygons.size() ); + + Tess4j< RegionSimple > tess4j = new Tess4j< RegionSimple >( () -> { return new RegionSimple( GluWindingRule.ODD ); } ); + tess4j.setDebug( true ); + for ( int i = 0; i < draw.polygons.size(); i++ ) { + if ( i >= 4 ) { + break; + } + + Polygon poly = draw.polygons.get( i ); + + System.out.println( "Adding polygon " + poly.getPoints().size() ); + for ( Point point : poly.getPoints() ) { + System.out.println( point.getX() + ", " + point.getY() ); + } + + tess4j.addPolygon( poly, new WindingRuleSimple() ); + } + + long start = System.currentTimeMillis(); + tess4j.tessellate(); + long end = System.currentTimeMillis(); + System.out.println( "Took " + ( end - start ) + "ms" ); + + return tess4j.getPolygons(); + } else { + System.out.println( "No data!" ); + } + + return new ArrayList< Polygon >(); + } + + 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( p7 ); + facet.points.add( p8 ); + 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 MeshingTest( List< Polygon > polys) { + data = polys; + + 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; + +// Vertex v = null; +// for ( int i = 0; i < 100; i++ ) { +// double deg = Math.random() * 360; +// double dist = Math.random() * 100 + 100; +// double x = dist * Math.cos( Math.toRadians( deg ) ); +// double y = dist * Math.sin( Math.toRadians( deg ) ); +// HalfWing edge = new HalfWing(); +// edge.getDest().setPosition( new Vector2d( x, y ) ); +// if ( v == null ) { +// v = edge.getOrigin(); +// } else { +// HalfWing.splice( edge, v.getWing() ); +// } +// } +// v.update(); +// +// HalfWing edge = v.getWing(); +// int i = 0; +// double ic = 0; +// do { +// Vector2d dest = edge.getDest().getPosition(); +// g.drawLine( centerX, centerY, ( int ) dest.getX() + centerX, ( int ) dest.getY() + centerY ); +// dest = dest.normalized(); +// g.drawString( i++ + "", ( int ) ( dest.x * 200 * ( ic + 1.1 ) ) + centerX, ( int ) ( dest.y * 200 * ( ic + 1.1 ) ) + centerY ); +// ic = ( ic + 0.07 ) % .7; +// edge = edge.getPrev(); +// } while ( edge != v.getWing() ); +// +// v.sort(); +// +// g.drawLine( centerX + 700, centerY, centerX + 700, centerY - 400 ); +// edge = v.getWing(); +// i = 0; +// ic = 0; +// do { +// Vector2d dest = edge.getDest().getPosition(); +// g.drawLine( centerX + 700, centerY, ( int ) dest.getX() + centerX + 700, ( int ) dest.y + centerY ); +// dest = dest.normalized(); +// g.drawString( i++ + "", ( int ) ( dest.x * 200 * ( ic + 1.1 ) ) + centerX + 700, ( int ) ( dest.y * 200 * ( ic + 1.1 ) ) + centerY ); +// ic = ( ic + 0.07 ) % .7; +// edge = edge.getPrev(); +// } while ( edge != v.getWing() ); + + List< Polygon > polygons = data; + + System.out.println( "Polygon count: " + polygons.size() ); + System.out.println( polygons.size() ); + for ( Polygon p : polygons ) { + List< Point > points = p.getPoints(); +// System.out.println( points.size() ); + int size = points.size(); + int[] xPoints = new int[ size ]; + int[] yPoints = new int[ size ]; + + for ( int i = 0; i < points.size(); i++ ) { + Point point = points.get( i ); + +// 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.getX() * scale ) + centerX; + yPoints[ i ] = 100 - ( int ) ( point.getY() * scale ) + centerY; + } + + g.setColor( new Color( ( int ) ( Math.random() * 0xFFFFFF ) ) ); + g.fillPolygon( xPoints, yPoints, size ); + } + + g.setColor( Color.BLACK ); + for ( Polygon p : polygons ) { + 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.getX() * scale ) + centerX - ( int ) diff, 100 - ( int ) ( point.getY() * scale ) + centerY - ( int ) diff, ( int ) ( diff * 2 ), ( int ) ( diff * 2 ) ); + } + } + + g.drawLine( centerX, 0, centerX, 2000 ); + g.drawLine( 0, centerY + 100, 2000, centerY + 100 ); + +// 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/MiniePlugin.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/MiniePlugin.java new file mode 100644 index 0000000..b2699bc --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/MiniePlugin.java @@ -0,0 +1,768 @@ +package com.aaaaahhhhhhh.bananapuncher714.minietest; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import org.apache.commons.lang.SystemUtils; +import org.bukkit.Bukkit; +import org.bukkit.Chunk; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.block.BlockState; +import org.bukkit.block.data.BlockData; +import org.bukkit.craftbukkit.v1_21_R4.CraftWorld; +import org.bukkit.craftbukkit.v1_21_R4.block.data.CraftBlockData; +import org.bukkit.entity.BlockDisplay; +import org.bukkit.entity.Display; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.world.ChunkLoadEvent; +import org.bukkit.event.world.ChunkUnloadEvent; +import org.bukkit.inventory.ItemStack; +import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.util.BoundingBox; +import org.bukkit.util.Transformation; +import org.bukkit.util.Vector; +import org.joml.Matrix4f; +import org.joml.Quaternionf; + +import com.aaaaahhhhhhh.bananapuncher714.minietest.command.SubCommand; +import com.aaaaahhhhhhh.bananapuncher714.minietest.command.executor.CommandExecutableMessage; +import com.aaaaahhhhhhh.bananapuncher714.minietest.command.validator.InputValidatorInt; +import com.aaaaahhhhhhh.bananapuncher714.minietest.command.validator.sender.SenderValidatorPlayer; +import com.aaaaahhhhhhh.bananapuncher714.minietest.objects.ChunkLocation; +import com.aaaaahhhhhhh.bananapuncher714.minietest.util.FileUtil; +import com.jme3.bullet.PhysicsSpace; +import com.jme3.bullet.PhysicsSpace.BroadphaseType; +import com.jme3.bullet.RotationOrder; +import com.jme3.bullet.collision.ContactListener; +import com.jme3.bullet.collision.PhysicsCollisionEvent; +import com.jme3.bullet.collision.PhysicsCollisionListener; +import com.jme3.bullet.collision.PhysicsCollisionObject; +import com.jme3.bullet.collision.shapes.BoxCollisionShape; +import com.jme3.bullet.collision.shapes.CollisionShape; +import com.jme3.bullet.collision.shapes.CompoundCollisionShape; +import com.jme3.bullet.collision.shapes.PlaneCollisionShape; +import com.jme3.bullet.collision.shapes.SphereCollisionShape; +import com.jme3.bullet.joints.New6Dof; +import com.jme3.bullet.joints.PhysicsJoint; +import com.jme3.bullet.joints.motors.MotorParam; +import com.jme3.bullet.objects.PhysicsBody; +import com.jme3.bullet.objects.PhysicsRigidBody; +import com.jme3.math.Matrix3f; +import com.jme3.math.Plane; +import com.jme3.math.Quaternion; +import com.jme3.math.Vector3f; + +import net.minecraft.core.BlockPosition; +import net.minecraft.world.phys.shapes.VoxelShape; + +public class MiniePlugin extends JavaPlugin { + private static final int TIME_STEPS = 5; + private static final float SIMULATION_SPEED = 1f; + + private PhysicsSpace space; + private boolean paused = false; + private boolean teleportOnDeactivate = false; + private float tick = 0; + private Map< PhysicsCollisionObject, AuxiliaryDisplayStruct > linkedDisplays = new HashMap< PhysicsCollisionObject, AuxiliaryDisplayStruct >(); + + private Set< PhysicsCollisionObject > active = new HashSet< PhysicsCollisionObject >(); + private Set< PhysicsCollisionObject > isTnt = new HashSet< PhysicsCollisionObject >(); + + private Map< PhysicsJoint, JointStruct > joints = new HashMap< PhysicsJoint, JointStruct >(); + + private Map< ChunkLocation, PhysicsRigidBody > chunks = new HashMap< ChunkLocation, PhysicsRigidBody >(); + + private Map< Vector3f, CollisionShape > collisionShapes = new HashMap< Vector3f, CollisionShape >(); + + @Override + public void onEnable() { + FileUtil.saveToFile( getResource( "native/windows/x86_64/bulletjme.dll" ), new File( getDataFolder() + "/lib", "bulletjme.dll" ), false ); + FileUtil.saveToFile( getResource( "native/osx/x86_64/libbulletjme.dylib" ), new File( getDataFolder() + "/lib", "libbulletjme.dylib" ), false ); + FileUtil.saveToFile( getResource( "native/linux/x86_64/libbulletjme.so" ), new File( getDataFolder() + "/lib", "libbulletjme.so" ), false ); + if ( !loadNativeLibraries() ) { + getLogger().severe( "Unable to load Bullet JME native libraries! Disabling plugin" ); + Bukkit.getPluginManager().disablePlugin( this ); + return; + } + + registerCommands(); + + space = new PhysicsSpace( BroadphaseType.DBVT ); + PlaneCollisionShape plane = new PlaneCollisionShape( new Plane( new Vector3f( 0, 1, 0 ), 0 ) ); + PhysicsRigidBody planeBody = new PhysicsRigidBody( plane, PhysicsBody.massForStatic ); + planeBody.setPhysicsLocation( new Vector3f( 0, 128, 0 ) ); + space.addCollisionObject( planeBody ); + space.setAccuracy( 1f / ( TIME_STEPS * 20f ) ); + + space.addContactListener( new ContactListener() { + @Override + public void onContactEnded( long manifoldId ) { + + } + + @Override + public void onContactProcessed( PhysicsCollisionObject objA, PhysicsCollisionObject objB, long manifoldPointId ) { + if ( isTnt.remove( objA ) ) { + impulse( objA.getPhysicsLocation(), 30f ); + if ( linkedDisplays.containsKey( objA ) ) { + linkedDisplays.get( objA ).display.remove(); + } + } + + if ( isTnt.remove( objB ) ) { + impulse( objB.getPhysicsLocation(), 30f ); + if ( linkedDisplays.containsKey( objB ) ) { + linkedDisplays.get( objB ).display.remove(); + } + } + } + + @Override + public void onContactStarted( long manifoldId ) { + + } + } ); + + Bukkit.getPluginManager().registerEvents( new Listener() { + @EventHandler + private void onChunkLoad( ChunkLoadEvent event ) { + if ( event.getWorld().getName().equalsIgnoreCase( "world" ) ) { + getLogger().info( "Loading chunk..." ); +// scanAndGenerate( event.getChunk() ); + saveChunk( event.getChunk() ); + } + } + + @EventHandler + private void onChunkUnload( ChunkUnloadEvent event ) { + if ( event.getWorld().getName().equalsIgnoreCase( "world" ) ) { + ChunkLocation loc = new ChunkLocation( event.getChunk() ); + if ( chunks.containsKey( loc ) ) { + space.removeCollisionObject( chunks.get( loc ) ); + chunks.remove( loc ); + } + } + } + }, this ); + + + getLogger().info( "There are " + Bukkit.getWorld( "world" ).getLoadedChunks().length + " chunks" ); + getLogger().info( "Converted " + saveAllChunks( Bukkit.getWorld( "world" ) ) ); + + Bukkit.getScheduler().runTaskTimer( this, () -> { + if ( !paused ) { + // Update the real world with the physics space objects + space.distributeEvents(); + space.update( tick, TIME_STEPS ); + + for ( Iterator< Entry< PhysicsJoint, JointStruct > > it = joints.entrySet().iterator(); it.hasNext(); ) { + Entry< PhysicsJoint, JointStruct > entry = it.next(); + PhysicsJoint joint = entry.getKey(); + JointStruct struct = entry.getValue(); + BlockState state = struct.state; + + if ( state.getBlock().getType() != state.getType() || !state.getChunk().isLoaded() ) { + space.removeJoint( joint ); + space.remove( struct.object ); + it.remove(); + } else if ( !joint.isEnabled() ) { + Location loc = state.getLocation(); + BlockData displayData = state.getBlockData(); + + // Create the display + BlockDisplay display = loc.getWorld().spawn( loc, BlockDisplay.class, d -> { + d.setBlock( displayData ); + } ); + + linkedDisplays.put( struct.object, new AuxiliaryDisplayStruct( display, struct.offset ) ); + + state.getBlock().setType( Material.AIR ); + + struct.object.setProtectGravity( false ); + Vector3f grav = new Vector3f(); + space.setGravity( grav ); + struct.object.setGravity( grav ); + + space.removeJoint( joint ); + it.remove(); + } + } + + for ( Iterator< Entry< PhysicsCollisionObject, AuxiliaryDisplayStruct > > it = linkedDisplays.entrySet().iterator(); it.hasNext(); ) { + Entry< PhysicsCollisionObject, AuxiliaryDisplayStruct > entry = it.next(); + PhysicsCollisionObject obj = entry.getKey(); + AuxiliaryDisplayStruct disp = entry.getValue(); + + if ( !disp.display.isValid() ) { + it.remove(); + space.removeCollisionObject( obj ); + active.remove( obj ); + isTnt.remove( obj ); + + continue; + } + + Location displayLocation = disp.display.getLocation(); + + // Do not calculate for this object if it is inactive + if ( !obj.isActive() ) { + if ( teleportOnDeactivate && active.remove( obj ) ) { + // Newly deactivated object, teleport the display to the actual position to prevent the translation from getting too large + + Transformation currentTransformation = disp.display.getTransformation(); + org.joml.Vector3f trans = currentTransformation.getTranslation(); + + displayLocation.add( trans.x(), trans.y(), trans.z() ); + disp.display.teleport( displayLocation ); + + Transformation newTrans = new Transformation( new org.joml.Vector3f( 0, 0, 0 ), currentTransformation.getLeftRotation(), currentTransformation.getScale(), currentTransformation.getRightRotation() ); + disp.display.setInterpolationDelay( 0 ); + disp.display.setInterpolationDuration( 0 ); + disp.display.setTransformation( newTrans ); + } + + continue; + } else { + active.add( obj ); + } + + Vector3f location = obj.getPhysicsLocation(); + Quaternion rotation = new Quaternion(); + obj.getPhysicsRotation( rotation ); + + Vector offsetVec = disp.offset; + Vector scale = disp.scale; + Matrix4f transformRot = new Matrix4f(); + transformRot.set( new Quaternionf( rotation.getX(), rotation.getY(), rotation.getZ(), rotation.getW() ) ); + + transformRot.mul( 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, - ( float ) offsetVec.getX(), - ( float ) offsetVec.getY(), - ( float ) offsetVec.getZ(), 1 ); + transformRot.mul( ( float ) scale.getX(), 0, 0, 0, 0, ( float ) scale.getY(), 0, 0, 0, 0, ( float ) scale.getZ(), 0, 0, 0, 0, 1 ); + Matrix4f transMat = new Matrix4f( 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, ( float ) ( location.getX() - displayLocation.getX() ), ( float ) ( location.getY() - displayLocation.getY() ), ( float ) ( location.getZ() - displayLocation.getZ() ), 1 ); + + disp.display.setInterpolationDelay( 0 ); + disp.display.setInterpolationDuration( 2 ); + disp.display.setTransformationMatrix( transMat.mul( transformRot ) ); + } + + tick += SIMULATION_SPEED / 20f; + } + }, 0, 1 ); + } + + private void registerCommands() { + new SubCommand( "minie" ) + .addSenderValidator( new SenderValidatorPlayer() ) + .add( new SubCommand( "spawn" ) + .defaultTo( ( sender, args, params ) -> { + Player player = ( Player ) sender; + Location loc = player.getLocation(); + loc.setPitch( 0 ); + loc.setDirection( new Vector( 0, 0, 0 ) ); + + BlockData displayData; + ItemStack item = player.getInventory().getItemInMainHand(); + if ( item != null && item.getType() != Material.AIR && item.getType().isBlock() ) { + displayData = item.getType().createBlockData(); + } else { + displayData = Material.TNT.createBlockData(); + } + + CraftBlockData data = ( CraftBlockData ) displayData; + + BoundingBox[] boxes = convertFrom( data.getState().f( null, null ) ); + Vector blockCenter = calculateCenter( boxes ); + + CollisionShape box; + if ( boxes.length == 0 ) { + player.sendMessage( "No bounding box detected! Wrong method?" ); + return; + } + + if ( boxes.length == 1 ) { + Vector min = boxes[ 0 ].getMin(); + Vector center = boxes[ 0 ].getCenter().subtract( min ); + box = new BoxCollisionShape( ( float ) center.getX(), ( float ) center.getY(), ( float ) center.getZ() ); + } else { + CompoundCollisionShape compound = new CompoundCollisionShape(); + for ( BoundingBox aabb : boxes ) { + Vector min = aabb.getMin(); + Vector center = aabb.getCenter(); + Vector half = center.clone().subtract( min ); + center.subtract( blockCenter ); + CollisionShape subShape = new BoxCollisionShape( ( float ) half.getX(), ( float ) half.getY(), ( float ) half.getZ() ); + compound.addChildShape( subShape, ( float ) center.getX(), ( float ) center.getY(), ( float ) center.getZ() ); + } + box = compound; + } + + PhysicsRigidBody rigid = new PhysicsRigidBody( box, 1f ); + + // Since the center of the rigid body and the box shape are different, we need to offset the location + rigid.setPhysicsLocation( new Vector3f( ( float ) ( loc.getX() + blockCenter.getX() ), ( float ) ( loc.getY() + blockCenter.getY() ), ( float ) ( loc.getZ() + blockCenter.getZ() ) ) ); + + // Convert the display direction vector to a quaternion + rigid.setPhysicsRotation( new Quaternion( 0, 0, 0, 1 ) ); + + space.addCollisionObject( rigid ); + + // Create the display + BlockDisplay display = loc.getWorld().spawn( loc, BlockDisplay.class, d -> { + d.setBlock( displayData ); + } ); + + linkedDisplays.put( rigid, new AuxiliaryDisplayStruct( display, blockCenter ) ); + + if ( displayData.getMaterial() == Material.TNT ) { + isTnt.add( rigid ); + } + + player.sendMessage( "Created display" ); + } ) ) + .add( new SubCommand( "constraint" ) + .defaultTo( ( sender, args, params ) -> { + Player player = ( Player ) sender; + Location loc = player.getLocation(); + loc.setPitch( 0 ); + loc.setDirection( new Vector( 0, 0, 0 ) ); + + BlockData displayData = Material.IRON_TRAPDOOR.createBlockData(); + CraftBlockData data = ( CraftBlockData ) displayData; + + final Vector scale = new Vector( 5, 2, 5 ); + + BlockPosition pos = new BlockPosition( loc.getBlockX(), loc.getBlockY(), loc.getBlockZ() ); + BoundingBox[] boxes = convertFrom( data.getState().f( ( ( CraftWorld ) loc.getWorld() ).getHandle(), pos ) ); + for ( BoundingBox box : boxes ) { + Vector min = box.getMin().multiply( scale ); + Vector max = box.getMax().multiply( scale ); + box.resize( min.getX(), min.getY(), min.getZ(), max.getX(), max.getY(), max.getZ() ); + } + Vector blockCenter = calculateCenter( boxes ); + + CollisionShape box; + if ( boxes.length == 0 ) { + player.sendMessage( "No bounding box detected! Wrong method?" ); + return; + } + + if ( boxes.length == 1 ) { + Vector min = boxes[ 0 ].getMin(); + Vector center = boxes[ 0 ].getCenter().subtract( min ); + box = new BoxCollisionShape( ( float ) center.getX(), ( float ) center.getY(), ( float ) center.getZ() ); + } else { + CompoundCollisionShape compound = new CompoundCollisionShape(); + for ( BoundingBox aabb : boxes ) { + Vector min = aabb.getMin(); + Vector center = aabb.getCenter(); + Vector half = center.clone().subtract( min ); + center.subtract( blockCenter ); + CollisionShape subShape = new BoxCollisionShape( ( float ) half.getX(), ( float ) half.getY(), ( float ) half.getZ() ); + compound.addChildShape( subShape, ( float ) center.getX(), ( float ) center.getY(), ( float ) center.getZ() ); + } + box = compound; + } + + PhysicsRigidBody rigid = new PhysicsRigidBody( box, 5f ); + + // Since the center of the rigid body and the box shape are different, we need to offset the location + rigid.setPhysicsLocation( new Vector3f( ( float ) ( loc.getX() + blockCenter.getX() ), ( float ) ( loc.getY() + blockCenter.getY() ), ( float ) ( loc.getZ() + blockCenter.getZ() ) ) ); + + // Convert the display direction vector to a quaternion + rigid.setPhysicsRotation( new Quaternion( 0, 0, 0, 1 ) ); + + space.addCollisionObject( rigid ); + + New6Dof constraint = new New6Dof( rigid, new Vector3f( 0, 0, 0 ), rigid.getPhysicsLocation(), Matrix3f.IDENTITY, Matrix3f.IDENTITY, RotationOrder.XYZ ); + + constraint.set( MotorParam.UpperLimit, 3, ( float ) Math.PI / 6 ); + constraint.set( MotorParam.LowerLimit, 3, ( float ) - Math.PI / 6 ); + constraint.set( MotorParam.UpperLimit, 4, 0 ); + constraint.set( MotorParam.LowerLimit, 4, 0 ); + constraint.set( MotorParam.UpperLimit, 5, 0 ); + constraint.set( MotorParam.LowerLimit, 5, 0 ); + + space.addJoint( constraint ); + + player.sendMessage( "Location: " + rigid.getPhysicsLocation() ); + + // Create the display + BlockDisplay display = loc.getWorld().spawn( loc, BlockDisplay.class, d -> { + d.setBlock( displayData ); + } ); + + linkedDisplays.put( rigid, new AuxiliaryDisplayStruct( display, blockCenter ).setScale( scale ) ); + + player.sendMessage( "Created constraint" ); + }) ) + .add( new SubCommand( "fixed" ) + .defaultTo( ( sender, args, params ) -> { + Player player = ( Player ) sender; + Block block = player.getTargetBlockExact( 20 ); + convert( block ); + + player.sendMessage( "Fixed" ); + } ) ) + .add( new SubCommand( "chunk" ) + .defaultTo( ( sender, args, params ) -> { + Player player = ( Player ) sender; + + player.sendMessage( "Registering chunk..." ); + player.sendMessage( "Registered " + scanAndGenerate( player.getLocation().getChunk() ) ); + } ) ) + .add( new SubCommand( "toggle" ) + .defaultTo( ( sender, args, params ) -> { + paused = !paused; + } ) ) + .add( new SubCommand( "teleport" ) + .defaultTo( ( sender, args, params ) -> { + teleportOnDeactivate = !teleportOnDeactivate; + } ) ) + .add( new SubCommand( "impulse" ) + .add( new SubCommand( new InputValidatorInt() ) + .defaultTo( ( sender, args, params ) -> { + Player player = ( Player ) sender; + Location loc = player.getLocation(); + + impulse( new Vector3f( ( float ) loc.getX(), ( float ) loc.getY(), ( float ) loc.getZ() ), params.getLast( int.class ) ); + } ) ) + .defaultTo( ( sender, args, params ) -> { + Player player = ( Player ) sender; + Location loc = player.getLocation(); + + impulse( new Vector3f( ( float ) loc.getX(), ( float ) loc.getY(), ( float ) loc.getZ() ), 15f ); + } ) ) + .defaultTo( new CommandExecutableMessage( "An argument must be provided" ) ) + .whenUnknown( new CommandExecutableMessage( "Unknown argument" ) ) + .applyTo( getCommand( "minie" ) ); + } + + private void impulse( Vector3f location, float power ) { + PhysicsRigidBody rigid = new PhysicsRigidBody( new SphereCollisionShape( 20 ), 1f ); + rigid.setPhysicsLocation( location ); + space.contactTest( rigid, new PhysicsCollisionListener() { + @Override + public void collision( PhysicsCollisionEvent event ) { + if ( event.getObjectB() instanceof PhysicsRigidBody ) { + PhysicsRigidBody objB = ( PhysicsRigidBody ) event.getObjectB(); + + Vector3f rLoc = objB.getPhysicsLocation(); + Vector3f to = rLoc.subtract( ( float ) location.getX(), ( float ) location.getY(), ( float ) location.getZ() ); + + float distSq = to.lengthSquared(); + + if ( distSq != 0 ) { + if ( power / distSq > 1 ) { + objB.applyCentralImpulse( to.normalize().mult( power / distSq ) ); + } + } + } + + } + } ); + } + + private int saveAllChunks( World world ) { + int total = 0; + for ( Chunk chunk : world.getLoadedChunks() ) { + total += saveChunk( chunk ); + } + return total; + } + + private int saveChunk( Chunk chunk ) { + File saveFile = new File( getDataFolder() + "/chunks", chunk.getWorld().getName() + "," + chunk.getX() + "," + chunk.getZ() ); + saveFile.getParentFile().mkdirs(); + saveFile.delete(); + + int count = 0; + try ( FileWriter writer = new FileWriter( saveFile ) ) { + World world = chunk.getWorld(); + for ( int y = world.getMinHeight(); y < world.getMaxHeight(); y++ ) { + for ( int z = 0; z < 16; z++ ) { + for ( int x = 0; x < 16; x++ ) { + Block block = chunk.getBlock( x, y, z ); + if ( !( block.isEmpty() && block.isLiquid() ) ) { + BlockState state = block.getState(); + BlockData displayData = state.getBlockData(); + Location loc = block.getLocation(); + CraftBlockData data = ( CraftBlockData ) displayData; + + BlockPosition pos = new BlockPosition( loc.getBlockX(), loc.getBlockY(), loc.getBlockZ() ); + BoundingBox[] boxes = convertFrom( data.getState().f( ( ( CraftWorld ) world ).getHandle(), pos ) ); + + if ( boxes.length > 0 ) { + // Save this block + for ( BoundingBox box : boxes ) { + writer.write( ( box.getMinX() + x ) + "," ); + writer.write( ( box.getMinY() + y ) + "," ); + writer.write( ( box.getMinZ() + z ) + "," ); + writer.write( ( box.getMaxX() + x ) + "," ); + writer.write( ( box.getMaxY() + y ) + "," ); + writer.write( ( box.getMaxZ() + z ) + "\n" ); + count++; + } + } + } + } + } + } + } catch (IOException e) { + e.printStackTrace(); + } + return count; + } + + private int scanAndGenerate( World world ) { + int total = 0; + for ( Chunk chunk : world.getLoadedChunks() ) { + total += scanAndGenerate( chunk ); + } + return total; + } + + private int scanAndGenerate( Chunk chunk ) { + World world = chunk.getWorld(); + CompoundCollisionShape compound = new CompoundCollisionShape(); + int worldHeight = world.getMaxHeight() - world.getMinHeight(); + boolean[][][] isSolid = new boolean[ worldHeight ][ 16 ][ 16 ]; + for ( int y = 0; y < worldHeight; y++ ) { + for ( int z = 0; z < 16; z++ ) { + for ( int x = 0; x < 16; x++ ) { + Block block = chunk.getBlock( x, y + world.getMinHeight(), z ); + if ( !( block.isEmpty() && block.isLiquid() ) ) { + BlockState state = block.getState(); + BlockData displayData = state.getBlockData(); + Location loc = block.getLocation(); + CraftBlockData data = ( CraftBlockData ) displayData; + + BlockPosition pos = new BlockPosition( loc.getBlockX(), loc.getBlockY(), loc.getBlockZ() ); + BoundingBox[] boxes = convertFrom( data.getState().f( ( ( CraftWorld ) world ).getHandle(), pos ) ); + + if ( boxes.length == 1 && boxes[ 0 ].getVolume() == 1 ) { + isSolid[ y ][ z ][ x ] = true; + } + } + } + } + } + + for ( int y = 0; y < worldHeight; y++ ) { + for ( int z = 0; z < 16; z++ ) { + for ( int x = 0; x < 16; x++ ) { + if ( x > 0 && x < 15 && z > 0 && z < 15 && y > 0 && y < worldHeight - 1 ) { + // Figure out if this block is surrounded by solid blocks to avoid having to add this block + if ( isSolid[ y - 1 ][ z ][ x ] + && isSolid[ y + 1 ][ z ][ x ] + && isSolid[ y ][ z - 1 ][ x ] + && isSolid[ y ][ z + 1 ][ x ] + && isSolid[ y ][ z ][ x - 1 ] + && isSolid[ y ][ z ][ x + 1 ] ) { + continue; + } + + } + + Block block = chunk.getBlock( x, y + world.getMinHeight(), z ); + + if ( !( block.isEmpty() && block.isLiquid() ) ) { + BlockState state = block.getState(); + BlockData displayData = state.getBlockData(); + Location loc = block.getLocation(); + CraftBlockData data = ( CraftBlockData ) displayData; + + BlockPosition pos = new BlockPosition( loc.getBlockX(), loc.getBlockY(), loc.getBlockZ() ); + BoundingBox[] boxes = convertFrom( data.getState().f( ( ( CraftWorld ) world ).getHandle(), pos ) ); + + if ( boxes.length == 0 ) { + continue; + } + + if ( boxes.length == 1 ) { + Vector min = boxes[ 0 ].getMin(); + Vector center = boxes[ 0 ].getCenter().subtract( min ); + Vector3f halfExtents = new Vector3f( ( float ) center.getX(), ( float ) center.getY(), ( float ) center.getZ() ); + + CollisionShape shape = collisionShapes.get( halfExtents ); + if ( shape == null ) { + shape = new BoxCollisionShape( halfExtents ); + collisionShapes.put( halfExtents, shape ); + } + Vector3f shapeCenter = new Vector3f( ( float ) boxes[ 0 ].getCenterX() + x, ( float ) boxes[ 0 ].getCenterY() + y, ( float ) boxes[ 0 ].getCenterZ() + z ); + compound.addChildShape( shape, shapeCenter ); + } else { + for ( BoundingBox aabb : boxes ) { + Vector min = aabb.getMin(); + Vector center = aabb.getCenter(); + Vector half = center.clone().subtract( min ); + Vector3f halfExtents = new Vector3f( ( float ) half.getX(), ( float ) half.getY(), ( float ) half.getZ() ); + + CollisionShape shape = collisionShapes.get( halfExtents ); + if ( shape == null ) { + shape = new BoxCollisionShape( halfExtents ); + collisionShapes.put( halfExtents, shape ); + } + compound.addChildShape( shape, ( float ) center.getX() + x, ( float ) center.getY() + y, ( float ) center.getZ() + z ); + } + } + } + } + } + } + + getLogger().info( "Chunk has " + compound.countChildren() ); + + // Create a rigid body + PhysicsRigidBody rigid = new PhysicsRigidBody( compound, PhysicsBody.massForStatic ); + rigid.setPhysicsLocation( new Vector3f( chunk.getX() << 4, world.getMinHeight(), chunk.getZ() << 4 ) ); + rigid.setPhysicsRotation( new Quaternion( 0, 0, 0, 1 ) ); + + space.addCollisionObject( rigid ); + + PhysicsRigidBody old = chunks.put( new ChunkLocation( chunk ), rigid ); + if ( old != null ) { + getLogger().warning( "Old rigid body found!" ); + space.remove( old ); + } + + return compound.countChildren(); + } + + private boolean convert( Block block ) { + BlockState state = block.getState(); + BlockData displayData = state.getBlockData(); + Location loc = block.getLocation(); + + CraftBlockData data = ( CraftBlockData ) displayData; + + BlockPosition pos = new BlockPosition( loc.getBlockX(), loc.getBlockY(), loc.getBlockZ() ); + BoundingBox[] boxes = convertFrom( data.getState().f( ( ( CraftWorld ) loc.getWorld() ).getHandle(), pos ) ); + Vector blockCenter = calculateCenter( boxes ); + + CollisionShape box; + if ( boxes.length == 0 ) { + return false; + } + + if ( boxes.length == 1 ) { + Vector min = boxes[ 0 ].getMin(); + Vector center = boxes[ 0 ].getCenter().subtract( min ); + box = new BoxCollisionShape( ( float ) center.getX(), ( float ) center.getY(), ( float ) center.getZ() ); + } else { + CompoundCollisionShape compound = new CompoundCollisionShape(); + for ( BoundingBox aabb : boxes ) { + Vector min = aabb.getMin(); + Vector center = aabb.getCenter(); + Vector half = center.clone().subtract( min ); + center.subtract( blockCenter ); + CollisionShape subShape = new BoxCollisionShape( ( float ) half.getX(), ( float ) half.getY(), ( float ) half.getZ() ); + compound.addChildShape( subShape, ( float ) center.getX(), ( float ) center.getY(), ( float ) center.getZ() ); + } + box = compound; + } + + PhysicsRigidBody rigid = new PhysicsRigidBody( box, 1f ); + rigid.setProtectGravity( true ); + rigid.setGravity( new Vector3f( 0, 0, 0 ) ); + + // Since the center of the rigid body and the box shape are different, we need to offset the location + rigid.setPhysicsLocation( new Vector3f( ( float ) ( loc.getX() + blockCenter.getX() ), ( float ) ( loc.getY() + blockCenter.getY() ), ( float ) ( loc.getZ() + blockCenter.getZ() ) ) ); + + // Convert the display direction vector to a quaternion + rigid.setPhysicsRotation( new Quaternion( 0, 0, 0, 1 ) ); + + space.addCollisionObject( rigid ); + + New6Dof constraint = new New6Dof( rigid, new Vector3f( 0, 0, 0 ), rigid.getPhysicsLocation(), Matrix3f.IDENTITY, Matrix3f.IDENTITY, RotationOrder.XYZ ); + + constraint.setBreakingImpulseThreshold( 5 ); + constraint.set( MotorParam.UpperLimit, 3, 0 ); + constraint.set( MotorParam.LowerLimit, 3, 0 ); + constraint.set( MotorParam.UpperLimit, 4, 0 ); + constraint.set( MotorParam.LowerLimit, 4, 0 ); + constraint.set( MotorParam.UpperLimit, 5, 0 ); + constraint.set( MotorParam.LowerLimit, 5, 0 ); + + space.addJoint( constraint ); + + joints.put( constraint, new JointStruct( state, rigid, blockCenter ) ); + + return true; + } + + private final boolean loadNativeLibraries() { + if ( SystemUtils.IS_OS_MAC ) { + System.load( new File( getDataFolder() + "/lib", "libbulletjme.dylib" ).getAbsolutePath() ); + } else if ( SystemUtils.IS_OS_LINUX ) { + System.load( new File( getDataFolder() + "/lib", "libbulletjme.so" ).getAbsolutePath() ); + } else if ( SystemUtils.IS_OS_WINDOWS ) { + System.load( new File( getDataFolder() + "/lib", "bulletjme.dll" ).getAbsolutePath() ); + } else { + return false; + } + return true; + } + + private static Vector calculateCenter( BoundingBox[] boxes ) { + Vector center = new Vector( 0, 0, 0 ); + // TODO Throw error, probably + if ( boxes.length == 0 ) { + return center; + } + + double totalVolume = 0; + for ( BoundingBox box : boxes ) { + Vector mid = box.getCenter(); + center.add( mid.multiply( box.getVolume() ) ); + totalVolume += box.getVolume(); + } + return center.multiply( 1 / totalVolume ); + } + + private static BoundingBox[] convertFrom( VoxelShape shape ) { + return shape.e().stream() + .map( aabb -> { return new BoundingBox( aabb.a, aabb.b, aabb.c, aabb.d, aabb.e, aabb.f ); } ) + .toArray( BoundingBox[]::new ); + } + + private class JointStruct { + BlockState state; + PhysicsRigidBody object; + Vector offset; + + JointStruct( BlockState state, PhysicsRigidBody body, Vector offset ) { + this.state = state; + this.object = body; + this.offset = offset.clone(); + } + } + + private class AuxiliaryDisplayStruct { + AuxiliaryDisplayStruct( Display display, Vector offset ) { + this.display = display; + this.offset = offset.clone(); + this.scale = new Vector( 1, 1, 1 ); + } + + AuxiliaryDisplayStruct setScale( Vector scale ) { + this.scale = scale.clone(); + + return this; + } + + Display display; + Vector offset; + Vector scale; + } +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/MinieTest.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/MinieTest.java new file mode 100644 index 0000000..ff37cc4 --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/MinieTest.java @@ -0,0 +1,37 @@ +package com.aaaaahhhhhhh.bananapuncher714.minietest; + +import com.jme3.bullet.PhysicsSpace; +import com.jme3.bullet.PhysicsSpace.BroadphaseType; +import com.jme3.bullet.collision.shapes.BoxCollisionShape; +import com.jme3.bullet.collision.shapes.PlaneCollisionShape; +import com.jme3.bullet.objects.PhysicsBody; +import com.jme3.bullet.objects.PhysicsRigidBody; +import com.jme3.math.Plane; +import com.jme3.math.Vector3f; + +public class MinieTest { + + public static void main( String[] args ) { + System.loadLibrary( "bulletjme" ); + + System.out.println( "Testing a simple PhysicsSpace" ); + PhysicsSpace space = new PhysicsSpace( BroadphaseType.DBVT ); + + PlaneCollisionShape plane = new PlaneCollisionShape( new Plane( new Vector3f( 0, 1, 0 ), 0 ) ); + PhysicsRigidBody planeBody = new PhysicsRigidBody( plane, PhysicsBody.massForStatic ); + planeBody.setPhysicsLocation( new Vector3f( 0, 0, 0 ) ); + + BoxCollisionShape box = new BoxCollisionShape( 0.5f ); + PhysicsRigidBody rigid = new PhysicsRigidBody( box, 1f ); + rigid.setPhysicsLocation( new Vector3f( 0f, 100f, 0f ) ); + + space.addCollisionObject( rigid ); + space.addCollisionObject( planeBody ); + + for ( float i = 0; i < 9.6; i += 1 / 256.0 ) { + space.update( i ); + Vector3f loc = rigid.getPhysicsLocation(); + System.out.println( i + "\t: " + loc.getX() + ", " + loc.getY() + ", " + loc.getZ() ); + } + } +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/CommandBase.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/CommandBase.java new file mode 100644 index 0000000..5f160a6 --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/CommandBase.java @@ -0,0 +1,58 @@ +package com.aaaaahhhhhhh.bananapuncher714.minietest.command; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +import org.bukkit.command.PluginCommand; +import org.bukkit.permissions.Permission; + +import com.aaaaahhhhhhh.bananapuncher714.minietest.util.BukkitUtil; + +public class CommandBase { + protected PluginCommand command; + + public CommandBase( String command ) { + this.command = BukkitUtil.constructCommand( command ); + } + + public CommandBase setPermission( String permission ) { + command.setPermission( permission ); + return this; + } + + public CommandBase setDescription( String description ) { + command.setDescription( description ); + return this; + } + + public CommandBase addAliases( String... aliases ) { + Set< String > aliasSet = new HashSet< String >( command.getAliases() ); + for ( String alias : aliases ) { + aliasSet.add( alias ); + } + command.setAliases( new ArrayList< String >( aliasSet ) ); + return this; + } + + public CommandBase addAliases( Collection< String > aliases ) { + Set< String > aliasSet = new HashSet< String >( command.getAliases() ); + aliasSet.addAll( aliases ); + command.setAliases( new ArrayList< String >( aliasSet ) ); + return this; + } + + public CommandBase setPermission( Permission permission ) { + return setPermission( permission.toString() ); + } + + public CommandBase setSubCommand( SubCommand subCommand ) { + subCommand.applyTo( command ); + return this; + } + + public PluginCommand build() { + return command; + } +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/CommandOption.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/CommandOption.java new file mode 100644 index 0000000..bb86f5a --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/CommandOption.java @@ -0,0 +1,25 @@ +package com.aaaaahhhhhhh.bananapuncher714.minietest.command; + +import org.bukkit.command.CommandSender; + +import com.aaaaahhhhhhh.bananapuncher714.minietest.command.executor.CommandExecutable; + +public class CommandOption { + protected CommandExecutable exe; + protected String[] args; + protected CommandParameters parameter; + + public CommandOption( CommandExecutable exe, String[] args, CommandParameters parameter ) { + this.exe = exe; + this.args = args; + this.parameter = parameter; + } + + public int getArgumentSize() { + return args.length; + } + + public void execute( CommandSender sender ) { + exe.execute( sender, args, parameter ); + } +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/CommandParameters.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/CommandParameters.java new file mode 100644 index 0000000..a3e0e8e --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/CommandParameters.java @@ -0,0 +1,65 @@ +package com.aaaaahhhhhhh.bananapuncher714.minietest.command; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import com.google.common.primitives.Primitives; + +public class CommandParameters { + protected List< Object > parameters = new ArrayList< Object >(); + + public CommandParameters() { + } + + public CommandParameters( CommandParameters copy ) { + parameters.addAll( copy.parameters ); + } + + public void add( Object object ) { + parameters.add( object ); + } + + public int size() { + return parameters.size(); + } + + public < T > T get( Class < T > clazz, int index ) { + clazz = Primitives.wrap( clazz ); + if ( index < 0 || index >= parameters.size() ) { + return null; + } + Object obj = parameters.get( index ); + if ( obj != null && clazz.isInstance( obj ) ) { + return clazz.cast( obj ); + } + return null; + } + + public < T > T getLast( Class< T > clazz ) { + clazz = Primitives.wrap( clazz ); + for ( int i = parameters.size() - 1; i >= 0; i-- ) { + Object obj = parameters.get( i ); + if ( obj != null && clazz.isInstance( obj ) ) { + return clazz.cast( obj ); + } + } + return null; + } + + public < T > T getFirst( Class< T > clazz ) { + clazz = Primitives.wrap( clazz ); + for ( int i = 0; i < parameters.size(); i++ ) { + Object obj = parameters.get( i ); + if ( obj != null && clazz.isInstance( obj ) ) { + return clazz.cast( obj ); + } + } + return null; + } + + @Override + public String toString() { + return "CommandParameters[" + parameters.stream().map( Object::toString ).collect( Collectors.joining( ", " ) ) + "]"; + } +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/CommandResult.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/CommandResult.java new file mode 100644 index 0000000..20e36bc --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/CommandResult.java @@ -0,0 +1,39 @@ +package com.aaaaahhhhhhh.bananapuncher714.minietest.command; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.bukkit.command.CommandSender; + +public class CommandResult { + protected List< CommandOption > options = new ArrayList< CommandOption >(); + + public void add( CommandOption option ) { + options.add( option ); + } + + public void addAll( Collection< CommandOption > options ) { + this.options.addAll( options ); + } + + public void add( CommandResult result ) { + this.options.addAll( result.getOptions() ); + } + + public List< CommandOption > getOptions() { + return options; + } + + public void execute( CommandSender sender ) { + CommandOption lowest = null; + for ( CommandOption option : options ) { + if ( lowest == null || option.getArgumentSize() < lowest.getArgumentSize() ) { + lowest = option; + } + } + if ( lowest != null ) { + lowest.execute( sender ); + } + } +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/SplitCommand.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/SplitCommand.java new file mode 100644 index 0000000..6d11338 --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/SplitCommand.java @@ -0,0 +1,27 @@ +package com.aaaaahhhhhhh.bananapuncher714.minietest.command; + +public class SplitCommand { + protected String[] input; + protected String[] arguments; + + public SplitCommand( String[] input, String[] arguments ) { + this.input = input; + this.arguments = arguments; + } + + public String[] getInput() { + return input; + } + + public void setInput( String[] input ) { + this.input = input; + } + + public String[] getArguments() { + return arguments; + } + + public void setArguments( String[] arguments ) { + this.arguments = arguments; + } +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/SubCommand.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/SubCommand.java new file mode 100644 index 0000000..29a503b --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/SubCommand.java @@ -0,0 +1,231 @@ +package com.aaaaahhhhhhh.bananapuncher714.minietest.command; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.command.PluginCommand; +import org.bukkit.util.StringUtil; + +import com.aaaaahhhhhhh.bananapuncher714.minietest.command.executor.CommandExecutable; +import com.aaaaahhhhhhh.bananapuncher714.minietest.command.validator.InputValidator; +import com.aaaaahhhhhhh.bananapuncher714.minietest.command.validator.InputValidatorString; +import com.aaaaahhhhhhh.bananapuncher714.minietest.command.validator.sender.SenderValidator; + +/** + * A build and run command framework for automatic tab completions and easy branching. + * + * @author BananaPuncher714 + */ +public class SubCommand { + protected List< SubCommand > subCommands = new ArrayList< SubCommand >(); + + protected InputValidator< ? > validator; + protected Set< SenderValidator > senderValidators = new HashSet< SenderValidator >(); + protected CommandExecutable whenUnknown; + protected CommandExecutable whenNone; + + /** + * Accept anything as a valid input + */ + public SubCommand() { + } + + // Helped constructor, really belongs in a factory. + /** + * Create a SubCommand with the string as the input validator. + * + * @param command + * The subcommand value. + */ + public SubCommand( String command ) { + this( new InputValidatorString( command ) ); + } + + /** + * Create a SubCommand with the input validator provided. + * + * @param validator + * Can be null. + */ + public SubCommand( InputValidator< ? > validator ) { + this.validator = validator; + } + + public SubCommand add( SubCommand builder ) { + subCommands.add( builder ); + return this; + } + + public SubCommand addSenderValidator( SenderValidator validator ) { + senderValidators.add( validator ); + return this; + } + + /** + * Ran when the arguments provided don't match any SubCommands registered. + * + * @param executable + * An executable where the arguments will start with the unknown subcommand. + * @return + * Builder pattern return. + */ + public SubCommand whenUnknown( CommandExecutable executable ) { + this.whenUnknown = executable; + return this; + } + + // Naming by jetp250 + /** + * Ran when there are no arguments provided, or if the executable for when unknown is not set. + * + * @param executable + * If null, nothing will happen. + * @return + * Builder pattern return. + */ + public SubCommand defaultTo( CommandExecutable executable ) { + this.whenNone = executable; + return this; + } + + public boolean matches( CommandSender sender ) { + for ( SenderValidator validator : senderValidators ) { + if ( !validator.isValid( sender ) ) { + return false; + } + } + return true; + } + + public boolean matches( CommandSender sender, String input[], String[] args ) { + return ( validator == null ? true : validator.isValid( sender, input, args ) ) && matches( sender ); + } + + public Collection< String > getTabCompletes( CommandSender sender, String[] input ) { + return validator == null ? null : validator.getTabCompletes( sender, input ); + } + + public List< SubCommand > getSubCommands() { + return subCommands; + } + + public InputValidator< ? > getInputValidator() { + return validator; + } + + public Set< SenderValidator > getSenderValidators() { + return senderValidators; + } + + public CommandResult submit( CommandSender sender, String[] command, String[] args, CommandParameters parameter ) { + CommandResult result = new CommandResult(); + parameter = new CommandParameters( parameter ); + if ( validator != null ) { + parameter.add( validator.get( sender, command ) ); + } else { + parameter.add( null ); + } + if ( args.length > 0 ) { + boolean found = false; + for ( SubCommand subCommand : subCommands ) { + SplitCommand split = split( args, subCommand.getInputValidator().getArgumentCount() ); + String[] input = split.getInput(); + String[] newArgs = split.getArguments(); + if ( subCommand.matches( sender, input, newArgs ) ) { + found = true; + + result.add( subCommand.submit( sender, input, newArgs, parameter ) ); + } + } + if ( !found ) { + if ( whenUnknown != null ) { + result.add( new CommandOption( whenUnknown, args, parameter ) ); + } else if ( whenNone != null ) { + result.add( new CommandOption( whenNone, args, parameter ) ); + } + } + } else { + if ( whenNone != null ) { + result.add( new CommandOption( whenNone, args, parameter ) ); + } + } + return result; + } + + public Collection< String > getTabCompletions( CommandSender sender, String[] args ) { + Set< String > tabs = new HashSet< String >(); + if ( args.length > 0 ) { + for ( SubCommand subCommand : subCommands ) { + if ( subCommand.matches( sender ) ) { + SplitCommand split = split( args, subCommand.getInputValidator().getArgumentCount() ); + String[] input = split.getInput(); + String[] newArgs = split.getArguments(); + // Check if the subcommand matches the argument, and if it has subcommands of its own + if ( subCommand.matches( sender, input, newArgs ) && !subCommand.getSubCommands().isEmpty() ) { + tabs.addAll( subCommand.getTabCompletions( sender, newArgs ) ); + } else if ( newArgs.length == 0 ) { + // If not, and this is the last argument, add all possible tab completes + // This allows for "recommendations" + Collection< String > completions = subCommand.getTabCompletes( sender, input ); + if ( completions != null ) { + tabs.addAll( completions ); + } + } + } + } + } + + return tabs; + } + + public SubCommand applyTo( PluginCommand command ) { + command.setExecutor( this::onCommand ); + command.setTabCompleter( this::onTabComplete ); + return this; + } + + private boolean onCommand( CommandSender sender, Command command, String arg2, String[] args ) { + CommandParameters parameters = new CommandParameters(); + if ( matches( sender ) ) { + String[] commandArr = new String[ args.length + 1 ]; + + commandArr[ 0 ] = command.getName(); + for ( int i = 1; i < commandArr.length; i++ ) { + commandArr[ i ] = args[ i - 1 ]; + } + + SplitCommand split = split( commandArr, validator.getArgumentCount() ); + submit( sender, split.getInput(), split.getArguments(), parameters ).execute( sender ); + } + return false; + } + + private List< String > onTabComplete( CommandSender sender, Command command, String arg2, String[] args ) { + List< String > completions = new ArrayList< String >(); + String[] commandArr = new String[ args.length + 1 ]; + + commandArr[ 0 ] = command.getName(); + for ( int i = 1; i < commandArr.length; i++ ) { + commandArr[ i ] = args[ i - 1 ]; + } + + SplitCommand split = split( commandArr, validator.getArgumentCount() ); + StringUtil.copyPartialMatches( args[ args.length - 1 ], getTabCompletions( sender, split.getArguments() ), completions ); + Collections.sort( completions ); + return completions; + } + + protected static SplitCommand split( String[] command, int inputSize ) { + String[] input = command.length > 0 ? Arrays.copyOfRange( command, 0, Math.min( inputSize, command.length ) ) : new String[ 0 ]; + String[] args = command.length > inputSize ? Arrays.copyOfRange( command, inputSize, command.length ) : new String[ 0 ]; + + return new SplitCommand( input, args ); + } +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/executor/CommandExecutable.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/executor/CommandExecutable.java new file mode 100644 index 0000000..655983d --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/executor/CommandExecutable.java @@ -0,0 +1,9 @@ +package com.aaaaahhhhhhh.bananapuncher714.minietest.command.executor; + +import org.bukkit.command.CommandSender; + +import com.aaaaahhhhhhh.bananapuncher714.minietest.command.CommandParameters; + +public interface CommandExecutable { + void execute( CommandSender sender, String[] args, CommandParameters params ); +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/executor/CommandExecutableMessage.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/executor/CommandExecutableMessage.java new file mode 100644 index 0000000..b4ad6ec --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/executor/CommandExecutableMessage.java @@ -0,0 +1,18 @@ +package com.aaaaahhhhhhh.bananapuncher714.minietest.command.executor; + +import org.bukkit.command.CommandSender; + +import com.aaaaahhhhhhh.bananapuncher714.minietest.command.CommandParameters; + +public class CommandExecutableMessage implements CommandExecutable { + protected String message; + + public CommandExecutableMessage( String message ) { + this.message = message; + } + + @Override + public void execute( CommandSender sender, String[] args, CommandParameters params ) { + sender.sendMessage( message ); + } +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/executor/package-info.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/executor/package-info.java new file mode 100644 index 0000000..0ae66c9 --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/executor/package-info.java @@ -0,0 +1,8 @@ +/** + * + */ +/** + * @author BananaPuncher714 + * + */ +package com.aaaaahhhhhhh.bananapuncher714.minietest.command.executor; \ No newline at end of file diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/package-info.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/package-info.java new file mode 100644 index 0000000..0453a47 --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/package-info.java @@ -0,0 +1,6 @@ +/** + * Classes for building complex command trees. + * + * @author BananaPuncher714 + */ +package com.aaaaahhhhhhh.bananapuncher714.minietest.command; \ No newline at end of file diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/validator/InputValidator.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/validator/InputValidator.java new file mode 100644 index 0000000..3309d26 --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/validator/InputValidator.java @@ -0,0 +1,14 @@ +package com.aaaaahhhhhhh.bananapuncher714.minietest.command.validator; + +import java.util.Collection; + +import org.bukkit.command.CommandSender; + +public interface InputValidator< T > { + Collection< String > getTabCompletes( CommandSender sender, String[] input ); + boolean isValid( CommandSender sender, String[] input, String[] args ); + T get( CommandSender sender, String[] input ); + default int getArgumentCount() { + return 1; + } +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/validator/InputValidatorArguments.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/validator/InputValidatorArguments.java new file mode 100644 index 0000000..db03cd0 --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/validator/InputValidatorArguments.java @@ -0,0 +1,37 @@ +package com.aaaaahhhhhhh.bananapuncher714.minietest.command.validator; + +import java.util.Collection; + +import org.bukkit.command.CommandSender; + +public class InputValidatorArguments implements InputValidator< Void > { + protected int min; + protected int max; + + public InputValidatorArguments( int amount ) { + this.min = amount; + this.max = amount; + } + + public InputValidatorArguments( int min, int max ) { + this.min = min; + this.max = max; + } + + @Override + public Collection< String > getTabCompletes( CommandSender sender, String[] input ) { + return null; + } + + @Override + public boolean isValid( CommandSender sender, String input[], String[] args ) { + int len = args.length; + return len >= min && len <= max; + } + + @Override + public Void get( CommandSender sender, String input[] ) { + return null; + } + +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/validator/InputValidatorBoolean.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/validator/InputValidatorBoolean.java new file mode 100644 index 0000000..2ad0c1f --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/validator/InputValidatorBoolean.java @@ -0,0 +1,47 @@ +package com.aaaaahhhhhhh.bananapuncher714.minietest.command.validator; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +import org.bukkit.command.CommandSender; + +public class InputValidatorBoolean implements InputValidator< Boolean > { + protected Set< String > trueVals = new HashSet< String >(); + protected Set< String > falseVals = new HashSet< String >(); + + public InputValidatorBoolean( String[] trueVal, String[] falseVal ) { + for ( String str : trueVal ) { + trueVals.add( str.toLowerCase() ); + } + + for ( String str : falseVal ) { + falseVals.add( str.toLowerCase() ); + } + } + + public InputValidatorBoolean() { + trueVals.add( "true" ); + falseVals.add( "false" ); + } + + @Override + public Collection< String > getTabCompletes( CommandSender sender, String[] input ) { + Set< String > combined = new HashSet< String >( trueVals ); + combined.addAll( falseVals ); + return combined; + } + + @Override + public boolean isValid( CommandSender sender, String input[], String[] args ) { + String lowercase = input[ 0 ].toLowerCase(); + return trueVals.contains( lowercase ) || falseVals.contains( lowercase ); + } + + @Override + public Boolean get( CommandSender sender, String input[] ) { + String lowercase = input[ 0 ].toLowerCase(); + return trueVals.contains( lowercase ); + } + +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/validator/InputValidatorChain.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/validator/InputValidatorChain.java new file mode 100644 index 0000000..b75d659 --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/validator/InputValidatorChain.java @@ -0,0 +1,44 @@ +package com.aaaaahhhhhhh.bananapuncher714.minietest.command.validator; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.bukkit.command.CommandSender; + +public class InputValidatorChain< T > implements InputValidator< T > { + protected InputValidator< T > primaryValidator; + protected List< InputValidator< ? > > validators = new ArrayList< InputValidator< ? > >(); + + public InputValidatorChain( InputValidator< T > primaryValidator ) { + this.primaryValidator = primaryValidator; + } + + public InputValidatorChain< T > addValidator( InputValidator< ? > validator ) { + validators.add( validator ); + return this; + } + + @Override + public Collection< String > getTabCompletes( CommandSender sender, String[] input ) { + return primaryValidator.getTabCompletes( sender, input ); + } + + @Override + public boolean isValid( CommandSender sender, String input[], String[] args ) { + if ( !primaryValidator.isValid( sender, input, args ) ) { + return false; + } + for ( InputValidator< ? > validator : validators ) { + if ( !validator.isValid( sender, input, args ) ) { + return false; + } + } + return true; + } + + @Override + public T get( CommandSender sender, String input[] ) { + return primaryValidator.get( sender, input ); + } +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/validator/InputValidatorInt.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/validator/InputValidatorInt.java new file mode 100644 index 0000000..a605554 --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/validator/InputValidatorInt.java @@ -0,0 +1,38 @@ +package com.aaaaahhhhhhh.bananapuncher714.minietest.command.validator; + +import java.util.Collection; + +import org.bukkit.command.CommandSender; + +public class InputValidatorInt implements InputValidator< Integer > { + protected int min = Integer.MIN_VALUE; + protected int max = Integer.MAX_VALUE; + + public InputValidatorInt() { + } + + public InputValidatorInt( int min, int max ) { + this.min = min; + this.max = max; + } + + @Override + public Collection< String > getTabCompletes( CommandSender sender, String[] input ) { + return null; + } + + @Override + public boolean isValid( CommandSender sender, String input[], String[] args ) { + try { + int i = Integer.valueOf( input[ 0 ] ); + return i >= min && i <= max; + } catch ( Exception exception ) { + return false; + } + } + + @Override + public Integer get( CommandSender sender, String input[] ) { + return Integer.parseInt( input[ 0 ] ); + } +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/validator/InputValidatorPattern.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/validator/InputValidatorPattern.java new file mode 100644 index 0000000..80c391a --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/validator/InputValidatorPattern.java @@ -0,0 +1,28 @@ +package com.aaaaahhhhhhh.bananapuncher714.minietest.command.validator; + +import java.util.Collection; + +import org.bukkit.command.CommandSender; + +public class InputValidatorPattern implements InputValidator< String > { + protected String pattern; + + public InputValidatorPattern( String pattern ) { + this.pattern = pattern; + } + + @Override + public Collection< String > getTabCompletes( CommandSender sender, String[] input ) { + return null; + } + + @Override + public boolean isValid( CommandSender sender, String input[], String[] args ) { + return input[ 0 ].matches( pattern ); + } + + @Override + public String get( CommandSender sender, String input[] ) { + return input[ 0 ]; + } +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/validator/InputValidatorPlayer.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/validator/InputValidatorPlayer.java new file mode 100644 index 0000000..2af5ed4 --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/validator/InputValidatorPlayer.java @@ -0,0 +1,31 @@ +package com.aaaaahhhhhhh.bananapuncher714.minietest.command.validator; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +import org.bukkit.Bukkit; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +public class InputValidatorPlayer implements InputValidator< Player > { + @Override + public Collection< String > getTabCompletes( CommandSender sender, String[] input ) { + Set< String > playerNames = new HashSet< String >(); + for ( Player player : Bukkit.getOnlinePlayers() ) { + playerNames.add( player.getName() ); + } + return playerNames; + } + + @Override + public boolean isValid( CommandSender sender, String input[], String[] args ) { + return Bukkit.getPlayerExact( input[ 0 ] ) != null; + } + + @Override + public Player get( CommandSender sender, String input[] ) { + return Bukkit.getPlayerExact( input[ 0 ] ); + } + +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/validator/InputValidatorString.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/validator/InputValidatorString.java new file mode 100644 index 0000000..d5d7f92 --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/validator/InputValidatorString.java @@ -0,0 +1,38 @@ +package com.aaaaahhhhhhh.bananapuncher714.minietest.command.validator; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +import org.apache.commons.lang.Validate; +import org.bukkit.command.CommandSender; + +public class InputValidatorString implements InputValidator< String > { + protected Set< String > values = new HashSet< String >(); + + public InputValidatorString( String value ) { + values.add( value.toLowerCase() ); + } + + public InputValidatorString( String... values ) { + Validate.isTrue( values.length > 0, "Must provide at least 1 argument!" ); + for ( String str : values ) { + this.values.add( str.toLowerCase() ); + } + } + + @Override + public Collection< String > getTabCompletes( CommandSender sender, String[] input ) { + return values; + } + + @Override + public boolean isValid( CommandSender sender, String input[], String[] args ) { + return values.contains( input[ 0 ].toLowerCase() ); + } + + @Override + public String get( CommandSender sender, String input[] ) { + return input[ 0 ]; + } +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/validator/package-info.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/validator/package-info.java new file mode 100644 index 0000000..5866d6b --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/validator/package-info.java @@ -0,0 +1,8 @@ +/** + * + */ +/** + * @author BananaPuncher714 + * + */ +package com.aaaaahhhhhhh.bananapuncher714.minietest.command.validator; \ No newline at end of file diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/validator/sender/SenderValidator.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/validator/sender/SenderValidator.java new file mode 100644 index 0000000..0b5c0df --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/validator/sender/SenderValidator.java @@ -0,0 +1,7 @@ +package com.aaaaahhhhhhh.bananapuncher714.minietest.command.validator.sender; + +import org.bukkit.command.CommandSender; + +public interface SenderValidator { + boolean isValid( CommandSender sender ); +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/validator/sender/SenderValidatorNotPlayer.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/validator/sender/SenderValidatorNotPlayer.java new file mode 100644 index 0000000..72e4743 --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/validator/sender/SenderValidatorNotPlayer.java @@ -0,0 +1,11 @@ +package com.aaaaahhhhhhh.bananapuncher714.minietest.command.validator.sender; + +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +public class SenderValidatorNotPlayer implements SenderValidator { + @Override + public boolean isValid( CommandSender sender ) { + return !( sender instanceof Player ); + } +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/validator/sender/SenderValidatorPermission.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/validator/sender/SenderValidatorPermission.java new file mode 100644 index 0000000..49f53fb --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/validator/sender/SenderValidatorPermission.java @@ -0,0 +1,27 @@ +package com.aaaaahhhhhhh.bananapuncher714.minietest.command.validator.sender; + +import java.util.HashSet; +import java.util.Set; + +import org.bukkit.command.CommandSender; + +public class SenderValidatorPermission implements SenderValidator { + protected Set< String > permissions = new HashSet< String >(); + + public SenderValidatorPermission( String... permissions ) { + for ( String permission : permissions ) { + this.permissions.add( permission ); + } + } + + @Override + public boolean isValid( CommandSender sender ) { + for ( String permission : permissions ) { + if ( sender.hasPermission( permission ) ) { + return true; + } + } + return false; + } + +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/validator/sender/SenderValidatorPlayer.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/validator/sender/SenderValidatorPlayer.java new file mode 100644 index 0000000..a911d4e --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/validator/sender/SenderValidatorPlayer.java @@ -0,0 +1,11 @@ +package com.aaaaahhhhhhh.bananapuncher714.minietest.command.validator.sender; + +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +public class SenderValidatorPlayer implements SenderValidator { + @Override + public boolean isValid( CommandSender sender ) { + return sender instanceof Player; + } +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/validator/sender/package-info.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/validator/sender/package-info.java new file mode 100644 index 0000000..0381584 --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/command/validator/sender/package-info.java @@ -0,0 +1,8 @@ +/** + * + */ +/** + * @author BananaPuncher714 + * + */ +package com.aaaaahhhhhhh.bananapuncher714.minietest.command.validator.sender; \ No newline at end of file diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/objects/ChunkLocation.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/objects/ChunkLocation.java new file mode 100644 index 0000000..fd74c61 --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/objects/ChunkLocation.java @@ -0,0 +1,286 @@ +package com.aaaaahhhhhhh.bananapuncher714.minietest.objects; + +import org.apache.commons.lang.Validate; +import org.bukkit.Bukkit; +import org.bukkit.Chunk; +import org.bukkit.ChunkSnapshot; +import org.bukkit.Location; +import org.bukkit.World; + +/** + * Represents a Bukkit chunk's location. + * + * @author BananaPuncher714 + */ +public class ChunkLocation { + private int x, z; + private String worldName; + private World world; + + /** + * Construct a ChunkLocation from a Location. + * + * @param location + * Cannot be null. + */ + public ChunkLocation( Location location ) { + Validate.notNull( location ); + world = location.getWorld(); + worldName = world.getName(); + x = location.getBlockX() >> 4; + z = location.getBlockZ() >> 4; + } + + /** + * Copy a ChunkLocation. + * + * @param location + * Cannot be null. + */ + public ChunkLocation( ChunkLocation location ) { + Validate.notNull( location ); + x = location.x; + z = location.z; + worldName = location.worldName; + world = location.world; + } + + /** + * Create a ChunkLocation from chunk coordinates. + * + * @param world + * Cannot be null. + * @param x + * Chunk coordinate x value. + * @param z + * Chunk coordinate z value. + */ + public ChunkLocation( World world, int x, int z ) { + Validate.notNull( world ); + this.x = x; + this.z = z; + this.worldName = world.getName(); + this.world = world; + + } + + public ChunkLocation( String world, int x, int z ) { + this.x = x; + this.z = z; + this.worldName = world; + } + + /** + * Create a ChunkLocation from a Bukkit chunk. + * + * @param chunk + * Cannot be null. + */ + public ChunkLocation( Chunk chunk ) { + Validate.notNull( chunk ); + world = chunk.getWorld(); + worldName = world.getName(); + x = chunk.getX(); + z = chunk.getZ(); + } + + /** + * Create a ChunkLocation from a ChunkSnapshot. + * + * @param snapshot + * Cannot be null. The world will not be cached, only the world name. + */ + public ChunkLocation( ChunkSnapshot snapshot ) { + Validate.notNull( snapshot ); + worldName = snapshot.getWorldName(); + x = snapshot.getX(); + z = snapshot.getZ(); + } + + /** + * Get the x coordinate. + * + * @return + * Chunk coordinate x value. + */ + public int getX() { + return x; + } + + /** + * Set the x coordinate. + * + * @param x + * Chunk coordinate x value. + */ + public ChunkLocation setX( int x ) { + this.x = x; + return this; + } + + /** + * Get the z coordinate. + * + * @return + * Chunk coordinate z value. + */ + public int getZ() { + return z; + } + + /** + * Set the z coordinate. + * + * @param z + * Chunk coordinate z value. + */ + public ChunkLocation setZ( int z ) { + this.z = z; + return this; + } + + + /** + * Add coordinates from current coordinates. + * + * @param x + * Chunk x coordinate value. + * @param z + * Chunk z coordinate value. + * @return + * This location. + */ + public ChunkLocation add( int x, int z ) { + this.x += x; + this.z += z; + return this; + } + + /** + * Subtract coordinates from current coordinates. + * + * @param x + * Chunk x coordinate value. + * @param z + * Chunk z coordinate value. + * @return + * This location. + */ + public ChunkLocation subtract( int x, int z ) { + this.x -= x; + this.z -= z; + return this; + } + + public String getWorldName() { + return worldName; + } + + /** + * Get the world. + * + * @return + * A non-null world, will fetch and cache the world if not cached already. + */ + public World getWorld() { + if ( world == null ) { + world = Bukkit.getWorld( worldName ); + } + return world; + } + + /** + * Set the world. + * + * @param world + * Cannot be null. + */ + public void setWorld( World world ) { + Validate.notNull( world ); + this.world = world; + this.worldName = world.getName(); + } + + /** + * Get the chunk this location represents. + * + * @return + * May load the chunk. + */ + public Chunk getChunk() { + return world.getChunkAt( x, z ); + } + + /** + * Check if the chunk that this location represents is loaded. + * + * @return + * If the chunk is loaded. Does not load the chunk. + */ + public boolean isLoaded() { + return getWorld().isChunkLoaded( x, z ); + } + + /** + * Load the chunk that this location represents. + */ + public void load() { + getWorld().loadChunk( x, z ); + } + + /** + * Check if this chunk exists. + * + * @return + * Checks if the chunk exists, does not have to be loaded. + */ + public boolean exists() { + return getWorld().loadChunk( x, z, false ); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ( ( worldName == null ) ? 0 : worldName.hashCode() ); + result = prime * result + x; + result = prime * result + z; + return result; + } + + @Override + public boolean equals(Object obj) { + if ( this == obj ) { + return true; + } + if ( obj == null ) { + return false; + } + if ( getClass() != obj.getClass() ) { + return false; + } + + ChunkLocation other = ( ChunkLocation ) obj; + + if ( worldName == null ) { + if ( other.worldName != null ) { + return false; + } + } else if ( !worldName.equals( other.worldName ) ) { + return false; + } + + if ( x != other.x ) { + return false; + } + if ( z != other.z ) { + return false; + } + return true; + } + + @Override + public String toString() { + return "ChunkLocation{x:" + x + ",z:" + z + ",world:" + worldName + "}"; + } +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/objects/Component.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/objects/Component.java new file mode 100644 index 0000000..2b9d2df --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/objects/Component.java @@ -0,0 +1,12 @@ +package com.aaaaahhhhhhh.bananapuncher714.minietest.objects; + +import java.util.Collection; +import java.util.Collections; + +public class Component { + + + public Collection< Component > getComponents() { + return Collections.emptySet(); + } +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/objects/mesh/Facet.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/objects/mesh/Facet.java new file mode 100644 index 0000000..913a494 --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/objects/mesh/Facet.java @@ -0,0 +1,11 @@ +package com.aaaaahhhhhhh.bananapuncher714.minietest.objects.mesh; + +import java.util.ArrayList; +import java.util.List; + +import org.bukkit.util.Vector; + +public class Facet { + public Vector normal; + public List< Vector > points = new ArrayList< Vector >(); +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/objects/mesh/LineSegment.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/objects/mesh/LineSegment.java new file mode 100644 index 0000000..fba5bb9 --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/objects/mesh/LineSegment.java @@ -0,0 +1,45 @@ +package com.aaaaahhhhhhh.bananapuncher714.minietest.objects.mesh; + +import org.bukkit.util.Vector; + +public class LineSegment { + public Vector point1, point2; + + public LineSegment( Vector point1, Vector point2 ) { + if ( point1.getX() < point2.getX() ) { + this.point1 = point1; + this.point2 = point2; + } else if ( point1.getX() > point2.getX() ) { + this.point1 = point2; + this.point2 = point1; + } else if ( point1.getY() < point2.getY() ) { + this.point1 = point1; + this.point2 = point2; + } else if ( point1.getY() > point2.getY() ) { + this.point1 = point2; + this.point2 = point1; + } else if ( point1.getZ() < point2.getZ() ) { + this.point1 = point1; + this.point2 = point2; + } else if ( point1.getZ() > point2.getZ() ) { + this.point1 = point2; + this.point2 = point1; + } else { + // point 1 and 2 are the same, I guess... + this.point1 = point1; + this.point2 = point2; + } + } + + public boolean isPoint() { + return point1.equals( point2 ); + } + + public double length() { + return point1.distance( point2 ); + } + + public Vector getVector() { + return point2.clone().subtract( point1 ); + } +} 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 new file mode 100644 index 0000000..5939a31 --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/objects/mesh/MeshBuilder.java @@ -0,0 +1,36 @@ +package com.aaaaahhhhhhh.bananapuncher714.minietest.objects.mesh; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import org.bukkit.util.Vector; + +public class MeshBuilder { + public List< Plane > planes = new ArrayList< Plane >(); + + public Plane addFacet( Facet facet ) { + for ( Plane plane : planes ) { + if ( plane.matches( facet.normal, facet.points.get( 0 ) ) ) { + plane.addShape( facet ); + return plane; + } + } + + Plane plane = new Plane(); + plane.normal = facet.normal; + 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 ); + return 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 new file mode 100644 index 0000000..578c584 --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/objects/mesh/Plane.java @@ -0,0 +1,46 @@ +package com.aaaaahhhhhhh.bananapuncher714.minietest.objects.mesh; + +import java.util.ArrayList; +import java.util.List; + +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; + +public class Plane { + public Vector normal; + public Vector point; + + 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(); + + List< Point > polygon = new ArrayList< Point >(); + for ( Vector p : facet.points ) { + double t = point.clone().subtract( p ).dot( normal ); + 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; + } +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/util/BukkitUtil.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/util/BukkitUtil.java new file mode 100644 index 0000000..60ef778 --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/util/BukkitUtil.java @@ -0,0 +1,183 @@ +package com.aaaaahhhhhhh.bananapuncher714.minietest.util; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Map; +import java.util.regex.Pattern; + +import org.bukkit.Bukkit; +import org.bukkit.command.PluginCommand; +import org.bukkit.event.Event; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.PluginDescriptionFile; +import org.bukkit.plugin.PluginLoader; +import org.bukkit.plugin.PluginManager; +import org.bukkit.plugin.SimplePluginManager; +import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.plugin.java.JavaPluginLoader; + +import com.aaaaahhhhhhh.bananapuncher714.minietest.MiniePlugin; + +public class BukkitUtil { + private static Constructor< PluginCommand > PLUGINCOMMAND_CONSTRUCTOR; + private static Field SIMPLEPLUGINMANAGER_FILEASSOCIATIONS; + private static Method JAVAPLUGINLOADER_GETCLASS_1; + private static Method JAVAPLUGINLOADER_GETCLASS_2; + private static Method JAVAPLUGINLOADER_SETCLASS; + private static Method JAVAPLUGINLOADER_REMOVECLASS_1; + private static Method JAVAPLUGINLOADER_REMOVECLASS_2; + + static { + try { + PLUGINCOMMAND_CONSTRUCTOR = PluginCommand.class.getDeclaredConstructor( String.class, Plugin.class ); + PLUGINCOMMAND_CONSTRUCTOR.setAccessible( true ); + SIMPLEPLUGINMANAGER_FILEASSOCIATIONS = SimplePluginManager.class.getDeclaredField( "fileAssociations" ); + SIMPLEPLUGINMANAGER_FILEASSOCIATIONS.setAccessible( true ); + JAVAPLUGINLOADER_SETCLASS = JavaPluginLoader.class.getDeclaredMethod( "setClass", String.class, Class.class ); + JAVAPLUGINLOADER_SETCLASS.setAccessible( true ); + } catch ( NoSuchMethodException | SecurityException | NoSuchFieldException e ) { + e.printStackTrace(); + } + + try { + JAVAPLUGINLOADER_GETCLASS_1 = JavaPluginLoader.class.getDeclaredMethod( "getClassByName", String.class ); + JAVAPLUGINLOADER_GETCLASS_1.setAccessible( true ); + } catch ( NoSuchMethodException | SecurityException e ) { + try { + JAVAPLUGINLOADER_GETCLASS_2 = JavaPluginLoader.class.getDeclaredMethod( "getClassByName", String.class, boolean.class, PluginDescriptionFile.class ); + JAVAPLUGINLOADER_GETCLASS_2.setAccessible( true ); + } catch ( NoSuchMethodException | SecurityException e1 ) { + e1.printStackTrace(); + } + } + + try { + JAVAPLUGINLOADER_REMOVECLASS_1 = JavaPluginLoader.class.getDeclaredMethod( "removeClass", String.class ); + JAVAPLUGINLOADER_REMOVECLASS_1.setAccessible( true ); + } catch ( NoSuchMethodException | SecurityException e ) { + try { + JAVAPLUGINLOADER_REMOVECLASS_2 = JavaPluginLoader.class.getDeclaredMethod( "removeClass", Class.class ); + JAVAPLUGINLOADER_REMOVECLASS_2.setAccessible( true ); + } catch ( NoSuchMethodException | SecurityException e1 ) { + e1.printStackTrace(); + } + } + } + + public static PluginCommand constructCommand( String id ) { + PluginCommand command = null; + try { + command = PLUGINCOMMAND_CONSTRUCTOR.newInstance( id, JavaPlugin.getPlugin( MiniePlugin.class ) ); + } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + e.printStackTrace(); + return null; + } + return command; + } + + /** + * Call an event synchronously, on the main thread + */ + public static void callEventSync( Event event ) { + if ( Bukkit.getServer().isPrimaryThread() ) { + Bukkit.getPluginManager().callEvent( event ); + } else { + Bukkit.getScheduler().scheduleSyncDelayedTask( JavaPlugin.getPlugin( MiniePlugin.class ), () -> { callEventSync( event ); } ); + } + } + + /** + * Check if the plugin given is loaded + * + * @param id + * case sensitive plugin id + * @return + * If the plugin is loaded and enabled + */ + public static boolean isPluginLoaded( String id ) { + return Bukkit.getPluginManager().isPluginEnabled( id ); + } + + public static Class< ? > getClassByName( String name ) { + PluginManager manager = Bukkit.getPluginManager(); + if ( manager instanceof SimplePluginManager ) { + SimplePluginManager sManager = ( SimplePluginManager ) manager; + try { + Map< Pattern, PluginLoader > associations = ( Map< Pattern, PluginLoader > ) SIMPLEPLUGINMANAGER_FILEASSOCIATIONS.get( sManager ); + for ( PluginLoader loader : associations.values() ) { + if ( loader instanceof JavaPluginLoader ) { + if ( JAVAPLUGINLOADER_GETCLASS_1 != null ) { + return ( Class< ? > ) JAVAPLUGINLOADER_GETCLASS_1.invoke( loader, name ); + } else { + return ( Class< ? > ) JAVAPLUGINLOADER_GETCLASS_2.invoke( loader, name, true, JavaPlugin.getPlugin( MiniePlugin.class ).getDescription() ); + } + } + } + } catch ( IllegalArgumentException | IllegalAccessException | InvocationTargetException e ) { + e.printStackTrace(); + } + } + return null; + } + + public static void setClassToJavaPluginLoader( String name, Class< ? > clazz ) { + PluginManager manager = Bukkit.getPluginManager(); + if ( manager instanceof SimplePluginManager ) { + SimplePluginManager sManager = ( SimplePluginManager ) manager; + try { + Map< Pattern, PluginLoader > associations = ( Map< Pattern, PluginLoader > ) SIMPLEPLUGINMANAGER_FILEASSOCIATIONS.get( sManager ); + for ( PluginLoader loader : associations.values() ) { + if ( loader instanceof JavaPluginLoader ) { + JAVAPLUGINLOADER_SETCLASS.invoke( loader, name, clazz ); + } + } + } catch ( IllegalArgumentException | IllegalAccessException | InvocationTargetException e ) { + e.printStackTrace(); + } + } + } + + public static boolean removeClassFromJavaPluginLoader( Class clazz ) { + if ( JAVAPLUGINLOADER_REMOVECLASS_2 != null ) { + PluginManager manager = Bukkit.getPluginManager(); + if ( manager instanceof SimplePluginManager ) { + SimplePluginManager sManager = ( SimplePluginManager ) manager; + try { + Map< Pattern, PluginLoader > associations = ( Map< Pattern, PluginLoader > ) SIMPLEPLUGINMANAGER_FILEASSOCIATIONS.get( sManager ); + for ( PluginLoader loader : associations.values() ) { + if ( loader instanceof JavaPluginLoader ) { + JAVAPLUGINLOADER_REMOVECLASS_2.invoke( loader, clazz ); + } + } + } catch ( IllegalArgumentException | IllegalAccessException | InvocationTargetException e ) { + e.printStackTrace(); + } + } + return true; + } + return false; + } + + public static boolean removeClassFromJavaPluginLoader( String name ) { + if ( JAVAPLUGINLOADER_REMOVECLASS_1 != null ) { + PluginManager manager = Bukkit.getPluginManager(); + if ( manager instanceof SimplePluginManager ) { + SimplePluginManager sManager = ( SimplePluginManager ) manager; + try { + Map< Pattern, PluginLoader > associations = ( Map< Pattern, PluginLoader > ) SIMPLEPLUGINMANAGER_FILEASSOCIATIONS.get( sManager ); + for ( PluginLoader loader : associations.values() ) { + if ( loader instanceof JavaPluginLoader ) { + JAVAPLUGINLOADER_REMOVECLASS_1.invoke( loader, name ); + } + } + } catch ( IllegalArgumentException | IllegalAccessException | InvocationTargetException e ) { + e.printStackTrace(); + } + } + return true; + } + return false; + } +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/util/FileUtil.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/util/FileUtil.java new file mode 100644 index 0000000..9deecbb --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/util/FileUtil.java @@ -0,0 +1,150 @@ +package com.aaaaahhhhhhh.bananapuncher714.minietest.util; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.OutputStream; +import java.io.Serializable; + +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; + +/** + * Simple file management including YAML updaters + * + * @author BananaPuncher714 + */ +public final class FileUtil { + public static void saveAndUpdate( InputStream stream, File output, boolean trim ) { + if ( !output.exists() ) { + saveToFile( stream, output, false ); + } + updateConfigFromFile( output, stream, trim ); + } + + public static void saveToFile( InputStream stream, File output, boolean force ) { + if ( force && output.exists() ) { + output.delete(); + } + if ( !output.exists() ) { + output.getParentFile().mkdirs(); + try ( OutputStream outStream = new FileOutputStream( output ) ) { + byte[] buffer = new byte[ stream.available() ]; + + int len; + while ( ( len = stream.read( buffer ) ) > 0) { + outStream.write( buffer, 0, len ); + } + stream.close(); + } catch ( FileNotFoundException e ) { + e.printStackTrace(); + } catch ( IOException e ) { + e.printStackTrace(); + } + } + } + + public static void updateConfigFromFile( File toUpdate, InputStream toCopy ) { + updateConfigFromFile( toUpdate, toCopy, false ); + } + + public static void updateConfigFromFile( File toUpdate, InputStream toCopy, boolean trim ) { + FileConfiguration config = YamlConfiguration.loadConfiguration( new InputStreamReader( toCopy ) ); + FileConfiguration old = YamlConfiguration.loadConfiguration( toUpdate ); + + for ( String key : config.getKeys( true ) ) { + if ( !old.contains( key ) ) { + old.set( key, config.get( key ) ); + } + } + + if ( trim ) { + for ( String key : old.getKeys( true ) ) { + if ( !config.contains( key ) ) { + old.set( key, null ); + } + } + } + + try { + old.save( toUpdate ); + } catch ( Exception exception ) { + exception.printStackTrace(); + } + } + + public static boolean move( File original, File dest, boolean force ) { + if ( dest.exists() ) { + if ( !force ) { + return false; + } else { + recursiveDelete( dest ); + } + } + dest.getParentFile().mkdirs(); + original.renameTo( dest ); + recursiveDelete( original ); + return true; + } + + public static void recursiveDelete( File file ) { + if ( !file.exists() ) { + return; + } + if ( file.isDirectory() ) { + for ( File lower : file.listFiles() ) { + recursiveDelete( lower ); + } + } + file.delete(); + } + + public static < T extends Serializable > T readObject( Class< T > clazz, File file ) throws IOException, ClassNotFoundException { + if ( !file.exists() ) { + return null; + } + T head = null; + FileInputStream fis = new FileInputStream( file ); + ObjectInputStream ois = new ObjectInputStream( fis ); + + head = ( T ) ois.readObject(); + + ois.close(); + fis.close(); + return head; + } + + public static void writeObject( Serializable object, File file ) { + file.getParentFile().mkdirs(); + try { + FileOutputStream fos = new FileOutputStream( file ); + ObjectOutputStream oos = new ObjectOutputStream( fos ); + + oos.writeObject( object ); + + oos.close(); + fos.close(); + } catch ( Exception exception ) { + exception.printStackTrace(); + } + } + + public static File getImageFile( File dir, String prefix ) { + File image = new File( dir + "/" + prefix + ".png" ); + for ( File file : dir.listFiles() ) { + String fileName = file.getName(); + if ( fileName.equalsIgnoreCase( prefix + ".gif" ) ) { + return file; + } else if ( fileName.equalsIgnoreCase( prefix + ".png" ) || fileName.equalsIgnoreCase( prefix + ".jpg" ) || fileName.equalsIgnoreCase( prefix + ".jpeg" ) || fileName.equalsIgnoreCase( prefix + ".bmp" ) ) { + image = file; + } + } + return image; + } +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/EdgeCollection.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/EdgeCollection.java new file mode 100644 index 0000000..193b33f --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/EdgeCollection.java @@ -0,0 +1,143 @@ +package com.aaaaahhhhhhh.bananapuncher714.tess4j; + +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiFunction; + +/* + * A primitive sorted linked list of edges + * - O(n) insert, search + * - O(1) upper, lower, remove + */ +public class EdgeCollection< T > implements Iterable< T > { + Map< T, Node > quickMap = new ConcurrentHashMap< T, Node >(); + Node head = new Node( null ); + + BiFunction< T, T, Boolean > isGreaterThan; + + public EdgeCollection( BiFunction< T, T, Boolean > compare ) { + isGreaterThan = compare; + } + + 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; + } + + // Not necessary, for now +// @Override +// public void remove() { +// T val = current.value; +// current = current.lower; +// EdgeCollection.this.remove( val ); +// } + }; + } +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/EdgeDict.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/EdgeDict.java new file mode 100644 index 0000000..b84347f --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/EdgeDict.java @@ -0,0 +1,128 @@ +package com.aaaaahhhhhhh.bananapuncher714.tess4j; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.BiFunction; +import java.util.function.Supplier; + +/** + * This edge dictionary is a loosely sorted collection of regions that are organized by the _current_ event vertex, + * and its relationship with the upper edge of each region. Regions are sorted on insertion, and stored in a doubly + * linked list thereafter. Searching for a region entails moving backwards through the list starting at the tail + * until a region which matches the sorting method is found. + * + * There are no checks in place to prevent undefined behavior of any kind. + * + * Regions are sorted in ascending order. + * + * Searching for a region is done from head to tail, until a region that is greater than or equal has been found. + */ +public class EdgeDict { + Map< RegionOld, Node > quickMap = new HashMap< RegionOld, Node >(); + /* + * The comparator function is responsible for determining whether a region is less than or equal to another + * + * Given o1 and o2, return true if o1 is less than or equal to o2 + */ + BiFunction< RegionOld, RegionOld, Boolean > comparator; + Node head; + + public EdgeDict( Supplier< VertexOld > supplier ) { + head = new Node( null ); + + head.after = head; + head.before = head; + + comparator = ( o1, o2 ) -> { + VertexOld vert = supplier.get(); + HalfEdge e1 = o1.upperEdge; + HalfEdge e2 = o2.upperEdge; + + // Check if the left vertex is the current event + if ( e1.sym().origin == vert ) { + if ( e2.sym().origin == vert ) { + // Compare right vertices + // Contrived, why not check slope instead? + // Edge case is if slope is infinity + if ( e1.origin.lessThanOrEqualTo( e2.origin ) ) { + // Check if the right vertex is below the edge + return e1.origin.compareTo( e2.sym().origin, e2.origin ) <= 0; + } + return e2.origin.compareTo( e1.sym().origin, e1.origin ) >= 0; + } + return vert.compareTo( e2.sym().origin, e2.origin ) <= 0; + } + + if ( e2.sym().origin == vert ) { + return vert.compareTo( e1.sym().origin, e1.origin ) >= 0; + } + + // Measure the distance and see if one is shorter + double d1 = vert.verticalDistance( e1.sym().origin, e1.origin ); + double d2 = vert.verticalDistance( e2.sym().origin, e2.origin ); + + return d1 >= d2; + }; + } + + public RegionOld above( RegionOld region ) { + return quickMap.get( region ).after.region; + } + + public RegionOld below( RegionOld region ) { + return quickMap.get( region ).before.region; + } + + public void insert( RegionOld region ) { + // Insert from the back + insertBefore( null, region ); + } + + public void remove( RegionOld region ) { + Node node = quickMap.remove( region ); + if ( node != null ) { + node.before.after = node.after.before; + } + region.upperEdge.region = null; + } + + public void insertBefore( RegionOld startRegion, RegionOld region ) { + Node node = new Node( region ); + quickMap.put( region, node ); + + // Start at the tail if null + Node temp = startRegion == null ? head : quickMap.get( startRegion ); + // Look for a region that is less than or equal to region, and insert after + + do { + temp = temp.before; + } while ( temp.region != null && !comparator.apply( temp.region, region ) ); + + node.after = temp.after; + node.after.before = node; + temp.after = node; + node.before = temp; + } + + // Similar to above + public RegionOld search( RegionOld region ) { + Node temp = head; + + // Find the first region that is greater than or equal to region + do { + temp = temp.after; + } while ( temp.region != null && !comparator.apply( region, temp.region ) ); + + return temp.region; + } + + protected class Node { + Node before; + Node after; + RegionOld region; + + Node( RegionOld region ) { + this.region = region; + } + } +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Face.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Face.java new file mode 100644 index 0000000..7a9c353 --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Face.java @@ -0,0 +1,19 @@ +package com.aaaaahhhhhhh.bananapuncher714.tess4j; + +public class Face { + // An edge that is attached to this face + HalfEdge edge; + boolean inside; + + public Face( HalfEdge edge ) { + this.edge = edge; + } + + public void assign() { + HalfEdge temp = edge; + do { + temp.face = this; + temp = temp.nextLeft; + } while ( temp != edge ); + } +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/HalfEdge.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/HalfEdge.java new file mode 100644 index 0000000..dd66224 --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/HalfEdge.java @@ -0,0 +1,249 @@ +package com.aaaaahhhhhhh.bananapuncher714.tess4j; + +public class HalfEdge { + // The half edge opposite of this one + HalfEdge opposite; + + // The vertex that this half edge extends from + VertexOld origin; + // The face to the left of this edge + Face face; + + // The next edge CCW around the origin + // Keeps the same origin + HalfEdge nextOrigin; + // The next edge CCW around the left face + // The destination becomes the origin + HalfEdge nextLeft; + + RegionOld region; + int winding; + + private HalfEdge() { + origin = new VertexOld( this ); + nextOrigin = this; + } + + // The opposite, or symmetrical half edge to this one + public HalfEdge sym() { + return opposite; + } + + public HalfEdge prevOrigin() { + return sym().nextLeft; + } + + public HalfEdge prevR() { + return sym().nextOrigin; + } + + public HalfEdge nextD() { + return prevR().sym(); + } + + // Positive means left to right, aka the destination > origin + public boolean isPositiveEdge() { + return origin.lessThanOrEqualTo( opposite.origin ); + } + + @Override + public String toString() { + return "HalfEdge{origin=" + origin + ",opposite=" + opposite.origin + ",nextOrigin=" + nextOrigin.origin + ",nextLeft=" + nextLeft.origin + "}"; + } + + public static HalfEdge makeEdge() { + // TODO Half edges are always made in pairs, should really simplify this and make it cleaner + HalfEdge main = new HalfEdge(); + HalfEdge opposite = new HalfEdge(); + + main.opposite = opposite; + main.nextLeft = opposite; + opposite.opposite = main; + opposite.nextLeft = main; + + Face face = new Face( main ); + main.face = face; + opposite.face = face; + + return main; + } + + public static void swapLinks( HalfEdge e1, HalfEdge e2 ) { + HalfEdge e1Next = e1.nextOrigin; + HalfEdge e2Next = e2.nextOrigin; + + e1Next.sym().nextLeft = e2; + e2Next.sym().nextLeft = e1; + e1.nextOrigin = e2Next; + e2.nextOrigin = e1Next; + } + + /** + * The basic operation for changing the mesh connectivity and topology. It changes the mesh so that + * original.nextOrigin = OLD( destination.nextOrigin ) + * destination.nextOrigin = OLD( original.nextOrigin ) + * where OLD( ... ) means the value before the splice operation + * + * This can have two effects on the vertex structure: + * if original.face == destination.face, then the face is split into two + * if original.face != destination.face, then the two faces are combined into one + * In both cases, destination.face is changed, but original.face is not. + */ + public static void splice( HalfEdge original, HalfEdge destination ) { + boolean mergeVertices = false; + boolean mergeFaces = false; + + // Merging vertices + if ( original.origin != destination.origin ) { + mergeVertices = true; + HalfEdge prev = destination.origin.edge; + HalfEdge dest = prev; + do { + dest.origin = original.origin; + dest = dest.nextOrigin; + } while ( dest != prev ); + } + + // Merging faces + if ( original.face != destination.face ) { + mergeFaces = true; + HalfEdge prev = destination.face.edge; + HalfEdge dest = prev; + do { + dest.face = original.face; + dest = dest.nextLeft; + } while ( dest != prev ); + } + + swapLinks( original, destination ); + + if ( !mergeVertices ) { + // Splitting a vertex into two, so create + // a new vertex with destination as the edge, + // update all edges in the destination loop, + // and set the old vertex edge to this + + // TODO Vertices always need a half edge, constructor? + new VertexOld( destination ).assign(); + original.origin.edge = original; + } + + if ( !mergeFaces ) { + // Splitting a face/loop into two + // Similar to splitting a vertex + new Face( destination ).assign();; + original.face.edge = original; + } + } + + /** + * Delete the edge. There are several cases: + * if edge.leftFace != edge.rightFace, then two faces/loops are joined into one + * else, one loop is being split into two, and the new loop will contain the opposite vertex + * This function could be implemented as two calls to splice, but it would be less efficient + */ + public static void delete( HalfEdge edge ) { + boolean mergeFaces = false; + HalfEdge sym = edge.sym(); + + if ( edge.face != sym.face ) { + mergeFaces = true; + HalfEdge original = edge.face.edge; + HalfEdge dest = original; + do { + dest.face = sym.face; + dest = dest.nextLeft; + } while ( dest != original ); + } + + if ( edge.nextOrigin == edge ) { + HalfEdge original = edge.origin.edge; + HalfEdge dest = original; + do { + dest.origin = null; + dest = dest.nextOrigin; + } while ( dest != original ); + } else { + sym.face.edge = edge.prevOrigin(); + edge.origin.edge = edge.nextOrigin; + + splice( edge, edge.prevOrigin() ); + if ( !mergeFaces ) { + // Creating a new face + new Face( edge ).assign(); + } + } + + if ( sym.nextOrigin == sym ) { + sym.face.assign(); + sym.origin.assign(); + } else { + edge.face.edge = sym.prevOrigin(); + sym.origin.edge = sym.nextOrigin; + splice( sym, sym.prevOrigin() ); + } + } + + public static HalfEdge addEdgeVertex( HalfEdge orig ) { + HalfEdge edge = makeEdge(); + HalfEdge sym = edge.sym(); + + swapLinks( edge, orig.nextLeft ); + + edge.origin = orig.sym().origin; + + VertexOld vert = new VertexOld( sym ); + HalfEdge dest = sym; + do { + dest.origin = vert; + dest = dest.nextOrigin; + } while ( dest != sym ); + sym.face = orig.face; + edge.face = orig.face; + + return edge; + } + + public static HalfEdge splitEdge( HalfEdge edge ) { + HalfEdge tempEdge = addEdgeVertex( edge ); + HalfEdge sym = tempEdge.sym(); + + swapLinks( edge.sym(), edge.sym().prevOrigin() ); + swapLinks( edge.sym(), sym ); + + edge.sym().origin = sym.origin; + sym.sym().origin.edge = sym.sym(); + sym.sym().face = edge.sym().face; + + return sym; + } + + public static HalfEdge connect( HalfEdge e1, HalfEdge e2 ) { + boolean mergeFaces = false; + HalfEdge edge = makeEdge(); + + HalfEdge sym = edge.sym(); + + if ( e1.face != e2.face ) { + mergeFaces = true; + e2.face.edge = e1; + e2.face.assign(); + } + + swapLinks( edge, e1.nextLeft ); + swapLinks( sym, e2 ); + + edge.origin = e1.sym().origin; + sym.origin = e2.origin; + edge.face = e1.face; + sym.face = e1.face; + + e1.face.edge = sym; + + if ( !mergeFaces ) { + new Face( edge ).assign(); + } + + return edge; + } +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/HalfWing.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/HalfWing.java new file mode 100644 index 0000000..63d3b29 --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/HalfWing.java @@ -0,0 +1,105 @@ +package com.aaaaahhhhhhh.bananapuncher714.tess4j; + +public class HalfWing { + protected HalfWing sym; + protected HalfWing prev; + protected HalfWing next; + + protected Vertex origin; + + public HalfWing() { + init(); + + new HalfWing( this ); + } + + private HalfWing( HalfWing 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; + } + + public HalfWing setOrigin( Vertex vert ) { + origin = vert; + return this; + } + + public HalfWing getSym() { + return sym; + } + + public HalfWing getPrev() { + return prev; + } + + public HalfWing setPrev( HalfWing wing ) { + this.prev = wing; + return this; + } + + public HalfWing getNext() { + return next; + } + + public HalfWing setNext( HalfWing wing ) { + this.next = wing; + return this; + } + + public Vertex getDest() { + return sym.origin; + } + + public Vector2d toVector2d() { + return getDest().getPosition().subtracted( getOrigin().getPosition() ); + } + + public boolean isZero() { + return getOrigin().equals( getDest() ); + } + + // Split this edge in half, and return the new edge + public HalfWing split() { + HalfWing wing = new HalfWing(); + + splice( wing, getNext() ); + + splice( getSym(), getNext() ); + splice( getSym(), wing.getSym() ); + + wing.setOrigin( getDest() ); + wing.getOrigin().setWing( wing ); + + getSym().setOrigin( wing.getDest() ); + getDest().setWing( getSym() ); + + wing.getOrigin().update(); + wing.getDest().update(); + + return wing.getSym(); + } + + public static void splice( HalfWing a, HalfWing b ) { + HalfWing ap = a.prev; + HalfWing bp = b.prev; + + a.prev = bp; + b.prev = ap; + + ap.sym.next = b; + bp.sym.next = a; + } +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Mesh.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Mesh.java new file mode 100644 index 0000000..9e460ff --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Mesh.java @@ -0,0 +1,9 @@ +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/Point.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Point.java new file mode 100644 index 0000000..71a4cb7 --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Point.java @@ -0,0 +1,46 @@ +package com.aaaaahhhhhhh.bananapuncher714.tess4j; + +public class Point { + double x, y; + + public Point( double x, double y ) { + this.x = x; + this.y = y; + } + + public double getX() { + return x; + } + + public double getY() { + return y; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + long temp; + temp = Double.doubleToLongBits(x); + result = prime * result + (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(y); + result = prime * result + (int) (temp ^ (temp >>> 32)); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Point other = (Point) obj; + if (Double.doubleToLongBits(x) != Double.doubleToLongBits(other.x)) + return false; + if (Double.doubleToLongBits(y) != Double.doubleToLongBits(other.y)) + return false; + return true; + } +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Polygon.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Polygon.java new file mode 100644 index 0000000..d502890 --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Polygon.java @@ -0,0 +1,15 @@ +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/Region.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Region.java new file mode 100644 index 0000000..de3bba6 --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Region.java @@ -0,0 +1,7 @@ +package com.aaaaahhhhhhh.bananapuncher714.tess4j; + +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/tess4j/RegionOld.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/RegionOld.java new file mode 100644 index 0000000..448c0d5 --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/RegionOld.java @@ -0,0 +1,18 @@ +package com.aaaaahhhhhhh.bananapuncher714.tess4j; + +public class RegionOld { + HalfEdge upperEdge; + int windingNumber = 0; + + boolean inside = false; + boolean fixUpperEdge = false; + boolean sentinel = false; + boolean dirty = false; + + public void fixUpperEdge( HalfEdge edge ) { + HalfEdge.delete( upperEdge ); + fixUpperEdge = false; + upperEdge = edge; + edge.region = this; + } +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Scratch.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Scratch.java new file mode 100644 index 0000000..3f13ddb --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Scratch.java @@ -0,0 +1,367 @@ +package com.aaaaahhhhhhh.bananapuncher714.tess4j; + +public class Scratch { + public static void main( String[] args ) { + { + 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 ) ) ); +// +// HalfWing toInsert = new HalfWing(); +// HalfWing first = new HalfWing(); +// +// toInsert.getOrigin().setPosition( new Vector2d( -.95454545454, -1.36363636 ) ); +// toInsert.getDest().setPosition( new Vector2d( 0, -2 ) ); +// first.getOrigin().setPosition( new Vector2d( -1.5, -3 ) ); +// first.getDest().setPosition( new Vector2d( 0, -2 ) ); +// +// toInsert.getOrigin().setPosition( new Vector2d( 0, 0 ) ); +// toInsert.getDest().setPosition( new Vector2d( 2, 0 ) ); +// first.getOrigin().setPosition( new Vector2d( 1, 1 ) ); +// first.getDest().setPosition( new Vector2d( 2, 3 ) ); +// +// System.out.println( greaterThanOrEqualTo( toInsert, first ) ); +// } +// { +// Point[] points = { +// new Point( 1, -1 ), +// new Point( 1, 1 ), +// new Point( -1, 1 ), +// new Point( 0, 1 ), +// new Point( 1, 1 ), +// new Point( 0, -1 ) +// }; +// +// Vertex v = null; +// for ( Point point : points ) { +// HalfWing edge = new HalfWing(); +// edge.getDest().setPosition( new Vector2d( point.x, point.y ) ); +// if ( v == null ) { +// v = edge.getOrigin(); +// } else { +// HalfWing.splice( edge, v.getWing() ); +// } +// } +// v.update(); +// +// System.out.println( "Before sort" ); +// HalfWing edge = v.getWing(); +// do { +// System.out.println( edge.getDest() ); +// edge = edge.getPrev(); +// } while ( edge != v.getWing() ); +// +// v.sort(); +// +// System.out.println( "After sort" ); +// edge = v.getWing(); +// do { +// System.out.println( edge.getDest() ); +// edge = edge.getPrev(); +// } while ( edge != v.getWing() ); +// } +// { +// HalfWing a = new HalfWing(); +// HalfWing b = new HalfWing(); +// +// b.getSym().setOrigin( a.getDest() ); +// +// a.getOrigin().setPosition( new Vector2d( -1, 1 ) ); +// a.getSym().getOrigin().setPosition( new Vector2d( 1, 1 ) ); +// b.getOrigin().setPosition( new Vector2d( 1, 0 ) ); +// +// System.out.println( greaterThanOrEqualTo( b, a ) ); +// } +// { +// Vertex v = null; +// for ( int i = 0; i < 100; i++ ) { +// double deg = Math.random() * 360; +// double dist = Math.random() * 100 + 100; +// double x = dist * Math.cos( Math.toRadians( deg ) ); +// double y = dist * Math.sin( Math.toRadians( deg ) ); +// HalfWing edge = new HalfWing(); +// edge.getDest().setPosition( new Vector2d( x, y ) ); +// if ( v == null ) { +// v = edge.getOrigin(); +// } else { +// HalfWing.splice( edge, v.getWing() ); +// } +// } +// v.update(); +// +// System.out.println( "Before sort" ); +// HalfWing edge = v.getWing(); +// do { +// System.out.println( edge.getDest() ); +// edge = edge.getPrev(); +// } while ( edge != v.getWing() ); +// +// v.sort(); +// +// System.out.println( "After sort" ); +// edge = v.getWing(); +// do { +// System.out.println( edge.getDest() ); +// edge = edge.getPrev(); +// } while ( edge != v.getWing() ); +// } + +// { +// Vector2d a1 = new Vector2d( 0, 0 ); +// Vector2d a2 = new Vector2d( 0, 5 ); +// Vector2d b1 = new Vector2d( 0, 0 ); +// Vector2d b2 = new Vector2d( 0, 5 ); +// System.out.println( isGreaterThan( a1, a2, b1, b2 ) ); +// } +// HalfEdge upper = HalfEdge.makeEdge(); +// HalfEdge lower = HalfEdge.makeEdge(); +// +// upper.sym().origin = lower.sym().origin; +// upper.sym().origin.x = -1; +// upper.sym().origin.y = -1; +// +// upper.origin.x = 1; +// upper.origin.y = 1; +// lower.origin.x = 2; +// lower.origin.y = 2; +// +// System.out.println( "Right spliced? " + checkForRightSplice( upper, lower ) ); +// System.out.println( upper ); +// System.out.println( lower ); +// System.out.println( upper.prevOrigin() ); + +// { +// Vertex v1 = new Vertex( null ); +// v1.x = 0; +// v1.y = 0; +// Vertex v2 = new Vertex( null ); +// v2.x = 5; +// v2.y = 0; +// Vertex v3 = new Vertex( null ); +// v3.x = 1; +// v3.y = 1; +// Vertex v4 = new Vertex( null ); +// v4.x = 6; +// v4.y = -4; +// +// System.out.println( "Colinear and overlapping" ); +// System.out.println( Tessellator.intersection( v1, v2, v3, v4 ).x ); +// System.out.println( Util.closestPoint( v1, v2, v3, v4 ).x ); +// } +// { +// Vertex v1 = new Vertex( null ); +// v1.x = 0; +// v1.y = 0; +// Vertex v2 = new Vertex( null ); +// v2.x = 5; +// v2.y = 0; +// Vertex v3 = new Vertex( null ); +// v3.x = 6; +// v3.y = 0; +// Vertex v4 = new Vertex( null ); +// v4.x = 10; +// v4.y = 0; +// +// System.out.println( "Colinear but not overlapping" ); +// System.out.println( Tessellator.intersection( v1, v2, v3, v4 ).x ); +// System.out.println( Util.closestPoint( v1, v2, v3, v4 ).x ); +// } +// { +// Vertex v1 = new Vertex( null ); +// v1.x = 0; +// v1.y = 3; +// Vertex v2 = new Vertex( null ); +// v2.x = 5; +// v2.y = 3; +// Vertex v3 = new Vertex( null ); +// v3.x = -6; +// v3.y = 6; +// Vertex v4 = new Vertex( null ); +// v4.x = 2.5; +// v4.y = 6; +// +// System.out.println( "Parallel and overlapping but not colinear" ); +// System.out.println( Tessellator.intersection( v1, v2, v3, v4 ).x ); +// System.out.println( Util.closestPoint( v1, v2, v3, v4 ).x ); +// System.out.println( Util.closestPoint( v1, v2, v3, v4 ).y ); +// } +// { +// Vertex v1 = new Vertex( null ); +// v1.x = 0; +// v1.y = 3; +// Vertex v2 = new Vertex( null ); +// v2.x = 5; +// v2.y = 3; +// Vertex v3 = new Vertex( null ); +// v3.x = -100; +// v3.y = 6; +// Vertex v4 = new Vertex( null ); +// v4.x = -1; +// v4.y = 6; +// +// System.out.println( "Parallel but not overlapping nor colinear" ); +// System.out.println( Tessellator.intersection( v1, v2, v3, v4 ).x ); +// System.out.println( Util.closestPoint( v1, v2, v3, v4 ).x ); +// System.out.println( Util.closestPoint( v1, v2, v3, v4 ).y ); +// } + +// { +// HalfEdge root = HalfEdge.makeEdge(); +// root.origin.y = -1; +// HalfEdge next = HalfEdge.makeEdge(); +// next.sym().origin.x = 1; +// HalfEdge.swapLinks( root.sym(), next ); +// HalfEdge newEdge = HalfEdge.addEdgeVertex( root ); +// newEdge.sym().origin.x = -1; +// System.out.println( root ); +// System.out.println( root.nextLeft ); +// System.out.println( root.nextLeft.sym() ); +// System.out.println( root.nextLeft.nextLeft.nextLeft.sym() ); +// } +// +// System.out.println( "---" ); +// { +// HalfEdge root = HalfEdge.makeEdge(); +// root.origin.y = -1; +// HalfEdge next = HalfEdge.makeEdge(); +// next.sym().origin.x = 1; +// HalfEdge.swapLinks( root.sym(), next ); +// HalfEdge newEdge = HalfEdge.splitEdge( root ); +// newEdge.origin.x = -1; +// System.out.println( root ); +// System.out.println( root.nextLeft ); +// System.out.println( root.nextLeft.nextLeft ); +// System.out.println( root.nextLeft.nextLeft.nextLeft ); +// } + } + + private static boolean greaterThanOrEqualTo( HalfWing a, HalfWing 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 ( !( a.isPositive() && b.isPositive() ) ) { +// throw new IllegalStateException( "Negative edge detected" ); +// } + + // a1 < a2 + Vector2d a1 = a.getOrigin().getPosition(); + Vector2d a2 = a.getDest().getPosition(); + // b1 < b2 + Vector2d b1 = b.getOrigin().getPosition(); + Vector2d b2 = b.getDest().getPosition(); + + if ( a1.equals( b1 ) ) { + return b2.subtracted( b1 ).cross( a2.subtracted( a1 ) ) >= 0; + } else if ( a1.x < b1.x ) { + System.out.println( "LESS A" ); + return b1.subtracted( a1 ).cross( a2.subtracted( a1 ) ) >= 0; + } else if ( a1.x > b1.x ) { + System.out.println( "MORAY" ); + return b2.subtracted( b1 ).cross( a1.subtracted( b1 ) ) >= 0; + } else { + return a1.y > b1.y; + } + } + + public static boolean isGreaterThan( Vector2dOld a1, Vector2dOld a2, Vector2dOld b1, Vector2dOld b2 ) { + // Invariants: + // a1 < a2 + // b1 < b2 + // a.length > 0 + // b.length > 0 + // a and b do not intersect + // a and b do not overlap + + if ( a1.equals( b1 ) ) { + return b2.subtract( b1 ).cross( a2.subtract( a1 ) ) >= 0; + } else if ( a1.x < b1.x ) { + return b1.subtract( a1 ).cross( a2.subtract( a1 ) ) >= 0; + } else if ( a1.x > b1.x ) { + return a1.subtract( b1 ).cross( b2.subtract( b1 ) ) >= 0; + } else { + return a1.y > b1.y; + } + } + + public static boolean checkForRightSplice( HalfEdge upEdge, HalfEdge downEdge) { + // Check if the right vertex of the upper edge is to the left or below the lower edge's right vertex + if ( upEdge.origin.lessThanOrEqualTo( downEdge.origin ) ) { + // Check if the right vertex is above the lower edge + if ( upEdge.origin.compareTo( downEdge.sym().origin, downEdge.origin ) > 0 ) { + // If so, no splicing is necessary + return false; + } + + // If the right vertex of the upper edge is not equal to the right vertex of the lower edge + if ( !upEdge.origin.equals( downEdge.origin ) ) { + // Split the lower edge in two, with the new edge on the right + HalfEdge.splitEdge( downEdge.sym() ); + // Move the right vertex of the lower edge to the + HalfEdge.splice( upEdge, downEdge.prevOrigin() ); + + } else if ( upEdge.origin != downEdge.origin ) { + // Merge the two vertices if they are not already the same + HalfEdge.splice( downEdge.prevOrigin(), upEdge ); + } + } else { + // Check if the lower edge's right vertex is on or below the upper edge + if ( downEdge.origin.compareTo( upEdge.sym().origin, upEdge.origin ) <= 0 ) { + return false; + } + + // Split the upper edge + HalfEdge.splitEdge( upEdge.sym() ); + // Move the upper edge to the lower edge's vertex + HalfEdge.splice( downEdge.prevOrigin(), upEdge ); + + } + return true; + } +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Tess4j.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Tess4j.java new file mode 100644 index 0000000..3ab0258 --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Tess4j.java @@ -0,0 +1,1842 @@ +package com.aaaaahhhhhhh.bananapuncher714.tess4j; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.PriorityQueue; +import java.util.Queue; +import java.util.Set; +import java.util.Stack; +import java.util.TreeSet; +import java.util.WeakHashMap; +import java.util.function.Supplier; + +import com.aaaaahhhhhhh.bananapuncher714.tess4j.region.RegionSimple; +import com.aaaaahhhhhhh.bananapuncher714.tess4j.region.RegionSimple.GluWindingRule; +import com.aaaaahhhhhhh.bananapuncher714.tess4j.region.WindingRuleSimple; + +// Tess4j(name subject to change) +public class Tess4j< T extends Region > { + // Change at your own peril + public static final double TOLERANCE = 0; + public static final double COLLINEAR_TOLERANCE = 1e-7; + public static final double VERTEX_TOLERANCE = 1e-7; + + private boolean debug = false; + + /* + * Represents the current stage that this tessellator is currently in + */ + private Tess4jStage tessStage; + private Vector2dComparator comparator = new Vector2dComparator() { + @Override + public double compareX( Vector2d a, Vector2d b ) { + return a.x - b.x; + } + + @Override + public double compareY( Vector2d a, Vector2d b ) { + return a.y - b.y; + } + }; + + /* + * Use a custom sorting direction, of sorts. Not sure if it works + */ + public Tess4j( Supplier< T > voidSupplier, Vector2dComparator customSort ) { + this( voidSupplier ); + + this.comparator = customSort; + } + + public Tess4j( Supplier< T > voidSupplier ) { + tessStage = new StagePreTess( voidSupplier ); + } + + public void addPolygon( Polygon polygon, WindingRule< T > rule ) { + StagePreTess stage = tessStage.get(); + Queue< Vertex > queue = stage.vertexQueue; + Map< HalfWing, WindingRule< T > > windingMap = stage.windingMap; + + // Populate the vertex queue + // TODO Ensure that the polygon has at least 3 points? + HalfWing wing = null; + for ( Point point : polygon.points ) { + if ( wing == null ) { + // Make a self loop with one edge and one vertex + wing = new HalfWing(); + HalfWing.splice( wing, wing.getSym() ); + wing.getOrigin().update(); + } else { + // Split the previous edge in 2 and set the new edge as the current edge + wing = wing.split(); + } + + // Set the origin + wing.getOrigin().setPosition( new Vector2d( point.x, point.y ) ); + + // TODO Temporary angle check +// final double angle = 128; +// double cos = Math.cos( angle ); +// double sin = Math.sin( angle ); +// double x = point.x * cos - point.y * sin; +// double y = point.x * sin + point.y * cos; +// wing.getOrigin().setPosition( new Vector2d( x, y ) ); + + // Update the edge winding rule + windingMap.put( wing, rule ); + windingMap.put( wing.getSym(), rule.inverse() ); + + queue.add( wing.getOrigin() ); + } + } + + public void tessellate() { + /* + * Given a heap of vertices and edges, remove any conflicting intersections and form interior regions + */ + generateRegions(); + + /* + * Merge all adjacent collinear edges + */ + consolidateAdjacentCollinear(); + + /* + * Attempt to reduce the total amount of triangles by merging edges and vertices on the same line + * + * Since the minimum amount of triangles required to triangulate a polygon is n - 2, where n is the amount of vertices, + * we want to remove as many vertices as possible. A polygon may even be split into multiple polygons, if doing so would reduce + * the overall amount of triangles, as given by the formula: total = summation( polygon.vertices - 2 ) + */ +// reduceVertices(); + + /* + * TODO + * For now, we can skip reducing the vertice count and assume that it's just going to be kind of greedy... This is clearly + * suboptimal in terms of triangles generated, but oh well. + */ + + /* + * Given a list of simple polygons in the form of half wings, further divide them into monotone polygons if they are not already. + * Should take approximately O(nlogn) time + */ +// partitionMonotone(); + + /* + * Triangulate a list of monotone regions in linear time + */ +// triangulate(); + +// advance(); + } + + private void generateRegions() { + StageRegionGeneration stage = advance(); + Queue< Vertex > queue = stage.getQueue(); + EdgeCollection< HalfWing > edgeCollection = stage.getCollection(); + Map< HalfWing, WindingRule< T > > windingMap = stage.getWindingMap(); + Map< HalfWing, T > regionMap = stage.regionMap; + Set< HalfWing > interiorWings = stage.interior; + Set< HalfWing > inverseInteriorWings = stage.inverseInterior; + + /* + * The queue holds a collection of vertices, sorted by the comparator + * There may be vertices that must be merged together, so do a quick loop + * over all the vertices and merge any that are close enough together. + * + * Remember, vertices must be sorted by x, then y, as specified by the comparator + */ + Vertex event; + while ( ( event = queue.poll() ) != null ) { + // Check for any vertices that are nearby, in x and y direction and merge if similar + Vector2d vec = event.getPosition(); + for ( Iterator< Vertex > it = queue.iterator(); it.hasNext(); ) { + Vertex v = it.next(); + + Vector2d vPos = v.getPosition(); + // Once we go past a certain difference in X value, we know that any further vertices + // cannot be similar to the current event vertex, so break + if ( comparator.compareX( vPos, vec ) > VERTEX_TOLERANCE ) { + break; + } + + // This assumes that the vertices are NOT directly connected + if ( isSimilar( vec, vPos ) ) { + // Merge vertices that are the same as the event vertex + HalfWing.splice( event.getWing(), v.getWing() ); + + it.remove(); + } + } + + // Update all attached edges + event.update(); + + // Resolve any colinear intersections with this vertex first + resolveVertexIntersections( event ); + + // Vertices should start with an even number of edges, since they come in pairs + // After sorting them this one time, they should not need to be mass-sorted again + // Any precise modifications should keep the order + List< HalfWing > sorted = sort( event ); + if ( sorted.size() % 2 == 1 ) { + throw new IllegalStateException( "Invalid amount of edges for vertex" ); + } + + // Remove any negative edges from the edge collection before doing anything else + // This helps with proper ordering when inserting new edges and prevents inconsistencies + List< HalfWing > wings = new LinkedList< HalfWing >(); + for ( HalfWing wing : sorted ) { + if ( !isPositive( wing ) ) { + if ( !edgeCollection.remove( wing.getSym() ) ) { + throw new IllegalStateException( "Left going edge was not added to the edge collection" ); + } + } else { + if ( debug ) { + System.out.println( wing.hashCode() ); + System.out.println( "Adding " + wing.getOrigin().getPosition() + " to " + wing.getDest().getPosition() ); + } + wings.add( wing ); + } + } + + // All edges should be positive edges at this point + for ( HalfWing wing : wings ) { + // Resolve any intersections before inserting them into the edge collection + // The only intersections possible at this moment are: + // - An intersection in the middle of 2 edges + // - Colinear edges with the same left endpoint + resolveEdgeIntersections( wing ); + + // The issue with resolving edge intersections is that if the edge is + // barely positive, it can actually make it negative + + if ( !isPositive( wing ) ) { + System.out.println( wing.getOrigin().getPosition() + " to " + wing.getDest().getPosition() ); + throw new IllegalStateException( "Edge must be positive!" ); + } + + // Add the edge to the edge collection if it is a right-going edge + // Insert it as a positive edge + // Useful since if we split the edge, it does not have to be re-inserted in to the edge collection + edgeCollection.insert( wing ); + } + + // Get the default outer region + T region = stage.getDefaultRegion(); + + // Start marking interior/exterior regions as we work our way up through the edge collection + // This is in preparation for the next stage + // Keep track of whether the upper or lower region is an interior/exterior region + for ( HalfWing wing : edgeCollection ) { + WindingRule< T > rule = windingMap.get( wing ); + + boolean preInterior = region.isInterior(); + region = rule.apply( region ); + if ( regionMap.containsKey( wing ) ) { + T old = regionMap.get( wing ); + + if ( !old.equals( region ) ) { + // If this edge for some reason has calculated a different region + throw new IllegalStateException( "Inconsistent winding rules" ); + } else if ( preInterior ^ inverseInteriorWings.contains( wing ) ) { + // If the previous region's interior status does not match what was recorded previously + throw new IllegalStateException( "Inconsistent lower winding rules" ); + } + } else { + regionMap.put( wing, region ); + if ( region.isInterior() ) { + interiorWings.add( wing ); + } + + if ( preInterior ) { + inverseInteriorWings.add( wing ); + } + } + } + + // Now that all edges have been inserted into the edge collection, and all regions have been properly marked, + // we can add an extra step to remove any edges that have the same endpoints... + // Optional step + removeEqualEdges( event ); + + // Any new vertices must not be less than or equal to the current vertex + Vertex next = queue.peek(); + if ( next != null && compare( next, event ) < 1 ) { + throw new IllegalStateException( "Vertex less than event vertex detected!" ); + } + } + } + + private void consolidateAdjacentCollinear() { + StageColinearConsolidation stage = advance(); + Set< HalfWing > polygons = stage.polygons; + Set< List< HalfWing > > newPolygons = stage.newPolygons; + /* + * A short stage to combine colinear edges + */ + + // First join any adjacent colinear edges + for ( HalfWing wing : polygons ) { + List< HalfWing > wings = new LinkedList< HalfWing >(); + Set< HalfWing > checked = new HashSet< HalfWing >(); + + // Fast forward to the start of a colinear chain, rather than the middle of one + while ( Math.abs( wing.toVector2d().normalized().cross( wing.getPrev().getSym().toVector2d().normalized() ) ) < COLLINEAR_TOLERANCE ) { + wing = wing.getNext(); + } + + int count = 0; + int reduced = 0; + + while ( !checked.contains( wing ) ) { + count++; + HalfWing next = wing.getNext(); + + Vector2d a = wing.toVector2d().normalized(); + Vector2d b = next.toVector2d().normalized(); + + // Colinear + if ( Math.abs( a.cross( b ) ) <= COLLINEAR_TOLERANCE ) { + HalfWing after = next.getNext(); + + if ( wing.getOrigin().equals( after.getOrigin() ) ) { + throw new IllegalStateException( "Invalid edge with length of 0" ); + } + + // Unlink next from its destination + HalfWing.splice( next.getSym(), after ); + + // Unlink wing from next + HalfWing.splice( wing.getSym(), next ); + + // Set next origin to something else + next.getOrigin().setWing( next.getPrev() ); + + // Unlink next from the current vertex + HalfWing.splice( next, next.getSym().getNext() ); + + // Link wing and the wing after next together + HalfWing.splice( wing.getSym(), after ); + + // Set the destination vertex + wing.getSym().setOrigin( after.getOrigin() ); + wing.getDest().update(); + after.getOrigin().setWing( after ); + + reduced++; + } else { + wings.add( wing ); + checked.add( wing ); + wing = wing.getNext(); + } + } + + newPolygons.add( wings ); + } + } + + private void reduceVertices() { + StageVertexReduction stage = advance(); + Set< List< HalfWing > > polygons = stage.polygons; + /* + * The next step is to remove as many vertices as possible, before partitioning into monotone polygons + * All polygons should be simple interior region polygons at this point + * + * Reducing vertex count can happen in a variety of ways: + * - Joining non intersecting colinear edges + * - Joining an edge and a vertex + * - Joining 3 vertices in a row + * Each polygon should be independent of any other polygon, aside from sharing a common vertex + * If parallelizing, then each vertex should be made independent, or something like that + * + * This is a polynomial time operation, but can be skipped if undesired... + * + * For each edge, scan ahead to locate the first edge that it intersects with or is colinear, as well as any vertices + * Stop if it hits an edge, continue if it hits a vertex but does not hit a wall + * Scan backwards for only vertices as well + * + * How to resolve if a vertex and edge are colinear? + */ + + /* + * Given a vertex, find all other vertices that it intersects with + * Check if it can actually intersect with that vertex, or if it is blocked + * Simple solution: + * For each vertex + * Scan each other vertex + * See if it intersects with that vertex + * If so, then add it to a list for that vector + * After scanning each vertex, remove any lists with only 1 intersection + * At the end, a list of vertices with potential lines + * For each line in the list, get a list of lines that it intersects with + * For each group of lines that all intersect, calculate the + * O(n2) minimum? Since there can be at most O(n2) colinear lines + */ + + class IntersectionPair { + HalfWing start; + HalfWing end; + HalfWing otherStart; + HalfWing otherEnd; + + public IntersectionPair( HalfWing start, HalfWing end, HalfWing otherStart, HalfWing otherEnd ) { + this.start = start; + this.end = end; + this.otherStart = otherStart; + this.otherEnd = otherEnd; + } + } + + class VertexIntersection { + List< HalfWing > colinear = new ArrayList< HalfWing >(); + List< IntersectionPair > pairs = new LinkedList< IntersectionPair >(); + + public VertexIntersection( HalfWing wing ) { + colinear.add( wing ); + } + + public void addIntersection( IntersectionPair pair ) { + pairs.add( pair ); + } + } + + class PathingVertex { + HalfWing origin; + Vector2d right; + + Map< Vector2d, VertexIntersection > colinear = new HashMap< Vector2d, VertexIntersection >(); + + PathingVertex( HalfWing origin ) { + this.origin = origin; + right = origin.toVector2d().normalize(); + } + + Vector2d getPosition() { + return origin.getOrigin().getPosition(); + } + + void addWing( HalfWing wing ) { + for ( Entry< Vector2d, VertexIntersection > entry : colinear.entrySet() ) { + Vector2d vec = entry.getKey(); + if ( Math.abs( right.cross( vec ) ) <= COLLINEAR_TOLERANCE ) { + entry.getValue().colinear.add( wing ); + return; + } + } + colinear.put( right, new VertexIntersection( wing ) ); + } + } + + /* + * Create a list of PathingVertices as we loop around the polygon + * If we reach the same vertex again and it is being checked, remove it + * Otherwise, create a new one and add it to the checklist + * For each checked vertex, see if it can see the current vertex + * If it can, then add it to a list for that vertex and vector + * It should be sorted by distance from start + * Do this until we reach the last vertex + * This should prevent opposite redundant vertex lines + * remove any with 1, since that is a regular line + * Should now have a bunch of lines from one vertex to multiple other vertices + * There are duplicates, so reduce + * Now that we have a full list, determine which ones intersect? Or can that be done in a different step... + * Technically that is polynomial time... + * Iterate backwards from the vertex, until we reach the intersection vert + * For each vertex that is not the start or the end + * Check each intersection on that vertex + * If it intersects with a point before the start vertex + * Then it must intersect with this line + * + * A double loop is unavoidable unless the vector can be reversed, such that a colinear line + * How to make it such that exactly opposite vertices are equal, and in constant time? + */ + for ( List< HalfWing > wings : polygons ) { + System.out.println( "Starting at " + wings.get( 0 ).getOrigin() ); + Map< HalfWing, PathingVertex > vertices = new HashMap< HalfWing, PathingVertex >(); + // Keep track of the index of each vertex for constant time access + Map< HalfWing, Integer > indices = new HashMap< HalfWing, Integer >(); + List< PathingVertex > nodes = new ArrayList< PathingVertex >(); + + for ( HalfWing wing : wings ) { + List< Integer > recentlyLinked = new LinkedList< Integer >(); + PathingVertex pathing = vertices.computeIfAbsent( wing, PathingVertex::new ); + + List< Vector2d > added = new LinkedList< Vector2d >(); + int linkedIndex = -1; + for ( PathingVertex v : nodes ) { + linkedIndex++; + // Check if this vertex intersects with the node + Vector2d toWing = wing.getOrigin().getPosition().subtracted( v.getPosition() ).normalize(); + + // TODO How to prevent intersections with unprocessed wings? + // -2 2 1 3 + double cross = v.right.cross( toWing ); + if ( cross < - COLLINEAR_TOLERANCE ) { + continue; + } else if ( cross >= COLLINEAR_TOLERANCE ) { + // Not colinear, and need to update the angle + v.right = toWing; + } else { + // Colinear + } + + addWing: { + // Only add the vertex once per line that passes through it + // That way there are no redundant sub-lines being formed + for ( Vector2d vec : added ) { + if ( Math.abs( v.right.cross( vec ) ) <= COLLINEAR_TOLERANCE ) { + break addWing; + } + } + + // For each vertex, add once per vector + v.addWing( wing ); + added.add( v.right ); + recentlyLinked.add( linkedIndex ); + } + } + + if ( indices.containsKey( wing ) ) { + throw new IllegalStateException( "Already added edge" ); + } + + indices.put( wing, nodes.size() ); + nodes.add( pathing ); + + /* + * Ideally, it should only check for an intersection once, and not have multiple with the same + */ + if ( !recentlyLinked.isEmpty() ) { + for ( int i = 0; i <= recentlyLinked.get( 0 ); i++ ) { + PathingVertex vertex = nodes.get( i ); + + for ( VertexIntersection linked : vertex.colinear.values() ) { + // Check each sub-segment of the linked list + // Each sub-segment is guaranteed to be a unique segment + for ( int j = -1; j < linked.colinear.size() - 1; j++ ) { + HalfWing start = j == -1 ? vertex.origin : linked.colinear.get( j ); + HalfWing end = linked.colinear.get( j + 1 ); + + int startIndex = indices.get( start ); + int endIndex = indices.get( end ); + if ( end != wing ) { + for ( int index : recentlyLinked ) { + if ( startIndex <= index && index < endIndex ) { + linked.pairs.add( new IntersectionPair( start, end, nodes.get( index ).origin, wing ) ); + } + } + } + } + } + } + } + + // Now that the wing has been computed, scan backwards until we reach the lowest vertex that intersects with this node + // This is to map out any intersections, and straighten out the vertices and put them in order + /* + * What exactly is being checked in this case? + * As we iterate over each vertex, for each previous vertex, if a connection is possible then a link is added + * However, it might be the case that between two previous vertices, a connection is made that interferes with the + * connection we just made. So, loop backwards to the most previous connection, and check if there are any that intersect + * along the way. This is O(n2) + * + */ + } + + for ( PathingVertex v : nodes ) { + v.colinear.entrySet().removeIf( e -> e.getValue().colinear.size() == 1 ); + + if ( !v.colinear.isEmpty() ) { + System.out.println( "Checking " + v.getPosition() ); + for ( Entry< Vector2d, VertexIntersection > entry : v.colinear.entrySet() ) { + System.out.println( "\t" + entry.getKey() ); + VertexIntersection intersection = entry.getValue(); + for ( HalfWing w : intersection.colinear ) { + System.out.println( "\t\t" + w.getOrigin() ); + } + System.out.println( "\tOther colinear intersections" ); + for ( IntersectionPair pair : intersection.pairs ) { + System.out.println( "\t" + pair.start.getOrigin() + "\t" + pair.end.getOrigin() + "\tto\t" + pair.otherStart.getOrigin() + "\t" + pair.otherEnd.getOrigin() ); + } + } + } + + // The lists of HalfWings in each vertex contains a unique line segmet comprising of 2 or more vertices + // that intersect with the vertex. There is no particular order of the vertices, therefore care must + // be taken when determining if any sub-segments are valid, vs the entire line as a whole + } + } + } + + private void greedySplitter() { + /* + * Given a partition, split it into as many smaller regions while there are 3 or more colinear points that can be connected + * + * Steps: + * - Locate all colinear points that are not obstructed + * - Remove sets with less than 3 points + * - Get the set of points which has the greatest amount + * - In the case of a tie, select the one with the least amount of other obstructions + * - Split the partition + * - For all colinear sets, split them into one or the other polygon + * - Remove sets with less than 3 points + * - Repeat the procedure until there are no more colinear sets remaining + */ + } + + private void partitionMonotone() { + StageMonotonePartition stage = advance(); + TreeSet< Vertex > vertices = stage.vertices; + Set< HalfWing > interior = stage.interior; + EdgeCollection< HalfWing > edgeCollection = new EdgeCollection< HalfWing >( this::greaterThanOrEqualTo ); + + /* + * Now that vertices have been reduced as much as possible, partition each polygon into a monotone region, and triangulate it, but slowly + */ + + // Helper class to keep track of the immediate upper and lower edges for a given vertex + class MarkedWing { + HalfWing upper; + HalfWing lower; + + public MarkedWing( HalfWing upper, HalfWing lower ) { + this.upper = upper; + this.lower = lower; + } + + public boolean equals( MarkedWing o ) { + return o != null && lower == o.lower && upper == o.upper; + } + } + + // Keep track of all vertices that need to be linked with a left-going edge + Map< Vertex, MarkedWing > leftMarked = new HashMap< Vertex, MarkedWing >(); + // Likewise, keep track of all vertices that need to be linked with a right-going edge + Map< Vertex, MarkedWing > rightMarked = new HashMap< Vertex, MarkedWing >(); + + Set< Vertex > toSort = new HashSet< Vertex >(); + // Sweep each vertex and mark the upper and lower regions + for ( Vertex event : vertices ) { + // Get the left and right wings + List< HalfWing > rightGoing = new LinkedList< HalfWing >(); + List< HalfWing > leftGoing = new LinkedList< HalfWing >(); + { + HalfWing wing = event.getWing(); + do { + if ( isPositive( wing ) ) { + rightGoing.add( wing ); + } else { + leftGoing.add( wing ); + if ( !edgeCollection.remove( wing.getSym() ) ) { + throw new IllegalStateException( "Edge not added to the edge collection" ); + } + } + } while ( ( wing = wing.getPrev() ) != event.getWing() ); + } + + // Create a mark if the vertex has no left xor no right going edges + // AKA, if it prevents the polygon from being monotone + // It should never have no left and no right edges, since + // that means it should have never been added to the queue in the first place + // If there are both left and right edges, then it's a normal edge and does not break monotony + MarkedWing eventMark = null; + if ( rightGoing.isEmpty() ^ leftGoing.isEmpty() ) { + HalfWing tempWing = event.getWing(); + if ( !isPositive( tempWing ) ) { + tempWing = tempWing.getSym(); + } + HalfWing lower = edgeCollection.searchLower( tempWing ); + // Since we are dealing with simple polygons, we can guarantee that + // if the lower edge is an interior edge, then the upper edge must be + // part of the same polygon, and that this point is inside + if ( interior.contains( lower ) ) { + // Get the upper edge of the same polygon + HalfWing upper = edgeCollection.upper( lower ); + + // The current vertex must be between upper and lower, + // and cannot be part of either edge + + // Save this information, for now + eventMark = new MarkedWing( upper, lower ); + } + } 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 ( Iterator< Entry< Vertex, MarkedWing > > it = rightMarked.entrySet().iterator(); it.hasNext(); ) { + Entry< Vertex, MarkedWing > entry = it.next(); + Vertex prev = entry.getKey(); + MarkedWing marked = entry.getValue(); + + HalfWing upper = marked.upper; + HalfWing lower = marked.lower; + + if ( upper.getDest() == event ) { + HalfWing newWing = new HalfWing(); + + newWing.setOrigin( event ); + newWing.getSym().setOrigin( prev ); + + HalfWing.splice( newWing, upper.getSym() ); + HalfWing.splice( newWing.getSym(), prev.getWing() ); + } else if ( lower.getDest() == event ) { + HalfWing newWing = new HalfWing(); + + newWing.setOrigin( event ); + newWing.getSym().setOrigin( prev ); + + HalfWing.splice( newWing, lower.getNext() ); + HalfWing.splice( newWing.getSym(), prev.getWing() ); + } else if ( marked.equals( eventMark ) ) { + // Need to sort this vertex, since we can't guarantee it's being inserted in the correct position + HalfWing newWing = new HalfWing(); + + newWing.setOrigin( event ); + newWing.getSym().setOrigin( prev ); + + HalfWing.splice( newWing, event.getWing() ); + HalfWing.splice( newWing.getSym(), prev.getWing() ); + + leftGoing.add( newWing ); + toSort.add( event ); + } else { + continue; + } + + // Remove the right marked vertex since it no longer breaks monotony + it.remove(); + } + + if ( eventMark != null ) { + // Since the mark might have had left going edges added, + // it may not need additional edges. Only add if it still + // does not have any left or right going edges + if ( leftGoing.isEmpty() ) { + leftMarked.put( event, eventMark ); + } else if ( rightGoing.isEmpty() ) { + rightMarked.put( event, eventMark ); + + toSort.add( event ); + } + } + + for ( HalfWing right : rightGoing ) { + if ( isPositive( right ) ) { + edgeCollection.insert( right ); + } else { + throw new IllegalStateException( "Attempted to insert negative edge" ); + } + } + } + + // Reverse sweep the vertices and connect any left marked vertices + for ( Entry< Vertex, MarkedWing > entry : leftMarked.entrySet() ) { + Vertex event = entry.getKey(); + MarkedWing mark = entry.getValue(); + // Only interested in marked vertices + if ( mark != null ) { + HalfWing upper = mark.upper; + HalfWing lower = mark.lower; + + Vertex lesser = upper.getOrigin(); + if ( compare( lesser, lower.getOrigin() ) > 0 ) { + lesser = lower.getOrigin(); + } + // Get the largest range of vertices that we need to check + Set< Vertex > checkSet = vertices.descendingSet().tailSet( event, 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 ( Vertex prev : checkSet ) { + // Do a similar check as with the right marked vertices + if ( lower.getOrigin() == prev ) { + HalfWing wing = new HalfWing(); + + wing.setOrigin( prev ); + wing.getSym().setOrigin( event ); + + HalfWing.splice( lower, wing ); + HalfWing.splice( wing.getSym(), event.getWing() ); + } else if ( upper.getOrigin() == prev ) { + HalfWing wing = new HalfWing(); + + wing.setOrigin( prev ); + wing.getSym().setOrigin( event ); + + HalfWing.splice( upper.getSym().getNext(), wing ); + HalfWing.splice( wing.getSym(), event.getWing() ); + } else if ( mark.equals( leftMarked.get( prev ) ) ) { + // Need to sort prev, since we can't guarantee it's being inserted in the correct position + HalfWing wing = new HalfWing(); + + wing.setOrigin( prev ); + wing.getSym().setOrigin( event ); + + HalfWing.splice( wing.getSym(), event.getWing() ); + HalfWing.splice( wing, prev.getWing() ); + + toSort.add( prev ); + } else { + // Not this vertex + continue; + } + + // Only need to merge with the first vertex to maintain monotony + break; + } + + toSort.add( event ); + } + } + + // When connecting vertices, the edges may have been inserted out of order + for ( Vertex vert : toSort ) { + sort( vert ); + } + + if ( debug ) { + Set< HalfWing > wings = new HashSet< HalfWing >( interior ); + int polyCount = 0; + while ( !wings.isEmpty() ) { + polyCount++; + final HalfWing start = wings.iterator().next(); + + Set< Vertex > checkSet = new HashSet< Vertex >(); + + boolean failed = false; + int count = 0; + HalfWing wing = start; + do { + count++; + wings.remove( wing ); + + if ( !checkSet.add( wing.getOrigin() ) ) { + failed = true; + } + } while ( ( wing = wing.getNext() ) != start ); + + if ( failed ) { + System.out.println( "Amount of edges: " + count ); + HalfWing temp = start; + do { + System.out.println( temp.getOrigin().getPosition().getX() + ", " + temp.getOrigin().getPosition().getY() ); + } while ( ( temp = temp.getNext() ) != start ); + } + } + System.out.println( "Amount of polygons: " + polyCount ); + } + } + + private void triangulate() { + StageTriangulation stage = advance(); + Set< HalfWing > interior = stage.interior; + Set< HalfWing > wings = stage.wings; + + // For each 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 + for ( HalfWing wing : wings ) { + Set< Vertex > toSort = new HashSet< Vertex >(); + Queue< HalfWing > vertices = new PriorityQueue< HalfWing >( ( a, b ) -> { + return compare( a.getOrigin(), b.getOrigin() ); + } ); + + HalfWing temp = wing; + do { + interior.add( temp ); + vertices.add( temp ); + } while ( ( temp = temp.getNext() ) != wing ); + + Stack< HalfWing > stack = new Stack< HalfWing >(); + // Add the first two vertices + stack.add( vertices.poll() ); + stack.add( vertices.poll() ); + + HalfWing prev = stack.peek(); + while ( vertices.size() > 1 ) { + HalfWing w = vertices.poll(); + if ( isPositive( w ) ^ isPositive( stack.peek() ) ) { + HalfWing current = stack.pop(); + while ( !stack.isEmpty() ) { + // Insert an edge from the current event to each vertex in the stack + HalfWing newWing = new HalfWing(); + + newWing.setOrigin( w.getOrigin() ); + newWing.getSym().setOrigin( current.getOrigin() ); + + HalfWing.splice( newWing, w ); + HalfWing.splice( newWing.getSym(), current ); + + toSort.add( w.getOrigin() ); + toSort.add( current.getOrigin() ); + + interior.add( newWing ); + + current = stack.pop(); + } + stack.push( prev ); + } else { + HalfWing lastPopped = stack.pop(); + boolean positive = isPositive( w ); + HalfWing leftEdge = positive ? w.getPrev() : w; + while ( !stack.isEmpty() ) { + HalfWing next = stack.peek(); + + HalfWing newWing = new HalfWing(); + + newWing.setOrigin( w.getOrigin() ); + newWing.getSym().setOrigin( next.getOrigin() ); + + // Check if the diagonal is inside the polygon + double cross = newWing.toVector2d().cross( leftEdge.toVector2d() ); + // Use a tolerance to avoid parallel lines with a non-zero cross product + boolean isInside = positive ? cross > COLLINEAR_TOLERANCE : cross < - COLLINEAR_TOLERANCE; + + if ( isInside ) { + HalfWing.splice( newWing, w ); + HalfWing.splice( newWing.getSym(), next ); + + toSort.add( w.getOrigin() ); + toSort.add( next.getOrigin() ); + + interior.add( newWing ); + + leftEdge = newWing; + + lastPopped = stack.pop(); + } else { + break; + } + } + stack.push( lastPopped ); + } + stack.push( w ); + + prev = w; + } + + HalfWing last = vertices.poll(); + stack.pop(); + while ( stack.size() > 1 ) { + HalfWing popped = stack.pop(); + + HalfWing newWing = new HalfWing(); + + newWing.setOrigin( last.getOrigin() ); + newWing.getSym().setOrigin( popped.getOrigin() ); + + HalfWing.splice( newWing, last ); + HalfWing.splice( newWing.getSym(), popped ); + + interior.add( newWing ); + + toSort.add( last.getOrigin() ); + toSort.add( popped.getOrigin() ); + } + + // Lazy solution, just sort everything + for ( Vertex v : toSort ) { + sort( v ); + } + } + } + + /* + * We can guarantee that the only left-going colinear edges are ones that have the same endpoints + * The next logical step is to find all edges that share the same endpoints(since there can be multiple) + * and either: remove all of them, or leave one if the lower or upper regions are not both interior or exterior + */ + private void removeEqualEdges( Vertex vert ) { + StageRegionGeneration stage = tessStage.get(); + Set< HalfWing > interiorWings = stage.interior; + Set< HalfWing > inverseInteriorWings = stage.inverseInterior; + + List< HalfWing > wings = new LinkedList< HalfWing >(); + { + final HalfWing start = vert.getWing(); + HalfWing wing = start; + do { + wings.add( wing ); + } while ( ( wing = wing.getPrev() ) != start ); + } + + class WingRange { + HalfWing start; + HalfWing end; + int winding = 0; + + public WingRange( HalfWing wing ) { + start = wing; + } + } + + // Form sets of similar edges + Map< Vertex, WingRange > ranges = new HashMap< Vertex, WingRange >(); + for ( HalfWing wing : wings ) { + // Only add edges that go from right to left + if ( !isPositive( wing ) ) { + Vertex v = wing.getDest(); + + WingRange range = ranges.computeIfAbsent( v, ( dir ) -> { return new WingRange( wing ); } ); + + if ( range.end != null && range.end.getPrev() != wing ) { + throw new IllegalStateException( "Vertex not sorted" ); + } + + range.end = wing; + + // Basic winding rules + // Add 1 if the upper region is in, -1 if the lower region is in, and 0 if both regions are in or out + boolean upperIn = interiorWings.contains( wing.getSym() ); + boolean lowerIn = inverseInteriorWings.contains( wing.getSym() ); + if ( upperIn ^ lowerIn ) { + range.winding += upperIn ? 1 : -1; + } else { + throw new IllegalStateException( "Wing is both an interior and exterior region!" ); + } + } + } + + for ( Entry< Vertex, WingRange > entry : ranges.entrySet() ) { + WingRange range = entry.getValue(); + HalfWing start = range.start; + HalfWing end = range.end; + + if ( range.winding == 0 ) { + // If there is no change in the interior status, then we want to remove this range of edges entirely + start = start.getSym().getNext(); + } else if ( start != end ) { + // Keep one edge and update the interior edge sets accordingly + if ( range.winding == 1 ) { + interiorWings.add( start.getSym() ); + inverseInteriorWings.remove( start.getSym() ); + } else if ( range.winding == -1 ) { + interiorWings.remove( start.getSym() ); + inverseInteriorWings.add( start.getSym() ); + } else { + throw new IllegalStateException( "Invalid winding order" ); + } + } else { + // Do nothing, since it is a normal edge + continue; + } + + if ( start == end ) { + // We want to remove ALL of the wings on this point... + // Essentially, remove this vertex from existence + HalfWing temp = start; + do { + interiorWings.remove( temp.getSym() ); + inverseInteriorWings.remove( temp.getSym() ); + + HalfWing prev = temp.getNext(); + HalfWing.splice( temp.getSym(), prev ); + prev.getOrigin().setWing( prev ); + + temp = temp.getPrev(); + } while ( temp != start ); + } else { + // Remove each edge in the range from their destination vertex + // Should be the destination vertex, but the order is not known + HalfWing temp = start; + while ( temp != end ) { + temp = temp.getPrev(); + + // Remove this edge from the interior wing set since it should no longer exist + interiorWings.remove( temp.getSym() ); + inverseInteriorWings.remove( temp.getSym() ); + + HalfWing prev = temp.getNext(); + HalfWing.splice( temp.getSym(), prev ); + prev.getOrigin().setWing( prev ); + } + + // Remove the range of edges from this vertex + HalfWing.splice( start, end ); + start.getOrigin().setWing( start ); + } + } + } + + /* + * As we sweep over each vertex, there is a chance that an edge that is still being processed may go through the current vertex + * We need to catch this and connect it to this vertex, to ensure no intersecting edges and for simpler processing later on + * + * There is also a chance that there are edges which have not been processed, but are close enough that they "pass" through this vertex + * Those need to be merged with this vertex BEFORE it gets processed + */ + private void resolveVertexIntersections( Vertex vertex ) { + StageRegionGeneration stage = tessStage.get(); + Queue< Vertex > queue = stage.getQueue(); + EdgeCollection< HalfWing > edgeCollection = stage.getCollection(); + Map< HalfWing, WindingRule< T > > windingMap = stage.getWindingMap(); + + // Split any edges passing through vertex, which do not end at vertex + Vector2d v = vertex.getPosition(); + for ( HalfWing wing : edgeCollection ) { + Vector2d a = wing.getOrigin().getPosition(); + Vector2d b = wing.getDest().getPosition(); + + if ( v == b ) { + // Skip if this wing ends at this vertex + continue; + } else if ( isSimilar( v, b ) || isSimilar( v, a ) ) { + // The vertices should have been merged already + throw new IllegalStateException( "Vertex not merged" ); + } else if ( compare( vertex, wing.getOrigin() ) < 1 || compare( vertex, wing.getDest() ) > -1 ) { + // Must be true: a < vertex < b + throw new IllegalStateException( "Vertex must be between edge" ); + } else { + Vector2d toB = b.subtracted( a ); + Vector2d toV = v.subtracted( a ); + + // Get perpendicular distance from v to the line formed by a + b + double t = toB.dot( toV ) / toB.length(); + Vector2d intersection = toB.multiplied( t ).add( a ); + // Or, if vertex is directly on the line formed by ab, then check the cross product + if ( Math.abs( toB.cross( toV ) ) <= COLLINEAR_TOLERANCE || isSimilar( v, intersection ) ) { + HalfWing split = wing.split(); + + // Set the vertex to this vertex + split.setOrigin( vertex ); + wing.getSym().setOrigin( vertex ); + + // Splice the newly split wing into the vertex + HalfWing.splice( vertex.getWing(), split ); + + // Don't forget to update the winding rules for the newly created edge!! + WindingRule< T > rule = windingMap.get( wing ); + windingMap.put( split, rule ); + windingMap.put( split.getSym(), rule.inverse() ); + } + } + } + + // Check for edges that may be close, but not necessarily be in the edge collection + // For example, if there is a positive horizontal edge from this vertex, and a + // vertical edge directly adjacent to this vertex, then the intersection formed by + // the two edges may be almost on top of this vertex, but greater. If that is the case, + // then there is a chance that the vertices that form the edges have not yet been processed + // and therefore the vertical edge is not in the edge collection. + // It is a pain to go back and modify a vertex once it has been processed, so pre-process + // it now, and split the not-yet processed edge into this vertex. + for ( Vertex queued : queue ) { + // Only check the lower vertices of edges, and only to a certain extent + // since we only need to worry about positive edges. + Vector2d queuedPos = queued.getPosition(); + if ( comparator.compareX( queuedPos, v ) > VERTEX_TOLERANCE ) { + break; + } else if ( comparator.compareY( queuedPos, v ) < VERTEX_TOLERANCE ) { + // Check any positive edges attached to the vertex to see if it should be merged into this vertex + HalfWing wing = queued.getWing(); + do { + if ( isPositive( wing ) ) { + Vector2d a = wing.getOrigin().getPosition(); + Vector2d b = wing.getDest().getPosition(); + + if ( v == b ) { + // Skip, will process this later + continue; + } else if ( isSimilar( v, b ) || isSimilar( v, a ) ) { + // The vertices should have been merged already + throw new IllegalStateException( "Vertex not merged!" ); + } else { + Vector2d toB = b.subtracted( a ); + Vector2d toV = v.subtracted( a ); + + // Get perpendicular distance from v to the line formed by a + b + double t = toB.dot( toV ) / toB.length(); + Vector2d intersection = toB.multiplied( t ).add( a ); + if ( isSimilar( v, intersection ) ) { + HalfWing split = wing.split(); + + // Set the vertex + split.setOrigin( vertex ); + wing.getSym().setOrigin( vertex ); + + // Splice the newly split wing into the vertex + HalfWing.splice( vertex.getWing(), split ); + + // Don't forget to update the winding rules for the newly created edge!! + WindingRule< T > rule = windingMap.get( wing ); + windingMap.put( split, rule ); + windingMap.put( split.getSym(), rule.inverse() ); + } + } + } + } while ( ( wing = wing.getPrev() ) != queued.getWing() ); + } + } + } + + /* + * Loop over each edge and try to split if they intersect. + * It may not capture all intersections, but the edge will + * be guaranteed to be intersection free + */ + private void resolveEdgeIntersections( HalfWing wing ) { + StageRegionGeneration stage = tessStage.get(); + Queue< Vertex > queue = stage.getQueue(); + EdgeCollection< HalfWing > edgeCollection = stage.getCollection(); + Map< HalfWing, WindingRule< T > > windingMap = stage.getWindingMap(); + + for ( HalfWing check : edgeCollection ) { + Optional< Intersection > optionalIntersection = getIntersection( wing, check ); + if ( optionalIntersection.isPresent() ) { + Intersection intersection = optionalIntersection.get(); + if ( intersection instanceof IntersectionEdgeToEdge ) { + IntersectionEdgeToEdge intEdge = ( IntersectionEdgeToEdge ) intersection; + + // The intersection should not intersect at the endpoints + if ( check.getDest() == wing.getDest() ) { + throw new IllegalStateException( "Invalid intersection" ); + } + + Vector2d point = intEdge.getPoint(); + // Check if point would have been merged with either of the destination vertices + // If it would have merged, then move the point of the destination to the intersection position + // This is to prevent creating edges between 2 vertices that are going to be merged, + // resulting in a 0 length edge. + Vertex origin; + // Flag for if a new vertex is created, and needs to be added to the event queue + boolean newPoint = true; + if ( isSimilar( point, wing.getDest().getPosition() ) ) { + // Use the current wing's destination as the intersection point + // Do not split + origin = wing.getDest(); + newPoint = false; + } else { + // Update the winding rules for both new edges + HalfWing aSplit = wing.split(); + WindingRule< T > aRule = windingMap.get( wing ); + windingMap.put( aSplit, aRule ); + windingMap.put( aSplit.getSym(), aRule.inverse() ); + + // Update the position + origin = aSplit.getOrigin(); + origin.setPosition( point ); + } + + if ( isSimilar( point, check.getDest().getPosition() ) ) { + // Use the intersected edge's endpoint as the intersection point + // Do not split + HalfWing.splice( check.getDest().getWing(), origin.getWing() ); + + origin = check.getDest(); + + newPoint = false; + } else { + HalfWing bSplit = check.split(); + WindingRule< T > bRule = windingMap.get( check ); + windingMap.put( bSplit, bRule ); + windingMap.put( bSplit.getSym(), bRule.inverse() ); + + // Splice the edges together into the same vertex + HalfWing.splice( bSplit, origin.getWing() ); + } + + // Update all vertices around the origin + origin.update(); + + // Don't re-insert the origin if it is an existing vertex + if ( newPoint ) { + queue.add( origin ); + } + } else if ( intersection instanceof IntersectionColinear ) { + IntersectionColinear intCol = ( IntersectionColinear ) intersection; + + HalfWing shorter = intCol.getShorterEdge(); + HalfWing longer = intCol.getLongerEdge(); + + // Update the winding rule for the split + HalfWing split = longer.split(); + WindingRule< T > rule = windingMap.get( longer ); + windingMap.put( split, rule ); + windingMap.put( split.getSym(), rule.inverse() ); + + // Update the vertex for the split + Vertex vert = shorter.getDest(); + split.setOrigin( vert ); + longer.getSym().setOrigin( vert ); + + // Splice the two together + HalfWing.splice( shorter.getSym(), split ); + } + } + } + } + + /* + * This makes some assumptions about a and b: + * - a and b are positive edges + * - a and b do not intersect at their rightmost endpoint + * - a and b share the same leftmost endpoint if they are colinear + */ + private Optional< Intersection > getIntersection( HalfWing a, HalfWing b ) { + 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(); + final Vector2d s = b.toVector2d(); + + // Ignore edges that are not colinear + if ( Math.abs( r.cross( s ) ) <= TOLERANCE ) { + + // Compare which destination is less + if ( compare( a.getDest(), b.getDest() ) <= 0 ) { + 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() ) ) { + // a.getDest().equals( 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 qp = q.subtracted( p ); + final double qpr = qp.cross( r ); + final double rs = r.cross( s ); + + // Check if parallel + if ( rs != 0 ) { + // Calculate t and u + final double t = qp.cross( s ) / rs; + final double u = qpr / rs; + + // Do the line segments intersect + // They should NOT intersect at their endpoints + if ( t > 0 && t < 1 && u > 0 && u < 1 ) { + // Calculate the point of intersection + final Vector2d intersection = r.multiply( t ).add( p ); + + return Optional.of( new IntersectionEdgeToEdge( intersection ) ); + } + } + } + return Optional.empty(); + } + + // Sort the edges around this vertex. + private List< HalfWing > sort( Vertex vertex ) { + List< HalfWing > edges = new ArrayList< HalfWing >(); + + final HalfWing vertWing = vertex.getWing(); + HalfWing edge = vertWing; + do { + if ( edge.isZero() ) { + // Invariant: Edges must have a length > 0 + throw new IllegalStateException( "Edge has length of 0" ); + } + edges.add( edge ); + } while ( ( edge = edge.getPrev() ) != vertWing ); + + // 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 lowest, and 12:00 is the highest + Collections.sort( edges, ( e1, e2 ) -> { + // This assumes that no edge has length of 0 + boolean e1p = isPositive( e1 ); + boolean e2p = isPositive( e2 ); + + if ( e1p ^ e2p ) { + return e1p ? 1 : -1; + } else { + Vector2d a1 = e1.getOrigin().getPosition(); + Vector2d a2 = e1.getDest().getPosition(); + Vector2d b1 = e2.getOrigin().getPosition(); + Vector2d b2 = e2.getDest().getPosition(); + + double cross = b2.subtracted( b1 ).normalized().cross( a2.subtracted( a1 ).normalized() ); + if ( Math.abs( cross ) <= TOLERANCE ) { + return 0; + } else { + return cross < 0 ? -1 : 1; + } + } + } ); + + for ( int i = 0; i < edges.size(); i++ ) { + // Get this edge and the next edge + HalfWing e1 = edges.get( i ); + HalfWing 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.setWing( edges.get( 0 ) ); + } + + return edges; + } + + /* + * 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 boolean greaterThanOrEqualTo( HalfWing a, HalfWing 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 + Vector2d a1 = a.getOrigin().getPosition(); + Vector2d a2 = a.getDest().getPosition(); + // b1 < b2 + Vector2d b1 = b.getOrigin().getPosition(); + Vector2d b2 = b.getDest().getPosition(); + + 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; + } + } + + private boolean isPositive( HalfWing wing ) { + return compare( wing.getOrigin(), wing.getDest() ) <= 0; + } + + private int compare( Vertex a, Vertex b ) { + return comparator.compare( a.getPosition(), b.getPosition() ); + } + + // Check if 2 vertices are 'close enough' + private boolean isSimilar( Vertex a, Vertex b ) { + return isSimilar( a.getPosition(), b.getPosition() ); + } + + private boolean isSimilar( Vector2d a, Vector2d b ) { + return Math.abs( a.distance( b ) ) <= VERTEX_TOLERANCE; + } + + < C extends Tess4jStage > C advance() { + tessStage = tessStage.advance(); + return tessStage.get(); + } + + public List< Polygon > getPolygons() { + if ( tessStage instanceof Tess4j.StageComplete ) { + StageComplete stage = tessStage.get(); + return stage.filledPolygons; + + } else if ( tessStage instanceof Tess4j.StageColinearConsolidation ) { + StageColinearConsolidation stage = tessStage.get(); + List< Polygon > polygons = new ArrayList< Polygon >(); + for ( List< HalfWing > wings : stage.newPolygons ) { + List< Point > points = new ArrayList< Point >(); + + HalfWing start = wings.get( 0 ); + HalfWing wing = start; + do { + Vector2d pos = wing.getOrigin().getPosition(); + points.add( new Point( pos.getX(), pos.getY() ) ); + } while ( ( wing = wing.next ) != start ); + + polygons.add( new Polygon( points ) ); + } + return polygons; + } + + return new ArrayList< Polygon >(); + } + + public void setDebug( boolean debug ) { + this.debug = debug; + } + + public static void main( String[] args ) { + 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; + } + + @Override + public double compareY( Vector2d a, Vector2d b ) { + return a.y - b.y; + } + } ); + 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 ), +// 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(); + + } + + /* + * Represents the stage that this tessellator is currently in. + * Used to help guarantee which class variables can be access during each stage + * and helps prevent calling certain methods at the wrong stage. + */ + private abstract class Tess4jStage { + @SuppressWarnings( "unchecked" ) + < C extends Tess4jStage > C get() { + try { + return ( C ) this; + } catch ( ClassCastException e ) { + throw new IllegalArgumentException( "Invalid stage class: " + getClass() ); + } + } + + abstract Tess4jStage advance(); + } + + /* + * Represents the stage while the user adds various polygons, before tessellation starts + */ + private class StagePreTess extends Tess4jStage { + Queue< Vertex > vertexQueue = new PriorityQueue< Vertex >( Tess4j.this::compare ); + EdgeCollection< HalfWing > edgeCollection = new EdgeCollection< HalfWing >( Tess4j.this::greaterThanOrEqualTo ); + /* + * Use a weak map for storing the winding rule per halfwing + * This also means we need to keep meticulous order of half wings when creating them, such as HalfWing.split + * Used while resolving intersections and calculating whether a region is interior/exterior + */ + Map< HalfWing, WindingRule< T > > windingMap = new WeakHashMap< HalfWing, WindingRule< T > >(); + /* + * A supplier for fetching a new region. The region must represent the space outside of all polygons + */ + Supplier< T > defaultRegionSupplier; + + StagePreTess( Supplier< T > defaultRegionSupplier ) { + this.defaultRegionSupplier = defaultRegionSupplier; + } + + @Override + Tess4jStage advance() { + return new StageRegionGeneration( this ); + } + } + + /* + * Represents the first stage of tessellation + * - Similar vertices are merged + * - Intersecting edges are split + * - Regions are partitioned into inner/outer + */ + private class StageRegionGeneration extends Tess4jStage { + private StagePreTess preStage; + + /* + * Contains the region for each HalfWing object + * Used while resolving intersections and calculating whether a region is interior/exterior + */ + Map< HalfWing, T > regionMap = new HashMap< HalfWing, T >(); + + /* + * Contains wings who's left face is interior + */ + Set< HalfWing > interior = new HashSet< HalfWing >(); + /* + * Contains wings who's right face is interior + */ + Set< HalfWing > inverseInterior = new HashSet< HalfWing >(); + + StageRegionGeneration( StagePreTess preTess ) { + this.preStage = preTess; + } + + @Override + Tess4jStage advance() { + return new StageColinearConsolidation( interior ); + } + + Queue< Vertex > getQueue() { + return preStage.vertexQueue; + } + + EdgeCollection< HalfWing > getCollection() { + return preStage.edgeCollection; + } + + Map< HalfWing, WindingRule< T > > getWindingMap() { + return preStage.windingMap; + } + + T getDefaultRegion() { + return preStage.defaultRegionSupplier.get(); + } + } + + /* + * Merge all adjacent colinear edges that are part of the same polygon + * Then provide a list of all the edges for each polygon + * Doesn't really have to be a list though, but it's convenient + */ + private class StageColinearConsolidation extends Tess4jStage { + Set< HalfWing > polygons = new HashSet< HalfWing >(); + Set< List< HalfWing > > newPolygons = new HashSet< List< HalfWing > >(); + + StageColinearConsolidation( Set< HalfWing > interior ) { + while ( !interior.isEmpty() ) { + final HalfWing start = interior.iterator().next(); + + polygons.add( start ); + + HalfWing wing = start; + do { + interior.remove( wing ); + } while ( ( wing = wing.getNext() ) != start ); + } + } + + @Override + Tess4jStage advance() { +// return new StageVertexReduction( newPolygons ); + return new StageMonotonePartition( newPolygons ); +// return new StageComplete( newPolygons ); + } + } + + /* + * Perform more aggressive vertex reduction by connecting edges and vertices on + * the same line, while also splitting into multiple polygons if required + * + * Ideally this would take in a set of monotone partitions, and for each + * one, divide it into multiple partitions(if applicable) which require + * a smaller amount of triangles to tessellate overall. + */ + private class StageVertexReduction extends Tess4jStage { + Set< List< HalfWing > > polygons; + + StageVertexReduction( Set< List< HalfWing > > polygons ) { + this.polygons = polygons; + } + + @Override + Tess4jStage advance() { +// return new StageMonotonePartition( polygons ); + return new StageComplete( polygons ); + } + + } + + private class StageMonotonePartition extends Tess4jStage { + TreeSet< Vertex > vertices = new TreeSet< Vertex >( Tess4j.this::compare ); + Set< HalfWing > interior = new HashSet< HalfWing >(); + + StageMonotonePartition( Set< List< HalfWing > > polygons ) { + Set< Vertex > vertices = new HashSet< Vertex >(); + if ( debug ) { + System.out.println( "Amount of polygons: " + polygons.size() ); + } + for ( List< HalfWing > list : polygons ) { + if ( debug ) { + System.out.println( "Amount of edges in the polygon: " + list.size() ); + } + for ( HalfWing w : list ) { + if ( isPositive( w ) ) { + interior.add( w ); + } + + vertices.add( w.getOrigin() ); + } + } + this.vertices.addAll( vertices ); + } + + @Override + Tess4jStage advance() { + return new StageTriangulation( interior ); +// return new StageComplete( interior, true ); + } + } + + private class StageTriangulation extends Tess4jStage { + Set< HalfWing > wings = new HashSet< HalfWing >(); + Set< HalfWing > interior = new HashSet< HalfWing >(); + + StageTriangulation( Set< HalfWing > wings ) { + // Get the minimum interior wing of each polygon + while ( !wings.isEmpty() ) { + final HalfWing start = wings.iterator().next(); + + this.wings.add( start ); + + Set< Vertex > checkSet = new HashSet< Vertex >(); + + HalfWing wing = start; + do { + wings.remove( wing ); + + if ( !checkSet.add( wing.getOrigin() ) ) { + throw new IllegalStateException( "Contains the same point more than once" ); + } + } while ( ( wing = wing.getNext() ) != start ); + } + } + + @Override + Tess4jStage advance() { + return new StageComplete( interior, false ); + } + } + + // TODO Provide each polygon with a region + private class StageComplete extends Tess4jStage { + List< Polygon > filledPolygons = new ArrayList< Polygon >(); + + StageComplete( Set< List< HalfWing > > wings ) { + for ( List< HalfWing > poly : wings ) { + List< Point > points = new ArrayList< Point >(); + + for ( HalfWing wing : poly ) { + Vector2d v = wing.getOrigin().getPosition(); + points.add( new Point( v.x, v.y ) ); + } + + filledPolygons.add( new Polygon( points ) ); + } + } + + StageComplete( Set< HalfWing > wings, boolean other ) { + // Convert the edges to polygons + while ( !wings.isEmpty() ) { + final HalfWing start = wings.iterator().next(); + + List< Point > points = new ArrayList< Point >(); + Set< Point > checkSet = new HashSet< Point >(); + HalfWing wing = start; + do { + Vector2d vec = wing.getOrigin().getPosition(); + Point point = new Point( vec.getX(), vec.getY() ); + points.add( point ); + if ( !checkSet.add( point ) ) { + throw new IllegalStateException( "Polygon is not simple" ); + } + wings.remove( wing ); + } while ( ( wing = wing.getNext() ) != start ); + filledPolygons.add( new Polygon( points ) ); + } + } + + @Override + Tess4jStage advance() { + throw new IllegalStateException( "Already at the last stage" ); + } + } + + /* + * 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 HalfWing shorter; + private final HalfWing longer; + + IntersectionColinear( HalfWing shorter, HalfWing longer ) { + this.shorter = shorter; + this.longer = longer; + } + + public HalfWing getShorterEdge() { + return shorter; + } + + public HalfWing getLongerEdge() { + return longer; + } + } +} + diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Tess4jTest.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Tess4jTest.java new file mode 100644 index 0000000..7c0bb9a --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Tess4jTest.java @@ -0,0 +1,263 @@ +package com.aaaaahhhhhhh.bananapuncher714.tess4j; + +import java.awt.Color; +import java.awt.Graphics; +import java.util.Arrays; +import java.util.List; + +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.SwingUtilities; + +import com.aaaaahhhhhhh.bananapuncher714.tess4j.region.RegionSimple; +import com.aaaaahhhhhhh.bananapuncher714.tess4j.region.RegionSimple.GluWindingRule; +import com.aaaaahhhhhhh.bananapuncher714.tess4j.region.WindingRuleSimple; + +public class Tess4jTest extends JPanel { + JFrame f; + + private int windowWidth = 1200; + private int windowHeight = 1000; + + private int centerX = 600; + private int centerY = 400; + + private Graphics g; + + private int scale = 100; + + public static void main( String[] args ) { + SwingUtilities.invokeLater( new Runnable() { + @Override + public void run() { + new Tess4jTest(); + } + } ); + } + + public Tess4jTest() { + 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; + +// Vertex v = null; +// for ( int i = 0; i < 100; i++ ) { +// double deg = Math.random() * 360; +// double dist = Math.random() * 100 + 100; +// double x = dist * Math.cos( Math.toRadians( deg ) ); +// double y = dist * Math.sin( Math.toRadians( deg ) ); +// HalfWing edge = new HalfWing(); +// edge.getDest().setPosition( new Vector2d( x, y ) ); +// if ( v == null ) { +// v = edge.getOrigin(); +// } else { +// HalfWing.splice( edge, v.getWing() ); +// } +// } +// v.update(); +// +// HalfWing edge = v.getWing(); +// int i = 0; +// double ic = 0; +// do { +// Vector2d dest = edge.getDest().getPosition(); +// g.drawLine( centerX, centerY, ( int ) dest.getX() + centerX, ( int ) dest.getY() + centerY ); +// dest = dest.normalized(); +// g.drawString( i++ + "", ( int ) ( dest.x * 200 * ( ic + 1.1 ) ) + centerX, ( int ) ( dest.y * 200 * ( ic + 1.1 ) ) + centerY ); +// ic = ( ic + 0.07 ) % .7; +// edge = edge.getPrev(); +// } while ( edge != v.getWing() ); +// +// v.sort(); +// +// g.drawLine( centerX + 700, centerY, centerX + 700, centerY - 400 ); +// edge = v.getWing(); +// i = 0; +// ic = 0; +// do { +// Vector2d dest = edge.getDest().getPosition(); +// g.drawLine( centerX + 700, centerY, ( int ) dest.getX() + centerX + 700, ( int ) dest.y + centerY ); +// dest = dest.normalized(); +// g.drawString( i++ + "", ( int ) ( dest.x * 200 * ( ic + 1.1 ) ) + centerX + 700, ( int ) ( dest.y * 200 * ( ic + 1.1 ) ) + centerY ); +// ic = ( ic + 0.07 ) % .7; +// edge = edge.getPrev(); +// } while ( edge != v.getWing() ); + +// Tess4j< RegionSimple > tess4j = new Tess4j< RegionSimple >( () -> { return new RegionSimple( GluWindingRule.ODD ); } ); + Tess4j< RegionSimple > tess4j = new Tess4j< RegionSimple >( () -> { return new RegionSimple( GluWindingRule.ODD ); }, new Vector2dComparator() { + @Override + public double compareX( Vector2d a, Vector2d b ) { + a = rotate( a, 128 ); + b = rotate( b, 128 ); + return a.x - b.x; + } + + @Override + public double compareY(Vector2d a, Vector2d b ) { + a = rotate( a, 128 ); + b = rotate( b, 128 ); + return b.y - a.y; + } + } ); +// 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( -5, 0 ), +// new Point( -2, 0 ), +// new Point( -2, 1 ), +// new Point( -5, 1 ) +// ) ), new WindingRuleSimple() ); +// tess4j.addPolygon( new Polygon( Arrays.asList( +// new Point( -1.75, .5 ), +// new Point( 4.25, .5 ), +// new Point( -0.25, -2.5 ), +// new Point( 1.25, 2 ), +// new Point( 2.75, -2.5 ) +// ) ), 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( -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() ); + for ( int i = 0; i < 4; i++ ) { + for ( int j = 0; j < 4; j++ ) { + tess4j.addPolygon( new Polygon( Arrays.asList( + new Point( i, j ), + new Point( i, j + 1 ), + new Point( i + 1, j + 1 ), + new Point( i + 1, j ) + ) ), new WindingRuleSimple() ); +// tess4j.addPolygon( new Polygon( Arrays.asList( +// new Point( i, j ), +// new Point( i, j + 1 ), +// new Point( i + 1, j ), +// new Point( i + 1, j + 1 ) +// ) ), new WindingRuleSimple() ); + } + } + + tess4j.tessellate(); + + List< Polygon > polygons = tess4j.getPolygons(); + + 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(); + 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 ); + + System.out.println( point.x + ", " + point.y ); + Vector2d pVec = new Vector2d( point.x, point.y ); + 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; + } + + g.setColor( new Color( ( int ) ( Math.random() * 0xFFFFFF ) ) ); + g.fillPolygon( xPoints, yPoints, size ); + } + + g.setColor( Color.BLACK ); + for ( Polygon p : polygons ) { + for ( Point point : p.points ) { + Vector2d pVec = new Vector2d( point.x, point.y ); + 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.drawLine( centerX, 0, centerX, 2000 ); + g.drawLine( 0, centerY + 100, 2000, centerY + 100 ); + +// 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/tess4j/Tessellator.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Tessellator.java new file mode 100644 index 0000000..5edfc7a --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Tessellator.java @@ -0,0 +1,506 @@ +package com.aaaaahhhhhhh.bananapuncher714.tess4j; + +import java.util.ArrayList; +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; +import java.util.WeakHashMap; + +import org.joml.Math; + +public class Tessellator { + // TODO Insecure + private static VertexOld vertex; + private static EdgeDict dict; + private static Queue< VertexOld > eventQueue; + + public static void main( String[] args ) { + List< Polygon > polygons = new ArrayList< Polygon >(); + polygons.add( new Polygon( Arrays.asList( + new Point( -1, -1 ), + new Point( 1, -1 ), + new Point( 1, 1 ), + new Point( -1, 1 ) + ) ) ); + tessellate( polygons ); + } + + public static void tessellate( Collection< Polygon > shapes ) { + eventQueue = new PriorityQueue< VertexOld >(); + dict = new EdgeDict( () -> { return vertex; } ); + + double minX = Double.MAX_VALUE; + double minY = Double.MAX_VALUE; + double maxX = Double.MIN_VALUE; + double maxY = Double.MIN_VALUE; + + // Get all unique vertices that are in use + Set< VertexOld > vertexCollection = Collections.newSetFromMap( new WeakHashMap< VertexOld, Boolean >() ); + for ( Polygon polygon : shapes ) { + HalfEdge edge = null; + for ( Point point : polygon.points ) { + if ( edge == null ) { + // Make a self loop with one edge and one vertex + edge = HalfEdge.makeEdge(); + HalfEdge.splice( edge, edge.sym() ); + } else { + HalfEdge.splitEdge( edge ); + edge = edge.nextLeft; + } + + edge.origin.x = point.x; + edge.origin.y = point.y; + + // 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 ); + + vertexCollection.add( edge.origin ); + vertexCollection.add( edge.sym().origin ); + } + } + eventQueue.addAll( vertexCollection ); + + // Add sentinel regions so that all polygons are between two edges/regions + addSentinel( minX, minY, maxX, maxY ); + + // Sweep over each vertex, from left to right, bottom to top + VertexOld current; + while ( !eventQueue.isEmpty() ) { + current = eventQueue.poll(); + while ( true ) { + VertexOld next = eventQueue.peek(); + // Get all vertices that overlap and merge them together + if ( next == null || !current.equals( next ) ) { + break; + } + next = eventQueue.poll(); + HalfEdge.splice( current.edge, next.edge ); + } + System.out.println( "SCANNING " + current ); + sweepEvent( current ); + } + } + + public static void addSentinel( double minX, double minY, double maxX, double maxY ) { + double width = ( maxX - minX ) + 0.01; + double height = ( maxY - minY ) + 0.01; + + double lowerX = minX - width; + double lowerY = minY - height; + double upperX = maxX + width; + double upperY = maxY + height; + + addSentinelRegion( lowerX, upperX, lowerY ); + addSentinelRegion( lowerX, upperX, upperY ); + } + + private static void addSentinelRegion( double startX, double endX, double y ) { + HalfEdge edge = HalfEdge.makeEdge(); + + edge.origin.x = endX; + edge.origin.y = y; + edge.sym().origin.x = startX; + edge.sym().origin.y = y; + + vertex = edge.sym().origin; + + RegionOld region = new RegionOld(); + region.upperEdge = edge; + region.inside = false; + region.sentinel = true; + dict.insert( region ); + } + + public static void sweepEvent( VertexOld vert ) { + vertex = vert; + + // Find the region that this vertex belongs to + HalfEdge edge = vert.edge; + while ( edge.region == null ) { + edge = edge.nextOrigin; + if ( edge == vert.edge ) { + // This is a new vertex which is not associated with any region + // Connect it to something + connectLeftVertex( vert ); + return; + } + } + } + + public static void connectLeftVertex( VertexOld vert ) { + RegionOld tmpRegion = new RegionOld(); + tmpRegion.upperEdge = vert.edge.sym(); + + // Find the regions immediately above and below this vertex + RegionOld up = dict.search( tmpRegion ); + RegionOld lower = dict.below( up ); + if ( lower == null ) { + // Coplanar polygon + return; + } + // Get the edges associated with both regions + HalfEdge upperEdge = up.upperEdge; + HalfEdge lowerEdge = lower.upperEdge; + + // If the vertex lies on the upper edge... ??? + if ( vert.compareTo( upperEdge.sym().origin, upperEdge.origin ) == 0 ) { + connectLeftDegenerate( up, vert ); + return; + } + + // Get the higher region + RegionOld region = lowerEdge.sym().origin.lessThanOrEqualTo( upperEdge.sym().origin ) ? up : lower; + if ( region.inside || region.fixUpperEdge ) { + HalfEdge edge; + if ( region == up ) { + edge = HalfEdge.connect( vert.edge.sym(), upperEdge.nextLeft ); + } else { + edge = HalfEdge.connect( lowerEdge.nextD(), vert.edge ).sym(); + } + + if ( region.fixUpperEdge ) { + region.fixUpperEdge( edge ); + } else { + computeWinding( addRegionBelow( up, edge ) ); + } + } else { + addRightEdges( up, vertex.edge, vertex.edge, null, true ); + } + } + + public static void addRightEdges( RegionOld region, HalfEdge first, HalfEdge last, HalfEdge topLeft, boolean clean ) { + // Add all edges from first to last + // The last edge to be inserted is the edge CCW of last + + System.out.println( "Region is " + region.upperEdge ); + HalfEdge edge = first; + do { + // Create regions for all edges starting from first, before last, in CCW fashion + System.out.println( "Inserting region for edge " + edge ); + // The region defining edge is always going to be right to left + addRegionBelow( region, edge.sym() ); + edge = edge.nextOrigin; + } while ( edge != last ); + + // Get the most CCW edge that is to the right of the sweep line + if ( topLeft == null ) { + topLeft = dict.below( region ).upperEdge.prevR(); + System.out.println( "Top left region is " + topLeft ); + } + RegionOld previousRegion = region; + HalfEdge previousEdge = topLeft; + + // Update winding and fix any problems with ordering + boolean firstTime = true; + while ( true ) { + // Iterate over each region from top to bottom + RegionOld tempRegion = dict.below( previousRegion ); + // Get the left to right, bottom to top edge + HalfEdge tempEdge = tempRegion.upperEdge.sym(); + System.out.println( "Looping for edge " + tempEdge ); + + // Break if the vertex changes + if ( tempEdge.origin != previousEdge.origin ) { + break; + } + + // Check if the previous edge is equal to the previous edge + // Otherwise there's a discrepancy and it needs to be relinked accordingly + if ( tempEdge.nextOrigin != previousEdge ) { + // Unlink the edge from its current position, and relink below the previous edge + HalfEdge.splice( tempEdge.nextOrigin, tempEdge ); + HalfEdge.splice( previousEdge.prevOrigin(), tempEdge ); + } + + // Calculate the winding number and if it is inside + tempRegion.windingNumber = previousRegion.windingNumber - tempEdge.winding; + tempRegion.inside = tempRegion.windingNumber % 2 == 1; + + previousRegion.dirty = true; + + if ( !firstTime && checkForRightSplice( previousRegion ) ) { + tempEdge.winding += previousEdge.winding; + + HalfEdge.delete( previousEdge ); + } + + firstTime = false; + previousRegion = tempRegion; + previousEdge = tempEdge; + } + + if ( clean ) { + walkDirtyRegions( previousRegion ); + } + } + + public static boolean checkForLeftSplice( RegionOld region ) { + // Check the upper and lower edge of the region for any inconsistencies + // Simiar to checkForRightSplice, but the left vertices + RegionOld below = dict.below( region ); + HalfEdge upEdge = region.upperEdge; + HalfEdge downEdge = below.upperEdge; + + if ( upEdge.sym().origin.lessThanOrEqualTo( downEdge.sym().origin ) ) { + if ( downEdge.sym().origin.compareTo( upEdge.sym().origin, upEdge.origin ) < 0 ) { + return false; + } + + // The lower edge is above the upper edge, so splice + dict.above( region ).dirty = true; + region.dirty = true; + + HalfEdge edge = HalfEdge.splitEdge( upEdge ); + HalfEdge.splice( downEdge.sym(), edge ); + edge.face.inside = region.inside; + } else { + if ( upEdge.sym().origin.compareTo( downEdge.sym().origin, downEdge.origin ) > 0 ) { + return false; + } + + region.dirty = true; + below.dirty = true; + HalfEdge edge = HalfEdge.splitEdge( downEdge ); + HalfEdge.splice( upEdge.nextLeft, downEdge.sym() ); + edge.sym().face.inside = region.inside; + } + return true; + } + + public static boolean checkForRightSplice( RegionOld region ) { + RegionOld below = dict.below( region ); + HalfEdge upEdge = region.upperEdge; + HalfEdge downEdge = below.upperEdge; + + // Check if the right vertex of the upper edge is to the left or below the lower edge's right vertex + if ( upEdge.origin.lessThanOrEqualTo( downEdge.origin ) ) { + // Check if the right vertex is above the lower edge + if ( upEdge.origin.compareTo( downEdge.sym().origin, downEdge.origin ) > 0 ) { + // If so, no splicing is necessary + return false; + } + + // If the right vertex of the upper edge is not equal to the right vertex of the lower edge + if ( !upEdge.origin.equals( downEdge.origin ) ) { + // Split the lower edge in two, with the new edge on the right + HalfEdge.splitEdge( downEdge.sym() ); + // Move the right vertex of the lower edge to the + HalfEdge.splice( upEdge, downEdge.prevOrigin() ); + + region.dirty = true; + below.dirty = true; + } else if ( upEdge.origin != downEdge.origin ) { + // Merge the two vertices if they are not already the same + eventQueue.remove( upEdge.origin ); + HalfEdge.splice( downEdge.prevOrigin(), upEdge ); + } + } else { + // Check if the lower edge's right vertex is on or below the upper edge + if ( downEdge.origin.compareTo( upEdge.sym().origin, upEdge.origin ) <= 0 ) { + return false; + } + + dict.above( region ).dirty = true; + region.dirty = true; + + // Split the upper edge + HalfEdge.splitEdge( upEdge.sym() ); + // Move the upper edge to the lower edge's vertex + HalfEdge.splice( downEdge.prevOrigin(), upEdge ); + + } + return true; + } + + public static void walkDirtyRegions( RegionOld region ) { + RegionOld lower = dict.below( region ); + while ( true ) { + // Find the lowest dirty region + while ( lower.dirty ) { + region = lower; + lower = dict.below( lower ); + } + + if ( !region.dirty ) { + lower = region; + region = dict.above( region ); + if ( region == null || !region.dirty ) { + // No dirty regions left + return; + } + } + + region.dirty = false; + HalfEdge upperEdge = region.upperEdge; + HalfEdge lowerEdge = lower.upperEdge; + + // If the start of the region edge is not the same as the start of the lower region edge + if ( upperEdge.sym().origin != lowerEdge.sym().origin ) { + // Consistency check + if ( checkForLeftSplice( region ) ) { + if ( lower.fixUpperEdge ) { + dict.remove( lower ); + HalfEdge.delete( lowerEdge ); + lower = dict.below( region ); + lowerEdge = lower.upperEdge; + } else if ( region.fixUpperEdge ) { + dict.remove( region ); + HalfEdge.delete( upperEdge ); + region = dict.above( lower ); + upperEdge = region.upperEdge; + } + } + } + + // If the end of the region edge is not the same as the end of the lower region edge + if ( upperEdge.origin != lowerEdge.origin ) { + if ( upperEdge.sym().origin != lowerEdge.sym().origin + && !region.fixUpperEdge + && !lower.fixUpperEdge + && ( upperEdge.sym().origin == vertex || lowerEdge.sym().origin == vertex ) ) { + if ( checkForIntersect( region ) ) { + return; + } + } else { + checkForRightSplice( region ); + } + } + + // Matching edges + if ( upperEdge.origin == lowerEdge.origin && upperEdge.sym().origin == lowerEdge.sym().origin ) { + // Degenerate loop + lowerEdge.winding += upperEdge.winding; + dict.remove( region ); + HalfEdge.delete( upperEdge ); + region = dict.above( lower ); + } + } + } + + public static boolean checkForIntersect( RegionOld upper ) { + // Check the upper and lower edges of this region to see if they intersect + + RegionOld lower = dict.below( upper ); + HalfEdge upperEdge = upper.upperEdge; + HalfEdge lowerEdge = lower.upperEdge; + VertexOld upLeft = upperEdge.sym().origin; + VertexOld upRight = upperEdge.origin; + VertexOld downLeft = lowerEdge.sym().origin; + VertexOld downRight = lowerEdge.origin; + + // Endpoints are the same, so no intersection + if ( upRight == downRight ) { + return false; + } + + double upY = Math.min( upRight.y, upLeft.y ); + double downY = Math.max( downRight.y, downLeft.y ); + // The edges do not overlap vertically + if ( upY > downY ) { + return false; + } + + // Check for any vertical intersection of the right point + if ( upRight.lessThanOrEqualTo( downRight ) ) { + if ( upRight.compareTo( downLeft, downRight ) > 0 ) { + return false; + } + } else { + if ( downRight.compareTo( upLeft, upRight ) < 0 ) { + return false; + } + } + + // TODO Compute intersection + + return false; + } + + public static Point intersection( VertexOld x1, VertexOld x2, VertexOld y1, VertexOld y2 ) { + Point point = new Point( 0, 0 ); + if ( !x1.lessThanOrEqualTo( x2 ) ) { + VertexOld temp = x1; + x1 = x2; + x2 = temp; + } + if ( !y1.lessThanOrEqualTo( y2 ) ) { + VertexOld temp = y1; + y1 = y2; + y2 = temp; + } + if ( !x1.lessThanOrEqualTo( y1 ) ) { + VertexOld temp = x1; + x1 = y1; + y1 = temp; + VertexOld temp2 = x2; + x2 = y2; + y2 = temp2; + } + + // Compare points + if ( !y1.lessThanOrEqualTo( x2 ) ) { + point.x = ( y1.x + x2.x ) / 2; + } else if ( x2.lessThanOrEqualTo( y2 ) ) { + double z1 = y1.verticalDistance( x1, x2 ); + double z2 = x2.verticalDistance( y1, y2 ); + if ( z1 + z1 < 0 ) { + z1 *= -1; + z2 *= -1; + } + point.x = interpolate( z1, y1.x, z2, x2.x ); + } else { + double z1 = y1.compareTo( x1, x2 ); + double z2 = - y2.compareTo( x1, x2 ); + if ( z1 + z2 < 0 ) { + z1 *= -1; + z2 *= -1; + } + point.x = interpolate( z1, y1.x, z2, y2.x ); + } + return point; + } + + public static double interpolate( double a, double x, double b, double y ) { + a = Math.max( a, 0 ); + b = Math.max( b, 0 ); + if ( a > b ) { + return y - ( ( x - y ) * ( b / ( a + b ) ) ); + } + if ( b != 0 ) { + return x + ( ( y - x ) * ( a / ( a + b ) ) ); + } + return ( x + y ) / 2; + } + + public static void computeWinding( RegionOld region ) { + region.windingNumber = dict.above( region ).windingNumber + region.upperEdge.winding; + // TODO Using odd for now + region.inside = region.windingNumber % 2 == 1; + } + + public static void connectLeftDegenerate( RegionOld region, VertexOld vert ) { + throw new IllegalArgumentException( "connectLeftDegenerate is unimplemented!" ); + } + + public static RegionOld addRegionBelow( RegionOld above, HalfEdge edge ) { + RegionOld region = new RegionOld(); + region.upperEdge = edge; + dict.insertBefore( above, region ); + region.fixUpperEdge = false; + region.sentinel = false; + + edge.region = region; + return region; + } +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Util.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Util.java new file mode 100644 index 0000000..195e80e --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Util.java @@ -0,0 +1,194 @@ +package com.aaaaahhhhhhh.bananapuncher714.tess4j; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Optional; + +import org.joml.Math; + +public class Util { + protected static String toString( Object obj ) { + StringBuilder builder = new StringBuilder(); + + builder.append( obj.getClass().getSimpleName() ); + builder.append( '{' ); + List< Field > fields = new ArrayList< Field >(); + fields.addAll( Arrays.asList( obj.getClass().getDeclaredFields() ) ); + fields.addAll( Arrays.asList( obj.getClass().getFields() ) ); + + for ( Iterator< Field > it = fields.iterator(); it.hasNext(); builder.append( "," ) ) { + Field field = it.next(); + builder.append( field.getName() ); + builder.append( '=' ); + try { + builder.append( field.get( obj ) ); + } catch ( IllegalArgumentException | IllegalAccessException e ) { + builder.append( "?" ); + } + } + + builder.append( '}' ); + + return builder.toString(); + } + + public static Optional< Point > intersection( final VertexOld x1, final VertexOld x2, final VertexOld x3, final VertexOld x4 ) { + Point point = null; + + final Vector2dOld p = x1.toVector(); + final Vector2dOld r = x2.subtract( x1 ); + final Vector2dOld q = x3.toVector(); + final Vector2dOld s = x4.subtract( x3 ); + + final Vector2dOld qp = q.subtract( p ); + final double qpr = qp.cross( r ); + final double rs = r.cross( s ); + + // Are the two lines parallel + if ( rs == 0 ) { + final Vector2dOld u = x2.toVector(); + final Vector2dOld v = x4.toVector(); + + // For normalizing against pr + final double rr = r.dot( r ); + + // t0 and t1 represent the distance of the second edge endpoints relative to the first edge normalized by r + // t0 = ( ( q - p ) . r ) / rr; + double t0 = qp.dot( r ) / rr; + // t1 = ( ( q + s - p ) . r ) / rr + double t1 = v.subtract( p ).dot( r ) / rr; + + // If the vectors are pointing in the opposite directions, then swap t0 and t1 + final boolean inverted = s.dot( r ) < 0; + if ( inverted ) { + double temp = t0; + t0 = t1; + t1 = temp; + } + + if ( qpr == 0 ) { + if ( t0 < 1 & t1 > 0 ) { + final double midt = Math.max( t0, 0 ) + Math.min( t1, 1 ) / 2.0; + final Vector2dOld midpoint = r.multiply( midt ).add( p ); + point = midpoint.toPoint(); + } else { + final Vector2dOld pu = t0 > 1 ? u : p; + // 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 = midpoint.toPoint(); + } + } + } else { + // Calculate t and u + final double t = qp.cross( s ) / rs; + final double u = qpr / rs; + + // Do the line segments intersect + if ( t >= 0 && t <= 1 && u >= 0 && u <= 1 ) { + // Calculate the point of intersection + final Vector2dOld intersection = r.multiply( t ).add( p ); + point = intersection.toPoint(); + } + } + return Optional.ofNullable( point ); + } + + // https://stackoverflow.com/questions/563198/how-do-you-detect-where-two-line-segments-intersect + // Vector based closest intersection method + public static Point closestPoint( final VertexOld x1, final VertexOld x2, final VertexOld x3, final VertexOld x4 ) { + Point point = new Point( 0, 0 ); + + final Vector2dOld p = x1.toVector(); + final Vector2dOld r = x2.subtract( x1 ); + final Vector2dOld q = x3.toVector(); + final Vector2dOld s = x4.subtract( x3 ); + + final Vector2dOld qp = q.subtract( p ); + final double qpr = qp.cross( r ); + final double rs = r.cross( s ); + + // Are the two lines parallel + if ( rs == 0 ) { + final Vector2dOld u = x2.toVector(); + final Vector2dOld v = x4.toVector(); + + // For normalizing against pr + final double rr = r.dot( r ); + + // t0 and t1 represent the distance of the second edge endpoints relative to the first edge normalized by r + // t0 = ( ( q - p ) . r ) / rr; + double t0 = qp.dot( r ) / rr; + // t1 = ( ( q + s - p ) . r ) / rr + double t1 = v.subtract( p ).dot( r ) / rr; + + // If the vectors are pointing in the opposite directions, then swap t0 and t1 + final boolean inverted = s.dot( r ) < 0; + if ( inverted ) { + double temp = t0; + t0 = t1; + t1 = temp; + } + + // Do the lines overlap + if ( t0 < 1 && t1 > 0 ) { + // The midpoint is an arbitrary point that just happens to look good as an intersection + // Find the midpoint between the overlapping region + final double midt = Math.max( t0, 0 ) + Math.min( t1, 1 ) / 2.0; + // If colinear + 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; + } else { + // Calculate the midpoint along pr + final Vector2dOld midp = r.multiply( midt ).add( p ); + // Calculate the perpendicular distance between both lines + final Vector2dOld perp = r.perpendicular(); + 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; + } + } else { + // The lines aren't really anywhere close to each other, so just find the midpoint between the 2 closest points + // If t0 > 1, then the second edge must lie to the right of the first edge + final Vector2dOld pu = t0 > 1 ? u : p; + // 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; + } + } else { + // Calculate t and u + final double t = qp.cross( s ) / rs; + final double u = qpr / rs; + + // Do the line segments intersect + 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; + } 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 + final double nearT = Math.max( Math.min( t, 1 ), 0 ); + final double nearU = Math.max( Math.min( u, 1 ), 0 ); + + 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; + } + } + return point; + } +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Vector2d.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Vector2d.java new file mode 100644 index 0000000..32bab8b --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Vector2d.java @@ -0,0 +1,206 @@ +package com.aaaaahhhhhhh.bananapuncher714.tess4j; + +public class Vector2d { + double x; + double y; + + public Vector2d() { + this( 0, 0 ); + } + + public Vector2d( double x, double y ) { + this.x = x; + this.y = y; + } + + public Vector2d( Vector2d 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 cross( Vector2d o ) { + return ( x * o.y ) - ( y * o.x ); + } + + public double lengthSquared() { + return dot( this ); + } + + public double length() { + return Math.sqrt( lengthSquared() ); + } + + public double distanceSquared( Vector2d o ) { + return distanceSquared( this, o ); + } + + public double distance( Vector2d o ) { + return distance( this, o ); + } + + public double dot( Vector2d o ) { + return dot( this, o ); + } + + public Vector2d add( double v ) { + x += v; + y += v; + return this; + } + + public Vector2d added( double v ) { + return new Vector2d( x + v, y + v ); + } + + public Vector2d add( Vector2d o ) { + x += o.x; + y += o.y; + return this; + } + + public Vector2d added( Vector2d o ) { + return add( this, o ); + } + + public Vector2d subtract( double v ) { + x -= v; + y -= v; + return this; + } + + public Vector2d subtracted( double v ) { + return new Vector2d( x - v, y - v ); + } + + public Vector2d subtract( Vector2d o ) { + x -= o.x; + y -= o.y; + return this; + } + + public Vector2d subtracted( Vector2d o ) { + return new Vector2d( x - o.x, y - o.y ); + } + + public Vector2d multiply( double v ) { + x *= v; + y *= v; + return this; + } + + public Vector2d multiplied( double v ) { + return new Vector2d( x * v, y * v ); + } + + public Vector2d multiply( Vector2d o ) { + x *= o.x; + y *= o.y; + return this; + } + + public Vector2d multiplied( Vector2d o ) { + return multiply( this, o ); + } + + public Vector2d divide( double v ) { + x /= v; + y /= v; + return this; + } + + public Vector2d divided( double v ) { + return new Vector2d( x / v, y / v ); + } + + public Vector2d divide( Vector2d o ) { + x /= o.x; + y /= o.y; + return this; + } + + public Vector2d divided( Vector2d o ) { + return new Vector2d( x / o.x, y / o.y ); + } + + public Vector2d normalize() { + return divide( length() ); + } + + public Vector2d normalized() { + return divided( length() ); + } + + public boolean isZero() { + return x == 0 && y == 0; + } + + public static double dot( Vector2d a, Vector2d b ) { + return a.x * b.x + a.y * b.y; + } + + public static Vector2d add( Vector2d a, Vector2d b ) { + return new Vector2d( a.x + b.x, a.y + b.y ); + } + + public static Vector2d multiply( Vector2d a, Vector2d b ) { + return new Vector2d( a.x * b.x, a.y * b.y ); + } + + public static double distanceSquared( Vector2d a, Vector2d b ) { + return a.subtracted( b ).lengthSquared(); + } + + public static double distance( Vector2d a, Vector2d b ) { + return Math.sqrt( distanceSquared( a, b ) ); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + long temp; + temp = Double.doubleToLongBits(x); + result = prime * result + (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(y); + result = prime * result + (int) (temp ^ (temp >>> 32)); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Vector2d other = (Vector2d) obj; + if (Double.doubleToLongBits(x) != Double.doubleToLongBits(other.x)) + return false; + if (Double.doubleToLongBits(y) != Double.doubleToLongBits(other.y)) + return false; + return true; + } + + @Override + public String toString() { + return "Vector2d{x=" + x + ",y=" + y + "}"; + } +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Vector2dComparator.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Vector2dComparator.java new file mode 100644 index 0000000..b278563 --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Vector2dComparator.java @@ -0,0 +1,17 @@ +package com.aaaaahhhhhhh.bananapuncher714.tess4j; + +import java.util.Comparator; + +public abstract class Vector2dComparator implements Comparator< Vector2d > { + public abstract double compareX( Vector2d a, Vector2d b ); + public abstract double compareY( Vector2d a, Vector2d b ); + + // Sorts by x, then y + public final int compare( Vector2d a, Vector2d b ) { + double x = compareX( a, b ); + if ( x == 0 ) { + return Double.compare( compareY( a, b ), 0 ); + } + return Double.compare( x, 0 ); + } +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Vector2dOld.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Vector2dOld.java new file mode 100644 index 0000000..ee78593 --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Vector2dOld.java @@ -0,0 +1,84 @@ +package com.aaaaahhhhhhh.bananapuncher714.tess4j; + +public class Vector2dOld { + final double x; + final double y; + + Vector2dOld( double x, double y ) { + this.x = x; + this.y = y; + } + + public double cross( Vector2dOld other ) { + return ( x * other.y ) - ( y * other.x ); + } + + public double dot( Vector2dOld other ) { + return x * other.x + y * other.y; + } + + public Vector2dOld subtract( Vector2dOld other ) { + return new Vector2dOld( x - other.x , y - other.y ); + } + + public Vector2dOld add( Vector2dOld other ) { + return new Vector2dOld( x + other.x, y + other.y ); + } + + public Vector2dOld perpendicular() { + return new Vector2dOld( -y, x ); + } + + public Vector2dOld multiply( double v ) { + return new Vector2dOld( x * v, y * v ); + } + + public Vector2dOld divide( double v ) { + return new Vector2dOld( x / v, y / v ); + } + + public Vector2dOld normalize() { + return divide( Math.sqrt( dot( this ) ) ); + } + + public Point toPoint() { + return new Point( x, y ); + } + + public double length() { + return Math.sqrt( dot( this ) ); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + long temp; + temp = Double.doubleToLongBits(x); + result = prime * result + (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(y); + result = prime * result + (int) (temp ^ (temp >>> 32)); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Vector2dOld other = (Vector2dOld) obj; + if (Double.doubleToLongBits(x) != Double.doubleToLongBits(other.x)) + return false; + if (Double.doubleToLongBits(y) != Double.doubleToLongBits(other.y)) + return false; + return true; + } + + @Override + public String toString() { + return "Vector2d{x=" + x + ",y=" + y + "}"; + } +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Vertex.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Vertex.java new file mode 100644 index 0000000..b9c4afa --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/Vertex.java @@ -0,0 +1,66 @@ +package com.aaaaahhhhhhh.bananapuncher714.tess4j; + +public class Vertex { + private HalfWing wing; + private Vector2d position; + + public Vertex( HalfWing wing ) { + setWing( wing ); + setPosition( new Vector2d() ); + } + + public HalfWing getWing() { + return wing; + } + + public Vertex setWing( HalfWing wing ) { + this.wing = wing; + return this; + } + + public Vector2d getPosition() { + return position; + } + + public Vertex setPosition( Vector2d vec ) { + this.position = vec; + return this; + } + + public void update() { + HalfWing wing = this.wing; + do { + wing.setOrigin( this ); + } while ( ( wing = wing.getPrev() ) != this.wing ); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((position == null) ? 0 : position.hashCode()); + return result; + } + + @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/tess4j/VertexOld.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/VertexOld.java new file mode 100644 index 0000000..af33a19 --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/VertexOld.java @@ -0,0 +1,104 @@ +package com.aaaaahhhhhhh.bananapuncher714.tess4j; + +public class VertexOld implements Comparable< VertexOld > { + double x; + double y; + + // An edge that is attached to this vertex + HalfEdge edge; + + public VertexOld( HalfEdge edge ) { + this.edge = edge; + } + + public void assign() { + HalfEdge temp = edge; + do { + temp.origin = this; + temp = temp.nextOrigin; + } while ( temp != edge ); + } + + public boolean isEqualTo( VertexOld o ) { + return x == o.x && y == o.y; + } + + public boolean lessThanOrEqualTo( VertexOld o ) { + return ( x < o.x ) || ( x == o.x && y <= o.y ); + } + + public boolean lessThanOrEqualTo2( VertexOld o ) { + return ( y < o.y ) || ( y == o.y && x <= o.x ); + } + + public double compareTo( VertexOld v1, VertexOld v2 ) { + double x1 = x - v1.x; + double x2 = v2.x - x; + + if ( x1 + x2 > 0 ) { + return ( y - v2.y ) * x1 + ( y - v1.y ) * x2; + } + return 0; + } + + public Vector2dOld subtract( VertexOld other ) { + return new Vector2dOld( x - other.x, y - other.y ); + } + + public Vector2dOld toVector() { + return new Vector2dOld( x, y ); + } + + /* + * Get the vertical distance from this point to the line segment represented by v1 and v2 + * + * This is assuming the following is true: + * - v1 <= this <= v2 + * If v1v2 is vertical (and thus passes through this), the result is 0 + */ + public double verticalDistance( VertexOld v1, VertexOld v2 ) { + // x1 >= 0 + double x1 = x - v1.x; + // x2 >= 0 + double x2 = v2.x - x; + + // Check if it is not vertical + if ( x1 + x2 > 0 ) { + // Find the slope + // Use smaller x value for better double precision, I suppose + if ( x1 < x2 ) { + return ( y - v1.y ) + ( ( v1.y - v2.y ) * ( x1 / ( x1 + x2 ) ) ); + } else { + return ( y - v2.y ) + ( ( v2.y - v1.y ) * ( x2 / ( x1 + x2 ) ) ); + } + } + return 0; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + VertexOld other = (VertexOld) obj; + if (Double.doubleToLongBits(x) != Double.doubleToLongBits(other.x)) + return false; + if (Double.doubleToLongBits(y) != Double.doubleToLongBits(other.y)) + return false; + return true; + } + + @Override + public String toString() { + return "Vertex{x=" + x + ",y=" + y + "}"; + } + + @Override + public int compareTo( final VertexOld o ) { + final int sortX = Double.compare( x, o.x ); + return sortX == 0 ? Double.compare( y, o.y ) : sortX; + } +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/WindingRule.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/WindingRule.java new file mode 100644 index 0000000..21f4b0f --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/WindingRule.java @@ -0,0 +1,23 @@ +package com.aaaaahhhhhhh.bananapuncher714.tess4j; + +/** + * A winding rule describes how a region is modified, particularly when crossing a border + * between a known region to an unknown region. The inverse winding rule must provide a + * symmetric change such that regions can be converted consistently. Normally, the same + * winding 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. + * + * @author BananaPuncher714 + * + * @param A region type that can be modified by this rule + */ +public interface WindingRule< T extends Region > { + // Apply this rule to the region, and return a new region + T apply( T region ); + // Get the inverse of this rule + WindingRule< T > inverse(); +} \ No newline at end of file diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/region/RegionSimple.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/region/RegionSimple.java new file mode 100644 index 0000000..f705c6e --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/region/RegionSimple.java @@ -0,0 +1,82 @@ +package com.aaaaahhhhhhh.bananapuncher714.tess4j.region; + +import com.aaaaahhhhhhh.bananapuncher714.tess4j.Region; + +public class RegionSimple extends Region { + private int windingNumber; + private 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 "Region: " + windingNumber + ", " + rule; + } + + public int getWindingNumber() { + return windingNumber; + } + + public RegionSimple setWindingNumber( int number ) { + this.windingNumber = number; + return this; + } + + public int increment() { + return ++windingNumber; + } + + public int decrement() { + return --windingNumber; + } + + public enum GluWindingRule { + ODD, + NONZERO, + POSITIVE, + NEGATIVE, + ABS_GEQ_TWO + } +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/region/WindingRuleSimple.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/region/WindingRuleSimple.java new file mode 100644 index 0000000..2445b6f --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/region/WindingRuleSimple.java @@ -0,0 +1,36 @@ +package com.aaaaahhhhhhh.bananapuncher714.tess4j.region; + +import com.aaaaahhhhhhh.bananapuncher714.tess4j.WindingRule; + +public class WindingRuleSimple implements WindingRule< RegionSimple > { + private final boolean inverse; + private WindingRuleSimple opposite; + + public WindingRuleSimple() { + this( false ); + } + + public WindingRuleSimple( boolean inverse ) { + this.inverse = inverse; + } + + @Override + public RegionSimple apply( RegionSimple region ) { + RegionSimple copy = new RegionSimple( region ); + if ( inverse ) { + copy.decrement(); + } else { + copy.increment(); + } + return copy; + } + + @Override + public WindingRuleSimple inverse() { + if ( opposite == null ) { + opposite = new WindingRuleSimple( !inverse ); + opposite.opposite = this; + } + return opposite; + } +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/vertex_reduction.txt b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/vertex_reduction.txt new file mode 100644 index 0000000..00755e7 --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/tess4j/vertex_reduction.txt @@ -0,0 +1,41 @@ +The task of triangulation can be completed in logarithmic time, based on the amount of regions and how many nodes each region contains. However, it is not +uncommon for regions to contain many segments that lie on the same line. When this is the case, often times the region can be split into multiple smaller +regions. The benefit of doing so is that there are less triangles that are generated, and each region can be triangulated in parallel, for a bonus speed +increase. The downside is that splitting a region in the most optimal method can be quite costly. It would take polynomial time based on the number of +nodes in order to find the combination of regions that would decrease triangle count the most. However, it can be argued that for certain region shapes +which may occur frequently such as the topology of connected Minecraft blocks, it may be worth the investment if time is less of an issue, and memory +usage matters more. + +Ideal time: O(n) or O(nlog(n)) + +For each point: +- Find all intersecting edges + - Easy check, see which side both ends of each segment are on + - If different sides, then it intersects with the line +- Find all other segments on the same line + - Easy check, find all lines with the same slope and intersection point or something + - Need to check if segment can be connected to + - This means checking all intersecting edges + - Also need to check for individual points + - A segment and a point results in 1 less triangle + - Two segments results in 2 less triangles +- Find all intersections with potential segments + - For each potential segment, check for intersection with each other + - Count how many it intersects with + - Greedy method... Add lines with least amount of intersections first? + - According to the rule of mutual crosses, the total amount of intersections is always even + +The overall algorithm can be broken down into 3 parts: +- Finding all possible segments between 3 or more points +- Removing all connections that intersect with the polygon edges +- Determining how many other possible segments intersect with each other +The first two are both O(n^2) time, which is unfortunate. It is unknown if it could +be made into O(logn) or better. +The last part is O(logn) + +Obviously, one can determine which points are visible from one point by iterating +over every edge and building a depth field. Then, all potential connecting segments +can be found by checking each visible point. +Alternatively, one can check each point to see if it lies on the same segment? +A logical structure to hold a segment would be the two endpoints, and any points that are +contained by the... diff --git a/src/main/resources/README.md b/src/main/resources/README.md new file mode 100644 index 0000000..4ae7517 --- /dev/null +++ b/src/main/resources/README.md @@ -0,0 +1,67 @@ +# Tess4j(name subject to change) +## About +Tess4j(name subject to change) is an all purpose 2d polygon tessellator inspired by the GLUTesselator and libtess2. This tessellator focuses around +user defined rules for interior/exterior regions, and aims to be a robust algorithm with a focus on reducing triangle +count while preserving the original shapes. + +Since it _is_ written in java, speed is not the main priority. Rather, this aims to serve as an easy to understand +and easy to modify library for anyone interested in adding improvements. + +## Restrictions +Tess4j(name subject to change) accepts one or more polygons. Polygons are defined as a collection of one or more 2d points +that form a loop. Steiner points are not (currently)supported. There may be some rounding errors for especially for very large +and small points, but this library aims for consistency, with a small tolerance for adjustments. + +## Algorithm +This library utilizes a very basic single sweep monotone partitioning algorithm, then greedily creates triangle fans and strips. +Additional monotone regions may be broken down into smaller regions if doing so would decrease the overall amount of triangles. + +### Invariants +Tess4j(name subject to change) assumes no responsibility or liability for any errors or omissions in the execution of this program. +The output returned by this program is provided on an "as is" basis with no guarantees of completeness, accuracy, usefulness or timeliness. +That said, here are a list of invariants that _should_ be upheld: +- Any vertex created during the tessellation will be greater than the current event vertex + +### The actual algorithm +Tess4j(name subject to change) begins by accepting a list of one or more polygons. The polygons are split into collections of connected +half-edges and vertices. The vertices are then added to a priority queue where they are sorted from left to right(-x to x), +bottom to top(-y to y) order. + +Once all polygons have been inserted and processed, sweeping of the vertices begins. The lowest unique vertex is extracted from +the priority queue. Any subsequent vertices with the same coordinates are merged together, and any new vertices created during the +sweep event are guaranteed to be greater than the current vertex. + +Sweep over each vertex and mark which ones are an interior non-monotone vertex. Process all left going edges, and mark the faces attached +as interior/exterior. Register all right going edges. Split any intersections into new vertices and edges, and add the vertices to the +event queue. + +**TODO** +Now that each face has been fully completed... reduce the amount of vertices before adding in additional edges for monotone regions. +To do this, get all colinear edges that share the same face, and does not intersect with any other edges??? +Loop through each edge and keep it in the list until it would intersect with another edge, and mark which edges/vertices it is +colinear with, as well as any potential new edges it might collide with. Solve which combination of edges would reduce +vertex count by the greatest amount. 2 vertices if connecting to an edge, 1 vertex if connecting to a vertex. Traveling salesman problem? +When checking for intersections with other support edges, only need to check all active potential aux edges... Consolidate colinears + +For each edge of a face, advance around the face until it intersects with an edge or vertex. Advance backwards as well, checking ONLY vertex intersections. +This will capture all possible conlinear reductions as new edges. + +How to determine which combination can reduce the total amount of vertices? Build clusters of intersecting vertices, + +After all vertex reduction edges have been added, get all faces and calculate the upper and lower sweep edge of +each vertex of each face for monotone partitioning. + +When processing the marked vertices to form monotone regions, some data must be stored for each point so +that a valid point with which to connect can be located afterwards: +- The immediate upper and lower edge of the vertex when it is being swept. +This allows the forward/backward monotone connecting to consistently find a valid point. When checking for valid points: +- The point must be less/greater than the current point if iterating forwards/backwards, respectively. +- The point must fulfill at least one of the following conditions: + - It shares the same upper edge + - It is part of the upper edge + - It shares the same lower edge + - It is part of the lower edge +Once a marked vertex has been connected, either by finding another valid vertex, or by being connected to from a marked +vertex, it can be unmarked and removed from the queue of marked vertices. + +Now that each face is a monotone region, split it into triangle fans and strips. \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..17e3514 --- /dev/null +++ b/src/main/resources/plugin.yml @@ -0,0 +1,14 @@ +name: MinieTest +main: com.aaaaahhhhhhh.bananapuncher714.minietest.MiniePlugin +version: 0.0.1 +description: Minecraft with Minie +author: BananaPuncher714 +api-version: 1.13 +#libraries: +#- com.github.stephengold:Minie:7.5.0 + +commands: + minie: + description: Main Minie command + aliases: [] + permission: minie \ No newline at end of file