diff --git a/buildNumber.properties b/buildNumber.properties index 79587008..88feb54c 100644 --- a/buildNumber.properties +++ b/buildNumber.properties @@ -1,3 +1,3 @@ #maven.buildNumber.plugin properties file -#Tue Aug 20 15:21:51 EDT 2024 -buildNumber=286 +#Thu Aug 22 17:01:13 EDT 2024 +buildNumber=287 diff --git a/src/main/java/electrosphere/util/CodeUtils.java b/src/main/java/electrosphere/util/CodeUtils.java new file mode 100644 index 00000000..d360d5a0 --- /dev/null +++ b/src/main/java/electrosphere/util/CodeUtils.java @@ -0,0 +1,19 @@ +package electrosphere.util; + +/** + * Utilities for making devving faster + */ +public class CodeUtils { + + /** + * Used as placeholder when haven't implemented error handling yet + * @param e The exception + * @param explanation Any kind of explanation string. Ideally explains what the case is + */ + public static void todo(Exception e, String explanation){ + System.out.println("TODO: handle exception"); + System.out.println(explanation); + e.printStackTrace(); + } + +} diff --git a/src/main/java/electrosphere/util/ds/Octree.java b/src/main/java/electrosphere/util/ds/Octree.java new file mode 100644 index 00000000..9180ab9a --- /dev/null +++ b/src/main/java/electrosphere/util/ds/Octree.java @@ -0,0 +1,594 @@ +package electrosphere.util.ds; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.joml.Vector3d; + +/** + * An octree implementation + */ +public class Octree { + + /** + * The number of children in a full, non-leaf node + */ + private static final int FULL_NODE_SIZE = 8; + + + //The root node + private OctreeNode root = null; + + /** + * Table used for looking up the presence of nodes + * Maps a position key to a corresponding Octree node + */ + Map> lookupTable = new HashMap>(); + + /** + * Creates an octree + * @param center The center of the root node of the octree + */ + public Octree(Vector3d boundLower, Vector3d boundUpper){ + root = new OctreeNode(boundLower, boundUpper); + } + + /** + * Adds a leaf to the octree + * @param location The location of the leaf + * @param data The data associated with the leaf + * @throws IllegalArgumentException Thrown if a location is provided that already has an exact match + */ + public void addLeaf(Vector3d location, T data) throws IllegalArgumentException { + //check if the location is already occupied + if(this.containsLeaf(location)){ + throw new IllegalArgumentException("Tried adding leaf that is already occupied!"); + } + OctreeNode node = new OctreeNode(data, location); + OctreeNode current = this.root; + while(current.children.size() == FULL_NODE_SIZE){ + if(current.hasChildInQuadrant(location)){ + OctreeNode child = current.getChildInQuadrant(location); + if(child.isLeaf()){ + //get parent of the new fork + OctreeNode parent = child.parent; + parent.removeChild(child); + + //while the newly forked child isn't between the two children, keep forking + OctreeNode nonLeaf = parent.forkQuadrant(child.getLocation()); + while(nonLeaf.getQuadrant(child.getLocation()) == nonLeaf.getQuadrant(node.getLocation())){ + nonLeaf = nonLeaf.forkQuadrant(child.getLocation()); + } + + + //add the child and the new node to the non-leaf node + nonLeaf.addChild(child); + nonLeaf.addChild(node); + this.lookupTable.put(this.getLocationKey(location),node); + break; + } else { + current = child; + } + } else { + current.addChild(node); + this.lookupTable.put(this.getLocationKey(location),node); + break; + } + } + } + + /** + * Checks if the octree contains a leaf at an exact location + * @param location The location + * @return true if a leaf exists for that exact location, false otherwise + */ + public boolean containsLeaf(Vector3d location){ + return lookupTable.containsKey(this.getLocationKey(location)); + } + + /** + * Gets the leaf at an exact location + * @param location The location + * @return The leaf + * @throws ArrayIndexOutOfBoundsException Thrown if a location is queries that does not have an associated leaf in the octree + */ + public OctreeNode getLeaf(Vector3d location) throws ArrayIndexOutOfBoundsException { + if(!this.containsLeaf(location)){ + throw new ArrayIndexOutOfBoundsException("Tried to get leaf at position that does not contain leaf!"); + } + return lookupTable.get(this.getLocationKey(location)); + } + + /** + * Removes a node from the octree + * @param node The node + */ + public void removeNode(OctreeNode node){ + OctreeNode parent = node.parent; + parent.removeChild(node); + if(node.isLeaf()){ + this.lookupTable.remove(this.getLocationKey(node.getLocation())); + } + } + + /** + * Gets the root node + * @return The root node + */ + public OctreeNode getRoot(){ + return this.root; + } + + /** + * Gets the number of leaves under this tree + * @return The number of leaves + */ + public int getNumLeaves(){ + return this.root.getNumLeaves(); + } + + /** + * Gets the key from the location + * @param location The location + * @return The key + */ + private String getLocationKey(Vector3d location){ + return location.x + "_" + location.y + "_" + location.z; + } + + + /** + * A single node in the octree + */ + public static class OctreeNode { + + /* + 6 +----------+ 7 + /| / | + / | / | + / | / | + 2 +---------+ 3 + 5 + | /4 | / + | / | / + | / | / + +---------+ + 0 1 + + + + Y Z + ^ / + | / + |/ + +---> X + + */ + + //the location of the node + private Vector3d midpoint; + + //the bounds of the node + private Vector3d lowerBound; + private Vector3d upperBound; + + //True if this is a leaf node, false otherwise + private boolean isLeaf; + + //the parent node + private OctreeNode parent; + + //the children of this node + private List> children = new LinkedList>(); + + //The data at the node + private T data; + + /** + * Constructor + * @param data The data at this octree node's position + * @param location The location of the node + */ + private OctreeNode(T data, Vector3d location){ + this.data = data; + this.midpoint = location; + this.isLeaf = true; + } + + /** + * Creates a non-leaf node + * @param lowerBound The lower bound of the node + * @param upperBound The upper bound of the node + */ + private OctreeNode(Vector3d lowerBound, Vector3d upperBound){ + this.data = null; + this.midpoint = new Vector3d( + ((upperBound.x - lowerBound.x) / 2.0) + lowerBound.x, + ((upperBound.y - lowerBound.y) / 2.0) + lowerBound.y, + ((upperBound.z - lowerBound.z) / 2.0) + lowerBound.z + ); + this.lowerBound = lowerBound; + this.upperBound = upperBound; + for(int i = 0; i < FULL_NODE_SIZE; i++){ + this.children.add(null); + } + this.isLeaf = false; + } + + /** + * Creates a non-leaf node + * @param midpoint The midpoint of the node + */ + private OctreeNode(Vector3d midpoint){ + this.data = null; + this.midpoint = midpoint; + for(int i = 0; i < FULL_NODE_SIZE; i++){ + this.children.add(null); + } + this.isLeaf = false; + } + + /** + * Gets the data in the node + * @return The data + */ + public T getData(){ + return data; + } + + /** + * Gets the location of the node + * @return The location + */ + public Vector3d getLocation(){ + return midpoint; + } + + /** + * Gets the parent node of this node + * @return The parent node + */ + public OctreeNode getParent(){ + return parent; + } + + /** + * Gets the children of this node + * @return The children + */ + public List> getChildren(){ + return this.children.stream().filter((node)->{return node != null;}).collect(Collectors.toList()); + } + + /** + * Checks if this node has a child in a given quadrant + * @param positionToCheck The position to check + * @return true if there is a child in that quadrant, false otherwise + */ + public boolean hasChildInQuadrant(Vector3d positionToCheck){ + int positionQuadrant = this.getQuadrant(positionToCheck); + OctreeNode child = this.children.get(positionQuadrant); + return child != null; + } + + /** + * Gets the child in a given quadrant + * @param positionToQuery The position of the quadrant + * @return The child if it exists, null otherwise + */ + public OctreeNode getChildInQuadrant(Vector3d positionToQuery){ + int positionQuadrant = this.getQuadrant(positionToQuery); + OctreeNode child = this.children.get(positionQuadrant); + return child; + } + + /** + * Checks if this is a leaf node or not + * @return true if it is a leaf, false otherwise + */ + public boolean isLeaf(){ + return this.isLeaf; + } + + /** + * Gets the number of child nodes + * @return The number of child nodes + */ + public int getNumChildren(){ + int acc = 0; + for(OctreeNode child : children){ + if(child != null){ + acc++; + } + } + return acc; + } + + /** + * Gets the number of leaves beneath this node + * @return The number of leaves + */ + public int getNumLeaves(){ + int acc = 0; + if(this.isLeaf()){ + return 1; + } else { + for(OctreeNode child : this.children){ + if(child != null){ + if(child.isLeaf()){ + acc++; + } else { + acc = acc + child.getNumLeaves(); + } + } + } + } + return acc; + } + + /** + * Adds a child to this node + * @param child The child + */ + private void addChild(OctreeNode child){ + if(hasChildInQuadrant(child.midpoint)){ + throw new IllegalArgumentException("Trying to add child in occupied quadrant!"); + } + int quadrant = this.getQuadrant(child.getLocation()); + this.children.set(quadrant,child); + child.parent = this; + this.setChildBounds(child); + } + + /** + * Removes a child from this node + * @param child The child + */ + private void removeChild(OctreeNode child){ + if(child == null){ + throw new IllegalArgumentException("Child cannot be null!"); + } + int quadrant = this.getQuadrant(child.getLocation()); + this.children.set(quadrant,null); + child.parent = null; + } + + /** + * Gets the quadrant of a position relative to this node + * @param positionToCheck The position to check + * @return The quadrant + */ + private int getQuadrant(Vector3d positionToCheck){ + if(positionToCheck.z < this.midpoint.z){ + if(positionToCheck.y < this.midpoint.y){ + if(positionToCheck.x < this.midpoint.x){ + return 0; + } else { + return 1; + } + } else { + if(positionToCheck.x < this.midpoint.x){ + return 2; + } else { + return 3; + } + } + } else { + if(positionToCheck.y < this.midpoint.y){ + if(positionToCheck.x < this.midpoint.x){ + return 4; + } else { + return 5; + } + } else { + if(positionToCheck.x < this.midpoint.x){ + return 6; + } else { + return 7; + } + } + } + } + + /** + * Sets the bounds of the child based on the quadrant it falls under + * @param child The child + */ + private void setChildBounds(OctreeNode child){ + if(child.midpoint.z < this.midpoint.z){ + if(child.midpoint.y < this.midpoint.y){ + if(child.midpoint.x < this.midpoint.x){ + child.lowerBound = new Vector3d(this.lowerBound); + child.upperBound = new Vector3d(this.midpoint); + } else { + child.lowerBound = new Vector3d( + this.midpoint.x, + this.lowerBound.y, + this.lowerBound.z + ); + child.upperBound = new Vector3d( + this.upperBound.x, + this.midpoint.y, + this.midpoint.z + ); + } + } else { + if(child.midpoint.x < this.midpoint.x){ + child.lowerBound = new Vector3d( + this.lowerBound.x, + this.midpoint.y, + this.lowerBound.z + ); + child.upperBound = new Vector3d( + this.midpoint.x, + this.upperBound.y, + this.midpoint.z + ); + } else { + child.lowerBound = new Vector3d( + this.midpoint.x, + this.midpoint.y, + this.lowerBound.z + ); + child.upperBound = new Vector3d( + this.upperBound.x, + this.upperBound.y, + this.midpoint.z + ); + } + } + } else { + if(child.midpoint.y < this.midpoint.y){ + if(child.midpoint.x < this.midpoint.x){ + child.lowerBound = new Vector3d( + this.lowerBound.x, + this.lowerBound.y, + this.midpoint.z + ); + child.upperBound = new Vector3d( + this.midpoint.x, + this.midpoint.y, + this.upperBound.z + ); + } else { + child.lowerBound = new Vector3d( + this.midpoint.x, + this.lowerBound.y, + this.midpoint.z + ); + child.upperBound = new Vector3d( + this.upperBound.x, + this.midpoint.y, + this.upperBound.z + ); + } + } else { + if(child.midpoint.x < this.midpoint.x){ + child.lowerBound = new Vector3d( + this.lowerBound.x, + this.midpoint.y, + this.midpoint.z + ); + child.upperBound = new Vector3d( + this.midpoint.x, + this.upperBound.y, + this.upperBound.z + ); + } else { + child.lowerBound = new Vector3d(this.midpoint); + child.upperBound = new Vector3d(this.upperBound); + } + } + } + } + + /** + * Gets the midpoint of a given quadrant + * @param quadrant The quadrant + * @return The midpoint + */ + private Vector3d getQuadrantMidpoint(int quadrant){ + Vector3d lowerBound = null; + Vector3d upperBound = null; + if(quadrant == 0){ + lowerBound = new Vector3d(this.lowerBound); + upperBound = new Vector3d(this.midpoint); + } else if(quadrant == 1) { + lowerBound = new Vector3d( + this.midpoint.x, + this.lowerBound.y, + this.lowerBound.z + ); + upperBound = new Vector3d( + this.upperBound.x, + this.midpoint.y, + this.midpoint.z + ); + } else if(quadrant == 2){ + lowerBound = new Vector3d( + this.lowerBound.x, + this.midpoint.y, + this.lowerBound.z + ); + upperBound = new Vector3d( + this.midpoint.x, + this.upperBound.y, + this.midpoint.z + ); + } else if(quadrant == 3) { + lowerBound = new Vector3d( + this.midpoint.x, + this.midpoint.y, + this.lowerBound.z + ); + upperBound = new Vector3d( + this.upperBound.x, + this.upperBound.y, + this.midpoint.z + ); + } else if(quadrant == 4){ + lowerBound = new Vector3d( + this.lowerBound.x, + this.lowerBound.y, + this.midpoint.z + ); + upperBound = new Vector3d( + this.midpoint.x, + this.midpoint.y, + this.upperBound.z + ); + } else if(quadrant == 5) { + lowerBound = new Vector3d( + this.midpoint.x, + this.lowerBound.y, + this.midpoint.z + ); + upperBound = new Vector3d( + this.upperBound.x, + this.midpoint.y, + this.upperBound.z + ); + } else if(quadrant == 6){ + lowerBound = new Vector3d( + this.lowerBound.x, + this.midpoint.y, + this.midpoint.z + ); + upperBound = new Vector3d( + this.midpoint.x, + this.upperBound.y, + this.upperBound.z + ); + } else if(quadrant == 7) { + lowerBound = new Vector3d(this.midpoint); + upperBound = new Vector3d(this.upperBound); + } else { + throw new IllegalArgumentException("Trying to get midpoint of invalid quadrant!" + quadrant); + } + Vector3d midpoint = new Vector3d( + ((upperBound.x - lowerBound.x) / 2.0) + lowerBound.x, + ((upperBound.y - lowerBound.y) / 2.0) + lowerBound.y, + ((upperBound.z - lowerBound.z) / 2.0) + lowerBound.z + ); + return midpoint; + } + + /** + * Forks a quadrant + * @param location The location within the quadrant + * @return The midpoint of the new node + */ + private OctreeNode forkQuadrant(Vector3d location){ + int quadrant = this.getQuadrant(location); + Vector3d midpoint = this.getQuadrantMidpoint(quadrant); + //create and add the non-leaf node + OctreeNode nonLeaf = new OctreeNode(midpoint); + this.addChild(nonLeaf); + return nonLeaf; + } + + } + +} diff --git a/src/test/java/electrosphere/util/ds/OctreeTests.java b/src/test/java/electrosphere/util/ds/OctreeTests.java new file mode 100644 index 00000000..8490334e --- /dev/null +++ b/src/test/java/electrosphere/util/ds/OctreeTests.java @@ -0,0 +1,292 @@ +package electrosphere.util.ds; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.LinkedList; +import java.util.List; +import java.util.Random; + +import org.joml.Vector3d; + +import annotations.FastTest; +import electrosphere.util.ds.Octree.OctreeNode; + +/** + * Unit testing for the octree implementation + */ +public class OctreeTests { + + /** + * Creates an octree + */ + @FastTest + public void testCreateOctree(){ + new Octree(new Vector3d(0,0,0), new Vector3d(32,32,32)); + } + + /** + * Add a single leaf + */ + @FastTest + public void testAddNode(){ + Octree octree = new Octree(new Vector3d(0,0,0), new Vector3d(32,32,32)); + octree.addLeaf(new Vector3d(1,1,1), 1); + // + //validate the tree + // + List> openSet = new LinkedList>(); + openSet.add(octree.getRoot()); + while(openSet.size() > 0){ + OctreeNode current = openSet.remove(0); + if(current.isLeaf()){ + assertNotNull(current.getData()); + } else { + assertNull(current.getData()); + for(OctreeNode child : current.getChildren()){ + if(child != null && child.getParent() != current){ + fail("Child not attached to parent!"); + } + if(child != null){ + openSet.add(child); + } + } + } + } + } + + /** + * Add two leaves in a line + */ + @FastTest + public void testAddTwo(){ + Octree octree = new Octree(new Vector3d(0,0,0), new Vector3d(32,32,32)); + octree.addLeaf(new Vector3d(1,1,1), 1); + octree.addLeaf(new Vector3d(2,1,1), 2); + // + //validate the tree + // + List> openSet = new LinkedList>(); + openSet.add(octree.getRoot()); + while(openSet.size() > 0){ + OctreeNode current = openSet.remove(0); + if(current.isLeaf()){ + assertNotNull(current.getData()); + } else { + assertNull(current.getData()); + for(OctreeNode child : current.getChildren()){ + if(child != null && child.getParent() != current){ + fail("Child not attached to parent!"); + } + if(child != null){ + openSet.add(child); + } + } + } + } + // + //Specific, expected values to check. Verifies that the octree construct itself correctly + // + OctreeNode current = octree.getRoot().getChildren().get(0); + assertEquals(8, current.getLocation().x); + assertEquals(8, current.getLocation().y); + assertEquals(8, current.getLocation().z); + assertTrue(!current.isLeaf()); + assertEquals(1, current.getNumChildren()); + + current = current.getChildren().get(0); + assertEquals(4, current.getLocation().x); + assertEquals(4, current.getLocation().y); + assertEquals(4, current.getLocation().z); + assertTrue(!current.isLeaf()); + assertEquals(1, current.getNumChildren()); + + current = current.getChildren().get(0); + assertEquals(2, current.getLocation().x); + assertEquals(2, current.getLocation().y); + assertEquals(2, current.getLocation().z); + assertTrue(!current.isLeaf()); + assertEquals(2, current.getNumChildren()); + + OctreeNode leaf1 = current.getChildren().get(0); + OctreeNode leaf2 = current.getChildren().get(1); + assertTrue(leaf1.isLeaf()); + assertTrue(leaf2.isLeaf()); + } + + /** + * Adds a whole bunch of nodes in a line + */ + @FastTest + public void testAddLine(){ + Octree octree = new Octree(new Vector3d(0,0,0), new Vector3d(32,32,32)); + for(int i = 0; i < 31; i++){ + octree.addLeaf(new Vector3d(i,1,0), i); + } + // + //validate the tree + // + List> openSet = new LinkedList>(); + openSet.add(octree.getRoot()); + while(openSet.size() > 0){ + OctreeNode current = openSet.remove(0); + if(current.isLeaf()){ + assertNotNull(current.getData()); + } else { + assertNull(current.getData()); + for(OctreeNode child : current.getChildren()){ + if(child != null && child.getParent() != current){ + fail("Child not attached to parent!"); + } + if(child != null){ + openSet.add(child); + } + } + } + } + } + + /** + * Get a single leaf + */ + @FastTest + public void testGetLeaf(){ + Octree octree = new Octree(new Vector3d(0,0,0), new Vector3d(32,32,32)); + octree.addLeaf(new Vector3d(1,1,1), 1); + OctreeNode leaf = octree.getLeaf(new Vector3d(1,1,1)); + assertNotNull(leaf); + // + //validate the tree + // + List> openSet = new LinkedList>(); + openSet.add(octree.getRoot()); + while(openSet.size() > 0){ + OctreeNode current = openSet.remove(0); + if(current.isLeaf()){ + assertNotNull(current.getData()); + } else { + assertNull(current.getData()); + for(OctreeNode child : current.getChildren()){ + if(child != null && child.getParent() != current){ + fail("Child not attached to parent!"); + } + if(child != null){ + openSet.add(child); + } + } + } + } + } + + /** + * Remove a single leaf + */ + @FastTest + public void testRemoveLeaf(){ + Octree octree = new Octree(new Vector3d(0,0,0), new Vector3d(32,32,32)); + octree.addLeaf(new Vector3d(1,1,1), 1); + OctreeNode leaf = octree.getLeaf(new Vector3d(1,1,1)); + assertNotNull(leaf); + assertNotNull(leaf.getParent()); + octree.removeNode(leaf); + // + //validate the tree + // + List> openSet = new LinkedList>(); + openSet.add(octree.getRoot()); + while(openSet.size() > 0){ + OctreeNode current = openSet.remove(0); + if(current.isLeaf()){ + assertNotNull(current.getData()); + } else { + assertNull(current.getData()); + for(OctreeNode child : current.getChildren()){ + if(child != null && child.getParent() != current){ + fail("Child not attached to parent!"); + } + if(child != null){ + openSet.add(child); + } + } + } + } + } + + /** + * Remove a leaf from a more complex tree + */ + @FastTest + public void testRemoveLeaf2(){ + Octree octree = new Octree(new Vector3d(0,0,0), new Vector3d(32,32,32)); + octree.addLeaf(new Vector3d(1,1,1), 1); + octree.addLeaf(new Vector3d(2,1,1), 2); + octree.addLeaf(new Vector3d(3,1,1), 3); + OctreeNode leaf = octree.getLeaf(new Vector3d(2,1,1)); + assertNotNull(leaf); + assertNotNull(leaf.getParent()); + + //removes 2 & 3, but not 1 + octree.removeNode(leaf.getParent()); + assertEquals(1, octree.getNumLeaves()); + + //remove 1 + leaf = octree.getLeaf(new Vector3d(1,1,1)); + assertNotNull(leaf); + assertNotNull(leaf.getParent()); + octree.removeNode(leaf.getParent()); + assertEquals(0, octree.getNumLeaves()); + + // + //validate the tree + // + List> openSet = new LinkedList>(); + openSet.add(octree.getRoot()); + while(openSet.size() > 0){ + OctreeNode current = openSet.remove(0); + if(current.isLeaf()){ + assertNotNull(current.getData()); + } else { + assertNull(current.getData()); + for(OctreeNode child : current.getChildren()){ + if(child != null && child.getParent() != current){ + fail("Child not attached to parent!"); + } + if(child != null){ + openSet.add(child); + } + } + } + } + } + + /** + * Adds lots of (random) points + */ + @FastTest + public void testAddManyPoints(){ + Octree octree = new Octree(new Vector3d(0,0,0), new Vector3d(256,256,256)); + + + // + //add points + Random rand = new Random(); + for(int i = 0; i < 1000; i++){ + Vector3d loc = new Vector3d( + rand.nextInt(0, 256), + rand.nextInt(0, 256), + rand.nextInt(0, 256) + ); + octree.addLeaf(loc, i); + } + + // + //verify at least one point was added + assertEquals(true, octree.getNumLeaves() > 0); + } + + + +}