diff --git a/Comparison/pom.xml b/Comparison/pom.xml index 00895559491d9bcfb94d38c3a59f0313b2bf5de1..6480685ee3f5cec1d275ec0ec7b113af325faba6 100644 --- a/Comparison/pom.xml +++ b/Comparison/pom.xml @@ -24,6 +24,7 @@ <publicPackage>cz.fidentis.analyst.visitors.*</publicPackage> <publicPackage>cz.fidentis.analyst.visitors.mesh.*</publicPackage> <publicPackage>cz.fidentis.analyst.visitors.kdtree.*</publicPackage> + <publicPackage>cz.fidentis.analyst.visitors.octree.*</publicPackage> </publicPackages> </configuration> </plugin> diff --git a/Comparison/src/main/java/cz/fidentis/analyst/visitors/octree/OctreeArrayIntersectionVisitor.java b/Comparison/src/main/java/cz/fidentis/analyst/visitors/octree/OctreeArrayIntersectionVisitor.java new file mode 100644 index 0000000000000000000000000000000000000000..a2740c97cc2b8de6cf3b9568a8039b3e4aedda08 --- /dev/null +++ b/Comparison/src/main/java/cz/fidentis/analyst/visitors/octree/OctreeArrayIntersectionVisitor.java @@ -0,0 +1,436 @@ +package cz.fidentis.analyst.visitors.octree; + +import cz.fidentis.analyst.mesh.MeshVisitor; +import cz.fidentis.analyst.mesh.core.MeshFacet; +import cz.fidentis.analyst.mesh.core.MeshPoint; +import cz.fidentis.analyst.mesh.core.MeshTriangle; +import cz.fidentis.analyst.octree.OctNode; +import cz.fidentis.analyst.octree.Octree; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import javax.vecmath.Point3d; +import javax.vecmath.Vector3d; + +/** + * This visitor goes through all the notes in mainFacets and for each point + * it calculates the distance to the other meshes (octrees) + * by throwing a ray from that point + * + * @author Enkh-Undral EnkhBayar + */ +public class OctreeArrayIntersectionVisitor extends MeshVisitor { + /** + * main mesh facets from which the intersections are calculated + */ + private final Set<MeshFacet> mainFacets; + + /** + * helper set for calculations, + * used to cache which triangles were already calculated + */ + private Set<MeshTriangle> calculatedTriangles = new HashSet<>(); + + /** + * calculated intersections. + * In format {@code <main mainFacet, <index, <mainFacet, intersection>>>} + * main mainFacet - main mainFacet saved in mainFacets which was used for intersection calculations + * index - index of the starting point in main mainFacet, + * said point can be accessed by mainFacet.get(index) + * mainFacet - mainFacet holding the intersection point + * intersection - intersection of ray coming from starting point and mainFacet + */ + private Map<MeshFacet, Map<Integer, Map<MeshFacet, Point3d>>> intersections = new HashMap<>(); + + /** + * Constructor + * + * @param mainFacet the main Mesh mainFacet from which to calculate the distances. + * Must not be {@code null} + * @throws IllegalArgumentException if some parameter is wrong + */ + public OctreeArrayIntersectionVisitor(MeshFacet mainFacet) { + this(new HashSet<>(Collections.singleton(mainFacet))); + if (mainFacet == null) { + throw new IllegalArgumentException("mainFacet"); + } + } + + /** + * Constructor + * + * @param mainFacets the main Mesh mainFacet from which to calculate the distances. + * Must not be {@code null} + * @throws IllegalArgumentException if some parameter is wrong + */ + public OctreeArrayIntersectionVisitor(Set<MeshFacet> mainFacets) { + if (mainFacets == null || mainFacets.isEmpty() || + (mainFacets.size() == 1 && mainFacets.contains(null))) { + throw new IllegalArgumentException("mainFacets"); + } + this.mainFacets = mainFacets; + } + + /** + * @return intersections from point in mainFacets to other mesh facets + */ + public Map<MeshFacet, Map<Integer, Map<MeshFacet, Point3d>>> getIntersections() { + Map<MeshFacet, Map<Integer, Map<MeshFacet, Point3d>>> unmodifiableMap = new HashMap<>(); + for (var entry : intersections.entrySet()) { + MeshFacet mainFacet = entry.getKey(); + Map<Integer, Map<MeshFacet, Point3d>> unmodifiableInnerMap = new HashMap<>(); + for (var innerEntry : entry.getValue().entrySet()) { + unmodifiableInnerMap.put(innerEntry.getKey(), Collections.unmodifiableMap(innerEntry.getValue())); + } + unmodifiableMap.put(mainFacet, Collections.unmodifiableMap(unmodifiableInnerMap)); + } + return Collections.unmodifiableMap(unmodifiableMap); + } + + @Override + public void visitMeshFacet(MeshFacet facet) { + if (facet == null) { + throw new IllegalArgumentException("facet is null"); + } + Octree octree = new Octree(facet); + for (MeshFacet mainFacet : mainFacets) { + int i = 0; + for (MeshPoint meshPoint : mainFacet.getVertices()) { + Vector3d vector = meshPoint.getNormal(); + Point3d point = meshPoint.getPosition(); + calculatedTriangles.clear(); + calculateDistanceInPoint(mainFacet, i, octree.getRoot(), point, vector, octree.getMinLen()); + vector.scale(-1); + calculatedTriangles.clear(); + calculateDistanceInPoint(mainFacet, i, octree.getRoot(), point, vector, octree.getMinLen()); + calculatedTriangles.clear(); + i++; + } + } + } + + /** + * updates the intersections. If there are already is an intersection with + * the facet, the one with lower distance is kept and other discarded. + * + * @param mainFacet mainFacet holding the starting point. + * has to be in {@code this.mainFacets} + * Must not be {@code null}. + * @param startPointIndex index of the point in mainFacet from which the + * intersection was calculated. + * mainFacet has to hold a point with startPointIndex + * Must not be {@code null) + * @param facet mainFacet in the node with the intersection. + Must not be {@code null) + * @param intersection intersection of mainFacet and ray from start. + Must not be {@code null). + */ + private void updateIntersections(MeshFacet mainFacet, int startPointIndex, MeshFacet facet, Point3d intersection) { + if (mainFacet == null) { + throw new IllegalArgumentException("start is null"); + } + if (facet == null) { + throw new IllegalArgumentException("facet is null"); + } + if (intersection == null) { + throw new IllegalArgumentException("intersection is null"); + } + Map<Integer, Map<MeshFacet, Point3d>> startingPointMap = new HashMap<>(); + Map<MeshFacet, Point3d> intersectionsMap = new HashMap<>(); + if (intersections.containsKey(mainFacet)) { + startingPointMap = intersections.get(mainFacet); + if (startingPointMap.containsKey(startPointIndex)) { + intersectionsMap = startingPointMap.get(startPointIndex); + if (intersectionsMap.containsKey(facet)) { + Point3d oldIntersection = intersectionsMap.get(facet); + Point3d startingPoint = mainFacet.getVertex(startPointIndex).getPosition(); + if (startingPoint.distance(oldIntersection) <= startingPoint.distance(intersection)) { + return; + } + } + } + } + intersectionsMap.put(facet, intersection); + startingPointMap.put(startPointIndex, intersectionsMap); + intersections.put(mainFacet, startingPointMap); + } + + /** + * Goes through the octree provided in param node and calculates all the + * distances from that node to other meshes + * + * @param mainFacet mainFacet holding the starting point. + * has to be in {@code this.mainFacets} + * Must not be {@code null} + * @param startPointIndex index of the point in mainFacet from which the + * intersection is calculated. + * mainFacet has to hold a point with startPointIndex + * Must not be {@code null} + * @param node to calculate the distances to. + * Must not be {@code null} + * @param p point in this cube (node) and if node is internal node + * it is also in the next cube. + * Must not be {@code null}. + * @param v vector from starting point in main node which defines the ray + * Must not be {@code null} nor (0, 0, 0). + * @param minLen the smallest length in any cube (node) in node. + * Must not be equal to 0 + * @return the point in next cube through which the ray passes + */ + private Point3d calculateDistanceInPoint(MeshFacet mainFacet, int origPointIndex, OctNode node, Point3d p, Vector3d v, double minLen) { + if (mainFacet == null) { + throw new IllegalArgumentException("meshFacet is null"); + } + if (node == null) { + throw new IllegalArgumentException("node is null"); + } + if (p == null) { + throw new IllegalArgumentException("p is null"); + } + if (v == null) { + throw new IllegalArgumentException("v is null"); + } + if (v == new Vector3d()) { + throw new IllegalArgumentException("v is (0, 0, 0)"); + } + if (minLen == 0) { + throw new IllegalArgumentException("minLen is equal to 0"); + } + Point3d origPoint = mainFacet.getVertex(origPointIndex).getPosition(); + + if (!node.isLeafNode()) { + int index = getOctantIndex(p, node); + OctNode child; + if (index == -1) { + p = rayCubeIntersection(origPoint, v, node, false); + p = getPointInNextCube(p, v, node, minLen); + index = getOctantIndex(p, node); + } + while (index != -1) { + child = node.getOctant(index); + p = calculateDistanceInPoint(mainFacet, origPointIndex, child, p, v, minLen); + index = getOctantIndex(p, node); + } + return p; + } + + for (Map.Entry<MeshFacet, Integer> entry : node.getFacets().entrySet()) { + MeshFacet facet = entry.getKey(); + Integer index = entry.getValue(); + for (MeshTriangle triangle : facet.getAdjacentTriangles(index)) { + if (calculatedTriangles.contains(triangle)) { + continue; + } + calculatedTriangles.add(triangle); + Point3d intersection = triangle.getRayIntersection(origPoint, v); + if (intersection != null) { + updateIntersections( + mainFacet, + origPointIndex, + facet, + intersection + ); + } + } + } + Point3d point = rayCubeIntersection(origPoint, v, node, true); + if (point == null) { + throw new RuntimeException("Didnt find intersection with bounding box"); + } + return getPointInNextCube(point, v, node, minLen); + } + + /** + * Calculates the point in the next cube. + * + * @param p point on the side / edge of the cube + * Must not be {@code null}. + * @param v vector of the ray + * Must not be {@code null} nor (0, 0, 0). + * @param cube cube from which to calculate point in next cube is needed + * Must not be {@code null}. + * @param minLen the smallest length in any cube (node) in node. + * Must not be equal to 0 + * @return the point in next cube through which the ray passes + */ + private Point3d getPointInNextCube(Point3d p, Vector3d v, OctNode cube, double minLen) { + if (p == null) { + throw new IllegalArgumentException("p is null"); + } + if (v == null) { + throw new IllegalArgumentException("v is null"); + } + if (v == new Vector3d()) { + throw new IllegalArgumentException("v is (0, 0, 0)"); + } + if (cube == null) { + throw new IllegalArgumentException("cube is null"); + } + if (minLen == 0) { + throw new IllegalArgumentException("minLen is equal to 0"); + } + double[] resultCoor = {p.x, p.y, p.z}; + double[] vCoor = {v.x, v.y, v.z}; + Point3d small = cube.getSmallBoundary(); + double[] smallCoor = {small.x, small.y, small.z}; + Point3d large = cube.getLargeBoundary(); + double[] largeCoor = {large.x, large.y, large.z}; + for (int i = 0; i < 3; i++) { + int sign; + if (vCoor[i] < 0) { + sign = -1; + } else if (vCoor[i] > 0) { + sign = 1; + } else { + continue; + } + if (resultCoor[i] == smallCoor[i]) { + resultCoor[i] += sign * (minLen / 2); + } else if (resultCoor[i] == largeCoor[i]) { + resultCoor[i] += sign * (minLen / 2); + } + } + Point3d resultPoint = new Point3d(resultCoor[0], resultCoor[1], resultCoor[2]); + if (resultPoint == p) { + throw new IllegalArgumentException("p is not on the edges of the cube"); + } + return resultPoint; + } + + /** + * Calculates the intersection between ray and the cube sides + * + * @param p starting point of the ray. + * Must not be {@code null}. + * @param v vector of the ray + * Must not be {@code null} nor (0, 0, 0). + * @param cube cube with the sides + * Must not be {@code null}. + * @param gotInsideOnce boolean paramater that tells us if ray got inside + * the most outer cube. If it did we care about the + * second point in the direction of the ray instead + * of the first one + * @return returns the intersection in the direction of the ray + */ + private Point3d rayCubeIntersection(Point3d p, Vector3d v, OctNode cube, boolean gotInsideOnce) { + if (p == null) { + throw new IllegalArgumentException("p is null"); + } + if (v == null) { + throw new IllegalArgumentException("v is null"); + } + if (v == new Vector3d()) { + throw new IllegalArgumentException("v is (0, 0, 0)"); + } + if (cube == null) { + throw new IllegalArgumentException("cube is null"); + } + Point3d smallBoundary = cube.getSmallBoundary(); + Point3d largeBoundary = cube.getLargeBoundary(); + Vector3d[] planeNormals = {new Vector3d(1, 0, 0), new Vector3d(0, 1, 0), new Vector3d(0, 0, 1)}; + double[] t = {-Double.MAX_VALUE, Double.MAX_VALUE}; + for (Vector3d planeNormal : planeNormals) { + double np = planeNormal.dot(new Vector3d(p)); + double vp = planeNormal.dot(v); + if (vp == 0) { + continue; + } + double[] offset = {planeNormal.dot(new Vector3d(smallBoundary)), + planeNormal.dot(new Vector3d(largeBoundary))}; + double[] tTmp = {(offset[0] - np) / vp, (offset[1] - np) / vp}; + if (tTmp[0] > tTmp[1]) { + double tmp = tTmp[0]; + tTmp[0] = tTmp[1]; + tTmp[1] = tmp; + } + t[0] = Double.max(tTmp[0], t[0]); + t[1] = Double.min(tTmp[1], t[1]); + } + if (t[0] > t[1]) { + return null; + } + Point3d point = new Point3d(p); + Vector3d vector = new Vector3d(v); + if (gotInsideOnce) { + vector.scale(t[1]); + } else { + vector.scale(t[0]); + } + point.add(vector); + return point; + } + + /** + * checks if p is inside the cube + * + * @param p point to be checked. + * Must not be {@code null}. + * @param cube cube with the bounding box. + * Must not be {@code null}. + * @return True if p is inside the cube false otherwise + */ + private boolean isPointInCube(Point3d p, OctNode cube) { + if (p == null) { + throw new IllegalArgumentException("p is null"); + } + if (cube == null) { + throw new IllegalArgumentException("cube is null"); + } + Point3d smallBoundary = cube.getSmallBoundary(); + Point3d largeBoundary = cube.getLargeBoundary(); + if (p.x < smallBoundary.x) { + return false; + } + if (p.y < smallBoundary.y) { + return false; + } + if (p.z < smallBoundary.z) { + return false; + } + if (p.x > largeBoundary.x) { + return false; + } + if (p.y > largeBoundary.y) { + return false; + } + return p.z <= largeBoundary.z; + } + + /** + * Calculates the index of the child which can house the point provided + * + * @param p point, must not be {@code null} + * @param cube cube, must not be {@code null} + * @return index (0 - 7) of child in cube. -1 if p is not inside the cube + */ + private int getOctantIndex(Point3d p, OctNode cube) { + if (p == null) { + throw new IllegalArgumentException("p is null"); + } + if (cube == null) { + throw new IllegalArgumentException("cube is null"); + } + if (!isPointInCube(p, cube)) { + return -1; + } + Point3d smallBoundary = cube.getSmallBoundary(); + Point3d largeBoundary = cube.getLargeBoundary(); + Point3d middlePoint = new Point3d( + (smallBoundary.x + largeBoundary.x) / 2, + (smallBoundary.y + largeBoundary.y) / 2, + (smallBoundary.z + largeBoundary.z) / 2); + int octantIndex = 0; + if (p.x > middlePoint.x) { + octantIndex += 4; + } + if (p.y > middlePoint.y) { + octantIndex += 2; + } + if (p.z > middlePoint.z) { + octantIndex += 1; + } + return octantIndex; + } +} diff --git a/Comparison/src/test/java/cz/fidentis/analyst/visitors/octree/OctreeArrayIntersectionVisitorTest.java b/Comparison/src/test/java/cz/fidentis/analyst/visitors/octree/OctreeArrayIntersectionVisitorTest.java new file mode 100644 index 0000000000000000000000000000000000000000..da31133a5b51f3a3f77452dad6cf785a55b0e13a --- /dev/null +++ b/Comparison/src/test/java/cz/fidentis/analyst/visitors/octree/OctreeArrayIntersectionVisitorTest.java @@ -0,0 +1,188 @@ +package cz.fidentis.analyst.visitors.octree; + +import cz.fidentis.analyst.mesh.core.CornerTableRow; +import cz.fidentis.analyst.mesh.core.MeshFacet; +import cz.fidentis.analyst.mesh.core.MeshFacetImpl; +import cz.fidentis.analyst.mesh.core.MeshPoint; +import cz.fidentis.analyst.mesh.core.MeshPointImpl; +import cz.fidentis.analyst.octree.Octree; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.vecmath.Point3d; +import javax.vecmath.Vector3d; +import org.apache.commons.lang3.tuple.Pair; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.Test; + +/** + * @author Enkh-Undral EnkhBayar + */ +public class OctreeArrayIntersectionVisitorTest { + + private MeshFacet getTrivialFacet(double offset, double size) { + MeshFacet facet = new MeshFacetImpl(); + facet.addVertex(new MeshPointImpl(new Point3d(0, 0, offset), new Vector3d(0, 0, 1), new Vector3d())); + facet.addVertex(new MeshPointImpl(new Point3d(size, 0, offset), new Vector3d(0, 0, 1), new Vector3d())); + facet.addVertex(new MeshPointImpl(new Point3d(0, size, offset), new Vector3d(0, 0, 1), new Vector3d())); + + facet.getCornerTable().addRow(new CornerTableRow(0, -1)); + facet.getCornerTable().addRow(new CornerTableRow(1, -1)); + facet.getCornerTable().addRow(new CornerTableRow(2, -1)); + + return facet; + } + + private void printIntersection(Map<MeshFacet, Map<Integer, Map<MeshFacet, Point3d>>> map) { + StringBuilder builder = new StringBuilder(); + for (var entry : map.entrySet()) { + MeshFacet mainFacet = entry.getKey(); + builder.append(mainFacet.toString()).append('\n'); + var startingPointMap = entry.getValue(); + int i = 0; + for (var startingPointEntry : startingPointMap.entrySet()) { + if (i == startingPointMap.size() - 1) { + builder.append("\\- "); + } else { + builder.append("|- "); + } + int index = startingPointEntry.getKey(); + builder.append(index).append(": ").append(mainFacet.getVertex(index).getPosition()); + builder.append('\n'); + int j = 0; + var intersectionMap = startingPointEntry.getValue(); + for (var intersectionEntry : intersectionMap.entrySet()) { + builder.append('\t'); + if (j == intersectionMap.size() - 1) { + builder.append("\\- "); + } else { + builder.append("|- "); + } + builder.append(intersectionEntry.getKey()); + builder.append(' '); + builder.append(intersectionEntry.getValue()); + } + builder.append('\n'); + i++; + } + } + System.out.println(builder); + } + + private void checkMap(Map<MeshFacet, Map<Integer, Map<MeshFacet, Point3d>>> map, Map<MeshFacet, Map<Integer, Map<MeshFacet, Point3d>>> correct) { + assertTrue(map != null, "map is null"); + Set<MeshFacet> mapKeySet = new HashSet<>(map.keySet()); + assertTrue(map.size() == correct.size()); + for (var entry : correct.entrySet()) { + MeshFacet mainFacet = entry.getKey(); + assertTrue(map.containsKey(mainFacet)); + Map<Integer, Map<MeshFacet, Point3d>> startingPointMapCorrect = entry.getValue(); + Map<Integer, Map<MeshFacet, Point3d>> startingPointMap = map.get(mainFacet); + assertTrue(startingPointMap.size() == startingPointMapCorrect.size()); + Set<Integer> startingPointMapKeySet = new HashSet(startingPointMap.keySet()); + for (var startingPointEntry : startingPointMapCorrect.entrySet()) { + int index = startingPointEntry.getKey(); + assertTrue(startingPointMap.containsKey(index)); + var intersectionsMapCorrect = startingPointEntry.getValue(); + var intersectionsMap = startingPointMap.get(index); + assertTrue(intersectionsMapCorrect.size() == intersectionsMap.size()); + Set<MeshFacet> intersectionMapKeySet = new HashSet(intersectionsMap.keySet()); + for (var intersectionEntry : intersectionsMapCorrect.entrySet()) { + MeshFacet facet = intersectionEntry.getKey(); + assertTrue(intersectionsMap.containsKey(facet)); + Point3d pointCorrect = intersectionEntry.getValue(); + Point3d point = intersectionsMap.get(facet); + assertTrue(point.equals(pointCorrect)); + intersectionMapKeySet.remove(facet); + } + assertTrue(intersectionMapKeySet.isEmpty()); + startingPointMapKeySet.remove(index); + } + assertTrue(startingPointMapKeySet.isEmpty()); + mapKeySet.remove(mainFacet); + } + assertTrue(mapKeySet.isEmpty()); + } + + private List<Integer> findIndices(List<Point3d> points, List<MeshPoint> vertices) { + List<Integer> indices = new ArrayList<>(); + for (int i = 0; i < points.size(); i++) { + indices.add(-1); + } + int i = 0; + for (MeshPoint vertex : vertices) { + for (int j = 0; j < points.size(); j++) { + if (vertex.getPosition().equals(points.get(j))) { + indices.set(j, i); + } + } + i++; + } + if (indices.contains(-1)) { + return null; + } + return indices; + } + + @Test + public void zeroCollision() { + MeshFacet main = getTrivialFacet(4, 0); + MeshFacet facet = new MeshFacetImpl(); + facet.addVertex(new MeshPointImpl(new Point3d(1, 1, 1), new Vector3d(0, 0, 1), new Vector3d())); + facet.addVertex(new MeshPointImpl(new Point3d(2, 1, 1), new Vector3d(0, 0, 1), new Vector3d())); + facet.addVertex(new MeshPointImpl(new Point3d(1, 2, 1), new Vector3d(0, 0, 1), new Vector3d())); + + facet.getCornerTable().addRow(new CornerTableRow(0, -1)); + facet.getCornerTable().addRow(new CornerTableRow(1, -1)); + facet.getCornerTable().addRow(new CornerTableRow(2, -1)); + var visitor = new OctreeArrayIntersectionVisitor(main); + visitor.visitMeshFacet(facet); + var intersections = visitor.getIntersections(); + assertTrue(intersections.isEmpty()); + } + + @Test + public void twoMeshesOneCollision() { + MeshFacet main = getTrivialFacet(1, 2); + MeshFacet second = getTrivialFacet(2, 1); + + List<Point3d> startingPoints = List.of(new Point3d(0, 0, 1)); + List<Integer> indices = findIndices(startingPoints, main.getVertices()); + assertTrue(indices != null); + + var visitor = new OctreeArrayIntersectionVisitor(main); + visitor.visitMeshFacet(second); + var intersections = visitor.getIntersections(); + Map<MeshFacet, Map<Integer, Map<MeshFacet, Point3d>>> correct = Map.of( + main, + Map.of( + indices.get(0), + Map.of(second, new Point3d(0, 0, 2)) + ) + ); + checkMap(intersections, correct); + } + + @Test + public void twoMeshesMultipleCollision() { + MeshFacet main = getTrivialFacet(2, 1); + MeshFacet second = getTrivialFacet(1, 2); + var visitor = new OctreeArrayIntersectionVisitor(main); + visitor.visitMeshFacet(second); + var intersections = visitor.getIntersections(); + List<Point3d> startingPoints = List.of(new Point3d(0, 0, 2), new Point3d(0, 1, 2), new Point3d(1, 0, 2)); + List<Integer> indices = findIndices(startingPoints, main.getVertices()); + assertTrue(indices != null); + Map<MeshFacet, Map<Integer, Map<MeshFacet, Point3d>>> correct = Map.of( + main, + Map.of( + indices.get(0), Map.of(second, new Point3d(0, 0, 1)), + indices.get(1), Map.of(second, new Point3d(0, 1, 1)), + indices.get(2), Map.of(second, new Point3d(1, 0, 1)) + ) + ); + checkMap(intersections, correct); + } +} diff --git a/MeshModel/pom.xml b/MeshModel/pom.xml index 23054306dcf92c96e7aebc9d57251be11c04f74d..f4d243e431f3719c29391c78d21ec6593794fc19 100644 --- a/MeshModel/pom.xml +++ b/MeshModel/pom.xml @@ -22,6 +22,7 @@ <publicPackage>cz.fidentis.analyst.mesh.io.*</publicPackage> <publicPackage>cz.fidentis.analyst.mesh.*</publicPackage> <publicPackage>cz.fidentis.analyst.kdtree.*</publicPackage> + <publicPackage>cz.fidentis.analyst.octree.*</publicPackage> <publicPackage>cz.fidentis.analyst.feature.*</publicPackage> <!--<publicPackage>cz.fidentis.analyst.mesh.core.MeshFacet</publicPackage>--> <!--<publicPackage>cz.fidentis.analyst.mesh.core.MeshPoint</publicPackage>--> diff --git a/MeshModel/src/main/java/cz/fidentis/analyst/mesh/core/MeshTriangle.java b/MeshModel/src/main/java/cz/fidentis/analyst/mesh/core/MeshTriangle.java index e2fedd373a13d6a374a4ced08b93299f39d931e3..8911867b1f118119e2b69a15282578cfe7a9772e 100644 --- a/MeshModel/src/main/java/cz/fidentis/analyst/mesh/core/MeshTriangle.java +++ b/MeshModel/src/main/java/cz/fidentis/analyst/mesh/core/MeshTriangle.java @@ -111,6 +111,134 @@ public class MeshTriangle implements Iterable<MeshPoint> { return normal; } + /** + * Computes intersection between two coplanar lines. + * + * @param firstOrigin starting point of first line + * Must not be {@code null} + * @param firstVector vector of the first line + * Has to be normalized. Must not be {@code null} + * @param secondOrigin starting point of second line + * Must not be {@code null} + * @param secondVector vector of the second line + * Has to be normalized. Must not be {@code null} + * @return point of intersection or {@code null} if the lines are parallel + * + * @author Enkh-Undral EnkhBayar + */ + public Point3d getLinesIntersection(Point3d firstOrigin, Vector3d firstVector, Point3d secondOrigin, Vector3d secondVector) { + double scalar = firstVector.dot(secondVector); + if (1 - EPS <= scalar && scalar <= 1 + EPS) { // lines are parallel + return null; + } + Vector3d ba = new Vector3d(firstOrigin); + ba.sub(secondOrigin); + Vector3d ab = new Vector3d(firstOrigin); + ab.scale(-1); + double a = ba.dot(secondVector); + double b = ab.dot(firstVector) * scalar; + double c = 1 - scalar * scalar; + double s = (a + b) / c; + + Vector3d scaledV = new Vector3d(secondVector); + scaledV.scale(s); + Point3d intersection = new Point3d(secondOrigin); + intersection.add(scaledV); + return intersection; + } + + /** + * determines whether or not the intersection between + * line origin + t * vector and edge AB (first - second) is valid. + * + * @param origin starting point of line + * Must not be {@code null} + * @param vector vector of the line + * Must not be {@code null} + * @param intersection point on line origin + t * vector + * which is on line containing AB + * Must not be {@code null} + * @param first Vertex of this {@code MeshTriangle} + * Must not be {@code null} + * @param second Vertex of this {@code MeshTriangle} + * Must not be {@code null} + * @return true if intersection is valid, false otherwise + * + * @author Enkh-Undral EnkhBayar + */ + private static boolean isIntersectionValid(Point3d origin, Vector3d vector, Point3d intersection, Point3d first, Point3d second) { + double[] aCoor = {first.x, first.y, first.z}; + double[] bCoor = {second.x, second.y, second.z}; + double[] iCoor = {intersection.x, intersection.y, second.z}; + // is intersection between first and second? + for (int i = 0; i < 3; i++) { + if (!(Double.min(aCoor[i], bCoor[i]) <= iCoor[i] && + iCoor[i] <= Double.min(aCoor[i], bCoor[i]))) { + return false; + } + } + // is intersection in direction of vector? + Vector3d oi = new Vector3d(intersection); + oi.sub(origin); + double scalar = vector.dot(oi); + return scalar > 0; + } + + /** + * calculates the intersection between this triangle and ray given + * + * @param origin Point of origin form which the ray starts + * @param vector directional vector of the ray + * @return point of intersection or null if there is no intersection + * + * @author Enkh-Undral EnkhBayar + */ + public Point3d getRayIntersection(Point3d origin, Vector3d vector) { + Vector3d normal = computeNormal(); + double np = normal.dot(new Vector3d(origin)); + double nv = normal.dot(vector); + if (nv == 0) { // plane and vector are parallel + if (np != 0) { // ray is not in plane + return null; + } + if (isPointInTriangle(origin)) { + return origin; + } + Vector3d u = new Vector3d(vector); + u.normalize(); + Point3d[] vertices = {getVertex1(), getVertex2(), getVertex3()}; + Point3d closestIntersection = null; + double smallestDistance = 0; + for (int i = 0; i < 3; i++) { + Vector3d v = new Vector3d(vertices[(i + 1) % 3]); + v.sub(vertices[i]); + v.normalize(); + Point3d newIntersection = getLinesIntersection(origin, u, vertices[i], v); + if (newIntersection == null) { + continue; + } + if (!isIntersectionValid(origin, vector, newIntersection, vertices[i], vertices[(i + 1) % 3])) { + continue; + } + double newDistance = origin.distance(newIntersection); + if (closestIntersection == null || newDistance < smallestDistance) { + closestIntersection = newIntersection; + smallestDistance = newDistance; + } + } + return closestIntersection; + } + + double offset = normal.dot(new Vector3d(getVertex1())); + double t = (offset - nv) / np; + + Point3d intersection = new Point3d(origin); + Vector3d scaledVector = new Vector3d(vector); + scaledVector.scale(t); + intersection.add(scaledVector); + return intersection; + } + /** * Computes the point laying on the triangle which is closest to * given 3D point. Return point is either one of the tringle's vertices, diff --git a/MeshModel/src/main/java/cz/fidentis/analyst/octree/OctNode.java b/MeshModel/src/main/java/cz/fidentis/analyst/octree/OctNode.java new file mode 100644 index 0000000000000000000000000000000000000000..71d77eaee60706b7513ba6d403df19bd1243bddd --- /dev/null +++ b/MeshModel/src/main/java/cz/fidentis/analyst/octree/OctNode.java @@ -0,0 +1,237 @@ +package cz.fidentis.analyst.octree; + +import cz.fidentis.analyst.mesh.core.MeshFacet; +import cz.fidentis.analyst.mesh.core.MeshPoint; +import java.io.Serializable; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.vecmath.Point3d; + +/** + * A single node of a Octree. Sharing vertices across meshes is supported + * (the node links all faces that share the same vertex). + * + * @author Enkh-Undral EnkhBayar + */ +public class OctNode implements Serializable { + + /** + * 3D location of the node, is null in internal nodes and empty node + */ + private MeshPoint location; + + /** + * Mesh facets sharing the stored vertex + */ + private Map<MeshFacet, Integer> facets = new HashMap<>(); + + /** + * Octree topology + */ + private List<OctNode> octants; + + /** + * 3D location of the boundary box - corner with smallest coordinates + */ + private Point3d smallestBoundary; + + /** + * 3D location of the boundary box - corner with largest coordinates + */ + private Point3d largestBoundary; + + /** + * Constructor of OctNode + * + * @param facets Mesh facet containing the mesh vertex. Must not be null nor empty + * @param indices The index under which the vertex is stored in the mesh facet. + * Must be >= 0 + * @param smallest boundary box - corner with smallest coordinates. + * Must not be null + * @param largest boundary box - corner with largest coordinates. + * Must not be null + * @throws IllegalArgumentException if some parameter is wrong + */ + public OctNode(List<MeshFacet> facets, List<Integer> indices, Point3d smallest, Point3d largest) { + if (facets == null || facets.isEmpty()) { + throw new IllegalArgumentException("facets"); + } + if (indices == null || indices.isEmpty()) { + throw new IllegalArgumentException("indices"); + } + if (facets.size() != indices.size()) { + throw new IllegalArgumentException("The number of facets and indiecs mismatch"); + } + if (smallest == null) { + throw new IllegalArgumentException("Smallest boundary in OctNode cannot be null"); + } + if (largest == null) { + throw new IllegalArgumentException("Largest boundary in OctNode cannot be null"); + } + + for (int i = 0; i < facets.size(); i++) { + this.facets.putIfAbsent(facets.get(i), indices.get(i)); + } + this.location = facets.get(0).getVertex(indices.get(0)); + this.smallestBoundary = smallest; + this.largestBoundary = largest; + } + + /** + * Constructor of OctNode for internal nodes + * + * @param octants List of octNodes which are children for this node. + * Must not be null and has to have size of 8. + * @param smallest boundary box - corner with smallest coordinates. + * Must not be null + * @param largest boundary box - corner with largest coordinates. + * Must not be null + * @throws IllegalArgumentException if some parameter is wrong + */ + protected OctNode(List<OctNode> octants, Point3d smallest, Point3d largest) { + if (octants == null || octants.size() != 8) { + throw new IllegalArgumentException("octants"); + } + if (smallest == null) { + throw new IllegalArgumentException("Smallest boundary in OctNode cannot be null"); + } + if (largest == null) { + throw new IllegalArgumentException("Largest boundary in OctNode cannot be null"); + } + + this.octants = octants; + this.smallestBoundary = smallest; + this.largestBoundary = largest; + } + + /** + * Constructor of OctNode for empty nodes. + * + * @param smallest boundary box - corner with smallest coordinates. + * Must not be null + * @param largest boundary box - corner with largest coordinates. + * Must not be null + * @throws IllegalArgumentException if some parameter is wrong + */ + protected OctNode(Point3d smallest, Point3d largest) { + if (smallest == null) { + throw new IllegalArgumentException("Smallest boundary in OctNode cannot be null"); + } + if (largest == null) { + throw new IllegalArgumentException("Largest boundary in OctNode cannot be null"); + } + + this.smallestBoundary = smallest; + this.largestBoundary = largest; + } + + public boolean isLeafNode() { + return octants == null || octants.isEmpty(); + } + + /** + * returns Octant under specific index + * + * @param index index of the octant returned. + * Must be between 0 and 7 including. + * @return Octant under specific index + */ + public OctNode getOctant(int index) { + if (!(0 <= index && index < 8)) { + throw new IllegalArgumentException("getOctant passed with illegal index, can only be 0 to 7"); + } + if (isLeafNode()) { + return null; + } + return octants.get(index); + } + + /** + * Returns 3D location of vertices stored in this node + * @return 3D location of vertices stored in this node + */ + public MeshPoint getLocation() { + return location; + } + + /** + * returns boundary box - corner with smallest coordinates. + * @return boundary box - corner with smallest coordinates. + */ + public Point3d getSmallBoundary() { + return smallestBoundary; + } + + /** + * returns boundary box - corner with largest coordinates. + * @return boundary box - corner with largest coordinates. + */ + public Point3d getLargeBoundary() { + return largestBoundary; + } + + /** + * Returns a map of all mesh facets that share the stored vertex. + * Value in the map contains the index which the + * vertex is stored in the mesh facet. + * + * @return Map of facets sharing the stored mesh vertex + */ + public Map<MeshFacet, Integer> getFacets() { + return Collections.unmodifiableMap(facets); + } + + /** + * Set octants / children + * + * @param octants children of this node. + * Must not be null and has to have size of 8 + * + */ + protected void setOctants(List<OctNode> octants) { + if (octants == null || octants.size() != 8) { + throw new IllegalArgumentException("setOctants passed with null or illegal size"); + } + this.location = null; + this.octants = octants; + } + + @Override + public String toString() { + String result = "[" + smallestBoundary + ", " + largestBoundary + "]"; + if (isLeafNode() && location != null) { + result += " " + location.getPosition(); + } + return result; + } + + protected String toString(String prefix) { + if (isLeafNode()) { + return adjustPrefix(prefix) + toString() + '\n'; + } + StringBuilder result = new StringBuilder(adjustPrefix(prefix)); + result.append(toString()).append('\n'); + if (prefix.endsWith("\\")) { + prefix = prefix.substring(0, prefix.length() - 1) + ' '; + } + for (int i = 0; i < 7; i++) { + result.append(getOctant(i).toString(prefix + '|')); + } + result.append(getOctant(7).toString(prefix + '\\')); + return result.toString(); + } + + private String adjustPrefix(String prefix) { + if (prefix.isEmpty()) { + return ""; + } + StringBuilder result = new StringBuilder(); + for (int i = 0; i < prefix.length() - 1; i++) { + result.append(prefix.charAt(i)).append(" "); + } + result.append(prefix.charAt(prefix.length() - 1)).append("-- "); + return result.toString(); + } +} diff --git a/MeshModel/src/main/java/cz/fidentis/analyst/octree/Octree.java b/MeshModel/src/main/java/cz/fidentis/analyst/octree/Octree.java new file mode 100644 index 0000000000000000000000000000000000000000..b6dfe2158efd496a1411e0be79f51eb9978854c6 --- /dev/null +++ b/MeshModel/src/main/java/cz/fidentis/analyst/octree/Octree.java @@ -0,0 +1,289 @@ +package cz.fidentis.analyst.octree; + +import cz.fidentis.analyst.mesh.core.MeshFacet; +import cz.fidentis.analyst.mesh.core.MeshFacetImpl; +import cz.fidentis.analyst.mesh.core.MeshPoint; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.vecmath.Point3d; + +/** + * {@code Octree} for storing vertices ({@code MeshPoint}s) of + * triangular meshes ({@code MeshFacet}s). + * Multiple mesh facets can by stored in a single {@code Octree}. In this case, + * vertices that are shared across multiple facets (have the same 3D location) + * are shared in the same node of the {@code Octree}. + * + * @author Enkh-Undral EnkhBayar + */ +public class Octree implements Serializable { + + private OctNode root; + + /** + * distance of the smallest cell in {@code Octree}. + * It is the smallest between dx, dy and dz + */ + private Double minLen = Double.MAX_VALUE; + + /** + * Constructor. + * + * If no mesh points (vertices) are provided, then an empty + * {@code Octree} is constructed (with the root node set to null). + * + * @param points A set of individual mesh points. + */ + public Octree(Set<MeshPoint> points) { + if (points == null) { + this.root = null; + return; + } + MeshFacet newFacet = new MeshFacetImpl(); + for (MeshPoint point : points) { + newFacet.addVertex(point); + } + buildTree(new LinkedList<>(Collections.singleton(newFacet))); + } + + /** + * Constructor. + * + * If no mesh points (vertices) are provided, then an empty + * {@code Octree} is constructed (with the root node set to null). + * + * @param facet Mesh facet + */ + public Octree(MeshFacet facet) { + this(new LinkedList<>(Collections.singleton(facet))); + } + + /** + * Constructor. + * + * If no mesh points (vertices) are provided, then an empty + * {@code Octree} is constructed (with the root node set to null). + * If multiple mesh facets share the same vertex, then they are stored + * efficiently in the same node of the {@code Octree}. + * + * @param facets The list of mesh facets to be stored. Facets can share vertices. + */ + public Octree(List<MeshFacet> facets) { + if(facets == null || facets.isEmpty() || facets.get(0).getVertices().isEmpty() ){ + this.root = null; + return; + } + buildTree(facets); + } + + /** + * Tree traversal - go to the "root" of the tree. + * + * @return root node of the tree + */ + public OctNode getRoot() { + return root; + } + + /** + * @return distance of the smallest cell in {@code Octree}. + */ + public Double getMinLen() { + return minLen; + } + + /** + * Recursively display the contents of the tree in a verbose format. + * Individual nodes are represented in format (if the node does not hold a point) + * [small boundary, large boundary] + * and in format if it does + * [small boundary, large boundary] point + * + * @return representation of the tree + */ + @Override + public String toString() { + return root.toString(""); + } + + /*********************************************************** + * PRIVATE METHODS * + ***********************************************************/ + + private Point3d[] updateBoundaries(Point3d small, Point3d large) { + double[] coorSmall = {small.x, small.y, small.z}; + double[] coorLarge = {large.x, large.y, large.z}; + int maxIndex = 0; + for (int i = 1; i <= 2; i++) { + if (coorLarge[i] - coorSmall[i] > coorLarge[maxIndex] - coorSmall[maxIndex]) { + maxIndex = i; + } + } + double halfMaxDiff = (coorLarge[maxIndex] - coorSmall[maxIndex]) / 2; + for (int i = 0; i <= 2; i++) { + if (i == maxIndex) { + continue; + } + double average = (coorSmall[i] + coorLarge[i]) / 2; + coorSmall[i] = average - halfMaxDiff; + coorLarge[i] = average + halfMaxDiff; + } + return new Point3d[] { + new Point3d(coorSmall[0], coorSmall[1], coorSmall[2]), + new Point3d(coorLarge[0], coorLarge[1], coorLarge[2]) + }; + } + + private void updateMinLen(Point3d smallestPoint, Point3d largestPoint) { + minLen = Double.min(minLen, largestPoint.x - smallestPoint.x); + minLen = Double.min(minLen, largestPoint.y - smallestPoint.y); + minLen = Double.min(minLen, largestPoint.z - smallestPoint.z); + } + + private void buildTree(List<MeshFacet> facets) { + HashMap<MeshPoint, AggregatedVertex> vertices = new HashMap(); + + /* + * Find bounding box and aggregate vertices + * with the same 3D location. + */ + Point3d smallestPoint = null; + Point3d largestPoint = null; + for (MeshFacet facet: facets) { + int index = 0; + for (MeshPoint p: facet.getVertices()) { + if (vertices.containsKey(p)) { + vertices.get(p).facets.add(facet); + vertices.get(p).indices.add(index); + } else { + vertices.put(p, new AggregatedVertex(facet, index)); + Point3d point = p.getPosition(); + if (smallestPoint == null) { + smallestPoint = p.getPosition(); + } else { + smallestPoint = new Point3d( + Double.min(smallestPoint.x, point.x), + Double.min(smallestPoint.y, point.y), + Double.min(smallestPoint.z, point.z) + ); + } + if (largestPoint == null) { + largestPoint = p.getPosition(); + } else { + largestPoint = new Point3d( + Double.max(largestPoint.x, point.x), + Double.max(largestPoint.y, point.y), + Double.max(largestPoint.z, point.z) + ); + } + } + index++; + } + } + Point3d[] boundaries = updateBoundaries(smallestPoint, largestPoint); + smallestPoint = boundaries[0]; + largestPoint = boundaries[1]; + updateMinLen(smallestPoint, largestPoint); + root = buildTree(vertices, smallestPoint, largestPoint); + } + + /** + * Builds Octree. + * + * @param vertices List of aggregated sorted vertices + * @param smallestPoint boundary coordinate with x, y, z lower or equal + * than any other point in created OctNode + * @param largestPoint boundary coordinate with x, y, z higher or equal + * than any other point in created OctNode + * @return new node of the Octree + */ + private OctNode buildTree(HashMap<MeshPoint, AggregatedVertex> vertices, Point3d smallestPoint, Point3d largestPoint) { + if (vertices.isEmpty()) { + return new OctNode(smallestPoint, largestPoint); + } + + if (vertices.size() == 1) { + updateMinLen(smallestPoint, largestPoint); + Map.Entry<MeshPoint, AggregatedVertex> entry = vertices.entrySet().stream().findFirst().get(); + return new OctNode(entry.getValue().facets, entry.getValue().indices, smallestPoint, largestPoint); + } + + Point3d middlePoint = new Point3d( + (smallestPoint.x + largestPoint.x) / 2, + (smallestPoint.y + largestPoint.y) / 2, + (smallestPoint.z + largestPoint.z) / 2); + + List<HashMap<MeshPoint, AggregatedVertex>> octants = splitSpace(vertices, middlePoint); + double[] xCoors = {smallestPoint.x, middlePoint.x, largestPoint.x}; + double[] yCoors = {smallestPoint.y, middlePoint.y, largestPoint.y}; + double[] zCoors = {smallestPoint.z, middlePoint.z, largestPoint.z}; + List<OctNode> doneOctants = new ArrayList<>(); + for (int i = 0; i < 8; i++) { + HashMap<MeshPoint, AggregatedVertex> octant = octants.get(i); + Point3d newSmallestPoint = new Point3d( + xCoors[i >> 2], yCoors[i >> 1 & 1], zCoors[i & 1] + ); + Point3d newLargestPoint = new Point3d( + xCoors[(i >> 2) + 1], yCoors[(i >> 1 & 1) + 1], zCoors[(i & 1) + 1] + ); + doneOctants.add(buildTree(octant, newSmallestPoint, newLargestPoint)); + } + OctNode node = new OctNode(doneOctants, smallestPoint, largestPoint); + return node; + } + + /** + * Splits all points into 8 octants separated by 3 planes which all + * intersect in middlePoint + * + * @param vertices List of aggregated sorted vertices + * @param middlePoint middle point of space + * @return 8 octants containing all vertices in space + */ + private List<HashMap<MeshPoint, AggregatedVertex>> splitSpace(HashMap<MeshPoint, AggregatedVertex> vertices, Point3d middlePoint) { + List<HashMap<MeshPoint, AggregatedVertex>> octants = new ArrayList<>(); + for (int i = 0; i < 8; i++) { + octants.add(new HashMap<>()); + } + for (MeshPoint vertex : vertices.keySet()) { + int octantIndex = 0; + if (vertex.getPosition().x > middlePoint.x) { + octantIndex += 4; + } + if (vertex.getPosition().y > middlePoint.y) { + octantIndex += 2; + } + if (vertex.getPosition().z > middlePoint.z) { + octantIndex += 1; + } + octants.get(octantIndex).put(vertex, vertices.get(vertex)); + } + return octants; + } + + /*********************************************************** + * EMBEDDED CLASSES + ************************************************************/ + + /** + * Helper class used during the Octree creation to store mesh vertices + * with the same 3D location. + * + * @author Radek Oslejsek + */ + private class AggregatedVertex { + public final List<MeshFacet> facets = new ArrayList<>(); + public final List<Integer> indices = new ArrayList<>(); + + AggregatedVertex(MeshFacet f, int i) { + facets.add(f); + indices.add(i); + } + } +} diff --git a/MeshModel/src/test/java/cz/fidentis/analyst/mesh/octree/OctreeTest.java b/MeshModel/src/test/java/cz/fidentis/analyst/mesh/octree/OctreeTest.java new file mode 100644 index 0000000000000000000000000000000000000000..f1cbc79c7a934728fd5f5bb661a0231900fc3c1e --- /dev/null +++ b/MeshModel/src/test/java/cz/fidentis/analyst/mesh/octree/OctreeTest.java @@ -0,0 +1,274 @@ +package cz.fidentis.analyst.mesh.octree; + +import cz.fidentis.analyst.mesh.core.MeshFacet; +import cz.fidentis.analyst.mesh.core.MeshFacetImpl; +import cz.fidentis.analyst.mesh.core.MeshPoint; +import cz.fidentis.analyst.mesh.core.MeshPointImpl; +import cz.fidentis.analyst.octree.OctNode; +import cz.fidentis.analyst.octree.Octree; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; +import java.util.Set; +import javax.vecmath.Point3d; +import javax.vecmath.Vector3d; +import org.apache.commons.lang3.tuple.Pair; + + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.Test; + +/** + * @author Enkh-Undral EnkhBayar + */ +public class OctreeTest { + + private Point3d position = new Point3d(0.1f, 0.5f, 0.7f); + private Vector3d normalAndTextCoord = new Vector3d(0,0,0); + + private boolean isPointInCube(Point3d location, OctNode cube) { + Point3d smallBoundary = cube.getSmallBoundary(); + Point3d largeBoundary = cube.getLargeBoundary(); + if (!(smallBoundary.x <= location.x && location.x <= largeBoundary.x)) { + return false; + } + if (!(smallBoundary.y <= location.y && location.y <= largeBoundary.y)) { + return false; + } + return smallBoundary.z <= location.z && location.z <= largeBoundary.z; + } + + private boolean checkBoundaries(OctNode parent, OctNode child, int index) { + Point3d pSmallBoundary = parent.getSmallBoundary(); + Point3d pLargeBoundary = parent.getLargeBoundary(); + Point3d middlePoint = new Point3d( + (pSmallBoundary.x + pLargeBoundary.x) / 2, + (pSmallBoundary.y + pLargeBoundary.y) / 2, + (pSmallBoundary.z + pLargeBoundary.z) / 2); + Point3d[] coors = {pSmallBoundary, middlePoint, pLargeBoundary}; + Point3d correctSmall = new Point3d( + coors[index >> 2].x, coors[index >> 1 & 1].y, coors[index & 1].z + ); + Point3d correctLarge = new Point3d( + coors[(index >> 2) + 1].x, coors[(index >> 1 & 1) + 1].y, coors[(index & 1) + 1].z + ); + Point3d cSmallBoundary = child.getSmallBoundary(); + Point3d cLargeBoundary = child.getLargeBoundary(); + double[] childCoors = { + cSmallBoundary.x, cSmallBoundary.y, cSmallBoundary.z, + cLargeBoundary.x, cLargeBoundary.y, cLargeBoundary.z + }; + double[] correctCoors = { + correctSmall.x, correctSmall.y, correctSmall.z, + correctLarge.x, correctLarge.y, correctLarge.z + }; + for (int i = 0; i < 6; i++) { + if (childCoors[i] != correctCoors[i]) { + return false; + } + } + return true; + } + + private void testCorrectlyBuiltTree(Octree tree) { + if (tree.getRoot() == null) { + return; + } + int maxDepth = 0; + Queue<Pair<OctNode, Integer>> queue = new LinkedList<>(); + queue.add(Pair.of(tree.getRoot(), 0)); + + while (!queue.isEmpty()) { + Pair<OctNode, Integer> pair = queue.poll(); + final OctNode node = pair.getKey(); + final int depth = pair.getValue(); + maxDepth = Integer.max(maxDepth, depth); + assertTrue(node != null); + assertTrue(node.getSmallBoundary() != null, "Small boundary can't be null"); + assertTrue(node.getLargeBoundary() != null, "Large boundary can't be null"); + + if (node.isLeafNode()) { + MeshPoint location = node.getLocation(); + if (location != null) { + assertTrue(isPointInCube(location.getPosition(), node), "Point is not within boundaries"); + } + continue; + } + assertTrue(node.getLocation() == null, "Internal node with location"); + boolean allEmpty = true; + for (int i = 0; i < 8; i++) { + final OctNode child = node.getOctant(i); + queue.add(Pair.of(child, depth + 1)); + assertTrue(checkBoundaries(node, child, i), "Boundaries in node are wrong"); + if (!(child.isLeafNode() && child.getLocation() == null)) { + allEmpty = false; + } + } + assertFalse(allEmpty, "All child nodes in some octnode are empty"); + } + assertTrue(tree.getMinLen() != null); + Point3d small = tree.getRoot().getSmallBoundary(); + Point3d large = tree.getRoot().getLargeBoundary(); + double length = large.x - small.x; + length = Double.min(length, large.y - small.y); + length = Double.min(length, large.z - small.z); + assertTrue((length / Math.pow(2, maxDepth)) == tree.getMinLen(), "minLen is wrongly calculated"); + } + + private boolean containsPoint(OctNode subTree, MeshPoint p) { + if (subTree == null) { + return false; + } + if (subTree.isLeafNode()) { + return subTree.getLocation() != null && subTree.getLocation().equals(p); + } + int index = 0; + Point3d smallestPoint = subTree.getSmallBoundary(); + Point3d largestPoint = subTree.getLargeBoundary(); + Point3d middlePoint = new Point3d( + (smallestPoint.x + largestPoint.x) / 2, + (smallestPoint.y + largestPoint.y) / 2, + (smallestPoint.z + largestPoint.z) / 2); + if (p.getPosition().x > middlePoint.x) { + index += 4; + } + if (p.getPosition().y > middlePoint.y) { + index += 2; + } + if (p.getPosition().z > middlePoint.z) { + index += 1; + } + return containsPoint(subTree.getOctant(index), p); + } + + @Test + public void testPut(){ + MeshPoint p = new MeshPointImpl(position,normalAndTextCoord,normalAndTextCoord); + + List<MeshFacet> facets = new LinkedList<>(); + MeshFacet facet = new MeshFacetImpl(); + facet.addVertex(p); + facets.add(facet); + Octree tree = new Octree(facets); + testCorrectlyBuiltTree(tree); + + assertTrue(containsPoint(tree.getRoot(), p), "point put in octree is not there after init"); + } + + @Test + public void testCorrectBuild() { + final Set<MeshPoint> points = new HashSet<>(); + points.add(new MeshPointImpl(new Point3d(-331, 203, 320), normalAndTextCoord, normalAndTextCoord)); + points.add(new MeshPointImpl(new Point3d(-371, -222, -111), normalAndTextCoord, normalAndTextCoord)); + points.add(new MeshPointImpl(new Point3d(-223, 190, 113), normalAndTextCoord, normalAndTextCoord)); + points.add(new MeshPointImpl(new Point3d(275, -414, -378), normalAndTextCoord, normalAndTextCoord)); + points.add(new MeshPointImpl(new Point3d(-357, 98, -217), normalAndTextCoord, normalAndTextCoord)); + points.add(new MeshPointImpl(new Point3d(297, 403, 299), normalAndTextCoord, normalAndTextCoord)); + points.add(new MeshPointImpl(new Point3d(145, 252, -77), normalAndTextCoord, normalAndTextCoord)); + points.add(new MeshPointImpl(new Point3d(319, 13, 87), normalAndTextCoord, normalAndTextCoord)); + points.add(new MeshPointImpl(new Point3d(-284, 40, -231), normalAndTextCoord, normalAndTextCoord)); + final Octree tree = new Octree(points); + testCorrectlyBuiltTree(tree); + for (MeshPoint point : points) { + assertTrue(containsPoint(tree.getRoot(), point), "point put in octree is not there after init"); + } + } + + @Test + public void testCorrectBuildUnbalanced() { + final Set<MeshPoint> points = new HashSet<>(); + int depth = 8; + double start = 0; + double end = Math.pow(2, depth); + points.add(new MeshPointImpl(new Point3d(-end, -end, -end), normalAndTextCoord, normalAndTextCoord)); + points.add(new MeshPointImpl(new Point3d(end, end, end), normalAndTextCoord, normalAndTextCoord)); + for (int i = 0; i < depth - 1; i++) { + double coor = start + (end - start) / 4; + start += (end - start) / 2; + Point3d p = new Point3d(coor, coor, coor); + points.add(new MeshPointImpl(p, normalAndTextCoord, normalAndTextCoord)); + } + + final Octree tree = new Octree(points); + testCorrectlyBuiltTree(tree); + for (MeshPoint point : points) { + assertTrue(containsPoint(tree.getRoot(), point), "point put in octree is not there after init"); + } + } + + @Test + public void testCorrectBuildBalanced() { + final Set<MeshPoint> points = new HashSet<>(); + int depth = 4; + double end = Math.pow(2, depth); + points.add(new MeshPointImpl(new Point3d(-end, -end, -end), normalAndTextCoord, normalAndTextCoord)); + points.add(new MeshPointImpl(new Point3d(end, end, end), normalAndTextCoord, normalAndTextCoord)); + int[][] directions = { + {-1, -1, -1}, {-1, -1, 1}, {-1, 1, -1}, {-1, 1, 1}, + {1, -1, -1}, {1, -1, 1}, {1, 1, -1}, {1, 1, 1} + }; + for (double coor = 1; coor < end - 1; coor += 2) { + for (int[] direction : directions) { + Point3d p = new Point3d(coor * direction[0], coor * direction[1], coor * direction[2]); + points.add(new MeshPointImpl(p, normalAndTextCoord, normalAndTextCoord)); + } + } + double coor = end - 1; + for (int i = 1; i < 7; i++) { + int[] direction = directions[i]; + Point3d p = new Point3d(coor * direction[0], coor * direction[1], coor * direction[2]); + points.add(new MeshPointImpl(p, normalAndTextCoord, normalAndTextCoord)); + } + + final Octree tree = new Octree(points); + testCorrectlyBuiltTree(tree); + for (MeshPoint point : points) { + assertTrue(containsPoint(tree.getRoot(), point), "point put in octree is not there after init"); + } + } + + @Test + public void testPutNothing(){ + MeshPoint p = new MeshPointImpl(position,normalAndTextCoord,normalAndTextCoord); + + List<MeshFacet> facets = new LinkedList<>(); + Octree tree = new Octree(facets); + testCorrectlyBuiltTree(tree); + + assertFalse(containsPoint(tree.getRoot(), p)); + } + + @Test + public void testTwoFacets(){ + List<MeshFacet> facets = new LinkedList<>(); + + MeshFacet facet1 = new MeshFacetImpl(); + MeshFacet facet2 = new MeshFacetImpl(); + + List<MeshPoint> points = new LinkedList<>(); + Point3d positionOfPoints; + + for(int i = 0; i < 5; i++){ + positionOfPoints = new Point3d(0.1f * i, 0.5f * i, 0.7f * i); + MeshPoint point = new MeshPointImpl(positionOfPoints, normalAndTextCoord, normalAndTextCoord); + points.add(point); + facet1.addVertex(point); + } + for(int i = 5; i < 10; i++){ + positionOfPoints = new Point3d(0.1f * i, 0.5f * i, 0.7f * i); + MeshPoint point = new MeshPointImpl(positionOfPoints, normalAndTextCoord, normalAndTextCoord); + points.add(point); + facet2.addVertex(point); + } + facets.add(facet1); + facets.add(facet2); + Octree tree = new Octree(facets); + testCorrectlyBuiltTree(tree); + + for (MeshPoint point : points) { + assertTrue(containsPoint(tree.getRoot(), point), "point put in octree is not there after init"); + } + } +}