diff --git a/src/main/java/org/apache/commons/math4/geometry/euclidean/threed/PolyhedronsSet.java b/src/main/java/org/apache/commons/math4/geometry/euclidean/threed/PolyhedronsSet.java index ab2728359..cfd9f7ccd 100644 --- a/src/main/java/org/apache/commons/math4/geometry/euclidean/threed/PolyhedronsSet.java +++ b/src/main/java/org/apache/commons/math4/geometry/euclidean/threed/PolyhedronsSet.java @@ -512,7 +512,7 @@ public class PolyhedronsSet extends AbstractRegion { final Plane plane = (Plane) cut.getHyperplane(); // establish search order - final double offset = plane.getOffset((Point) point); + final double offset = plane.getOffset(point); final boolean in = FastMath.abs(offset) < getTolerance(); final BSPTree near; final BSPTree far; diff --git a/src/test/java/org/apache/commons/math4/geometry/GeometryTestUtils.java b/src/test/java/org/apache/commons/math4/geometry/GeometryTestUtils.java new file mode 100644 index 000000000..74f2bebc0 --- /dev/null +++ b/src/test/java/org/apache/commons/math4/geometry/GeometryTestUtils.java @@ -0,0 +1,324 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.math4.geometry; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.apache.commons.math3.util.FastMath; +import org.apache.commons.math4.geometry.euclidean.oned.Cartesian1D; +import org.apache.commons.math4.geometry.euclidean.oned.Euclidean1D; +import org.apache.commons.math4.geometry.euclidean.oned.IntervalsSet; +import org.apache.commons.math4.geometry.euclidean.oned.OrientedPoint; +import org.apache.commons.math4.geometry.euclidean.oned.SubOrientedPoint; +import org.apache.commons.math4.geometry.euclidean.oned.Vector1D; +import org.apache.commons.math4.geometry.euclidean.threed.Cartesian3D; +import org.apache.commons.math4.geometry.euclidean.threed.Euclidean3D; +import org.apache.commons.math4.geometry.euclidean.threed.Plane; +import org.apache.commons.math4.geometry.euclidean.threed.SubPlane; +import org.apache.commons.math4.geometry.euclidean.threed.Vector3D; +import org.apache.commons.math4.geometry.euclidean.twod.Cartesian2D; +import org.apache.commons.math4.geometry.euclidean.twod.Euclidean2D; +import org.apache.commons.math4.geometry.euclidean.twod.Line; +import org.apache.commons.math4.geometry.euclidean.twod.PolygonsSet; +import org.apache.commons.math4.geometry.euclidean.twod.SubLine; +import org.apache.commons.math4.geometry.euclidean.twod.Vector2D; +import org.apache.commons.math4.geometry.partitioning.BSPTree; +import org.apache.commons.math4.geometry.partitioning.BSPTreeVisitor; +import org.junit.Assert; + +/** Class containing various geometry-related test utilities. + * @since 4.0 + */ +public class GeometryTestUtils { + + /** Asserts that corresponding values in the given vectors are equal, using the specified + * tolerance value. + * @param expected + * @param actual + * @param tolerance + */ + public static void assertVectorEquals(Vector1D expected, Vector1D actual, double tolerance) { + String msg = "Expected vector to equal " + expected + " but was " + actual + ";"; + Assert.assertEquals(msg, expected.getX(), actual.getX(), tolerance); + } + + /** Asserts that corresponding values in the given vectors are equal, using the specified + * tolerance value. + * @param expected + * @param actual + * @param tolerance + */ + public static void assertVectorEquals(Vector2D expected, Vector2D actual, double tolerance) { + String msg = "Expected vector to equal " + expected + " but was " + actual + ";"; + Assert.assertEquals(msg, expected.getX(), actual.getX(), tolerance); + Assert.assertEquals(msg, expected.getY(), actual.getY(), tolerance); + } + + /** Asserts that corresponding values in the given vectors are equal, using the specified + * tolerance value. + * @param expected + * @param actual + * @param tolerance + */ + public static void assertVectorEquals(Vector3D expected, Vector3D actual, double tolerance) { + String msg = "Expected vector to equal " + expected + " but was " + actual + ";"; + Assert.assertEquals(msg, expected.getX(), actual.getX(), tolerance); + Assert.assertEquals(msg, expected.getY(), actual.getY(), tolerance); + Assert.assertEquals(msg, expected.getZ(), actual.getZ(), tolerance); + } + + /** Prints a string representation of the given 1D {@link BSPTree} to + * the console. This is intended for quick debugging of small trees. + * @param tree + */ + public static void printTree1D(BSPTree tree) { + TreePrinter1D printer = new TreePrinter1D(); + System.out.println(printer.writeAsString(tree)); + } + + /** Prints a string representation of the given 2D {@link BSPTree} to + * the console. This is intended for quick debugging of small trees. + * @param tree + */ + public static void printTree2D(BSPTree tree) { + TreePrinter2D printer = new TreePrinter2D(); + System.out.println(printer.writeAsString(tree)); + } + + /** Prints a string representation of the given 3D {@link BSPTree} to + * the console. This is intended for quick debugging of small trees. + * @param tree + */ + public static void printTree3D(BSPTree tree) { + TreePrinter3D printer = new TreePrinter3D(); + System.out.println(printer.writeAsString(tree)); + } + + /** + * Base for classes that create string representations of {@link BSPTree}s. + * @param + */ + public static abstract class TreePrinter implements BSPTreeVisitor { + + /** Indent per tree level */ + protected static final String INDENT = " "; + + /** Current depth in the tree */ + protected int depth; + + /** Contains the string output */ + protected StringBuilder output = new StringBuilder(); + + /** Returns a string representation of the given {@link BSPTree}. + * @param tree + * @return + */ + public String writeAsString(BSPTree tree) { + output.delete(0, output.length()); + + tree.visit(this); + + return output.toString(); + } + + /** {@inheritDoc} */ + @Override + public Order visitOrder(BSPTree node) { + return Order.SUB_MINUS_PLUS; + } + + /** {@inheritDoc} */ + @Override + public void visitInternalNode(BSPTree node) { + writeLinePrefix(node); + writeInternalNode(node); + + write("\n"); + + ++depth; + } + + /** {@inheritDoc} */ + @Override + public void visitLeafNode(BSPTree node) { + writeLinePrefix(node); + writeLeafNode(node); + + write("\n"); + + BSPTree cur = node; + while (cur.getParent() != null && cur.getParent().getPlus() == cur) { + --depth; + cur = cur.getParent(); + } + } + + /** Writes the prefix for the current line in the output. This includes + * the line indent, the plus/minus node indicator, and a string identifier + * for the node itself. + * @param node + */ + protected void writeLinePrefix(BSPTree node) { + for (int i=0; i node); + + /** Writes a leaf node. The default implementation here simply writes + * the node attribute as a string. + * @param node + */ + protected void writeLeafNode(BSPTree node) { + write(String.valueOf(node.getAttribute())); + } + } + + /** Class for creating string representations of 1D {@link BSPTree}s. + */ + public static class TreePrinter1D extends TreePrinter { + + /** {@inheritDoc} */ + @Override + protected void writeInternalNode(BSPTree node) { + SubOrientedPoint cut = (SubOrientedPoint) node.getCut(); + + OrientedPoint hyper = (OrientedPoint) cut.getHyperplane(); + write("cut = { hyperplane: "); + if (hyper.isDirect()) { + write("[" + hyper.getLocation().getX() + ", inf)"); + } + else { + write("(-inf, " + hyper.getLocation().getX() + "]"); + } + + IntervalsSet remainingRegion = (IntervalsSet) cut.getRemainingRegion(); + if (remainingRegion != null) { + write(", remainingRegion: ["); + + boolean isFirst = true; + for (double[] interval : remainingRegion) { + if (isFirst) { + isFirst = false; + } + else { + write(", "); + } + write(Arrays.toString(interval)); + } + + write("]"); + } + + write("}"); + } + } + + /** Class for creating string representations of 2D {@link BSPTree}s. + */ + public static class TreePrinter2D extends TreePrinter { + + /** {@inheritDoc} */ + @Override + protected void writeInternalNode(BSPTree node) { + SubLine cut = (SubLine) node.getCut(); + Line line = (Line) cut.getHyperplane(); + IntervalsSet remainingRegion = (IntervalsSet) cut.getRemainingRegion(); + + write("cut = { angle: " + FastMath.toDegrees(line.getAngle()) + ", origin: " + line.toSpace(Cartesian1D.ZERO) + "}"); + write(", remainingRegion: ["); + + boolean isFirst = true; + for (double[] interval : remainingRegion) { + if (isFirst) { + isFirst = false; + } + else { + write(", "); + } + write(Arrays.toString(interval)); + } + + write("]"); + } + } + + /** Class for creating string representations of 3D {@link BSPTree}s. + */ + public static class TreePrinter3D extends TreePrinter { + + /** {@inheritDoc} */ + @Override + protected void writeInternalNode(BSPTree node) { + SubPlane cut = (SubPlane) node.getCut(); + Plane plane = (Plane) cut.getHyperplane(); + PolygonsSet polygon = (PolygonsSet) cut.getRemainingRegion(); + + write("cut = { normal: " + plane.getNormal() + ", origin: " + plane.getOrigin() + "}"); + write(", remainingRegion = ["); + + boolean isFirst = true; + for (Cartesian2D[] loop : polygon.getVertices()) { + // convert to 3-space for easier debugging + List loop3 = new ArrayList<>(); + for (Cartesian2D vertex : loop) { + if (vertex != null) { + loop3.add(plane.toSpace(vertex)); + } + else { + loop3.add(null); + } + } + + if (isFirst) { + isFirst = false; + } + else { + write(", "); + } + + write(loop3.toString()); + } + + write("]"); + } + } +} diff --git a/src/test/java/org/apache/commons/math4/geometry/euclidean/oned/Cartesian1DTest.java b/src/test/java/org/apache/commons/math4/geometry/euclidean/oned/Cartesian1DTest.java index a01c9ef8f..ebac5256c 100644 --- a/src/test/java/org/apache/commons/math4/geometry/euclidean/oned/Cartesian1DTest.java +++ b/src/test/java/org/apache/commons/math4/geometry/euclidean/oned/Cartesian1DTest.java @@ -22,165 +22,140 @@ import java.text.DecimalFormatSymbols; import java.text.NumberFormat; import java.util.Locale; -import org.apache.commons.math4.exception.DimensionMismatchException; import org.apache.commons.math4.exception.MathArithmeticException; +import org.apache.commons.math4.geometry.Point; import org.apache.commons.math4.geometry.Space; +import org.apache.commons.math4.geometry.Vector; import org.apache.commons.math4.util.FastMath; import org.apache.commons.numbers.core.Precision; import org.junit.Assert; import org.junit.Test; public class Cartesian1DTest { + + private static final double TEST_TOLERANCE = 1e-15; + @Test - public void testConstructors() throws DimensionMismatchException { - checkVector(new Cartesian1D(3, new Cartesian1D(FastMath.PI / 3)), - FastMath.PI); - checkVector(new Cartesian1D(2, Cartesian1D.ONE, -3, new Cartesian1D(2)), - -4); - checkVector(new Cartesian1D(2, Cartesian1D.ONE, - 5, new Cartesian1D(2), - -3, new Cartesian1D(3)), - 3); - checkVector(new Cartesian1D(2, Cartesian1D.ONE, - 5, new Cartesian1D(2), - 5, new Cartesian1D(-2), - -3, new Cartesian1D(-3)), - 11); + public void testConstants() { + // act/assert + checkVector(Cartesian1D.ZERO, 0.0); + checkVector(Cartesian1D.ONE, 1.0); + checkVector(Cartesian1D.NaN, Double.NaN); + checkVector(Cartesian1D.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY); + checkVector(Cartesian1D.POSITIVE_INFINITY, Double.POSITIVE_INFINITY); + } + + @Test + public void testConstructor_simple() { + // act/assert + checkVector(new Cartesian1D(2), 2); + checkVector(new Cartesian1D(-2), -2); + checkVector(new Cartesian1D(FastMath.PI), FastMath.PI); + } + + @Test + public void testConstructor_multiplicative() { + // act/assert + checkVector(new Cartesian1D(2, new Cartesian1D(3)), 6); + checkVector(new Cartesian1D(-2, new Cartesian1D(3)), -6); + } + + @Test + public void testConstructor_linear2() { + // act/assert + checkVector(new Cartesian1D( + 2, new Cartesian1D(3), + 5, new Cartesian1D(7)), 41); + checkVector(new Cartesian1D( + 2, new Cartesian1D(3), + -5, new Cartesian1D(7)),-29); + } + + @Test + public void testConstructor_linear3() { + // act/assert + checkVector(new Cartesian1D( + 2, new Cartesian1D(3), + 5, new Cartesian1D(7), + 11, new Cartesian1D(13)), 184); + checkVector(new Cartesian1D( + 2, new Cartesian1D(3), + 5, new Cartesian1D(7), + -11, new Cartesian1D(13)), -102); + } + + @Test + public void testConstructor_linear4() { + // act/assert + checkVector(new Cartesian1D( + 2, new Cartesian1D(3), + 5, new Cartesian1D(7), + 11, new Cartesian1D(13), + 17, new Cartesian1D(19)), 507); + checkVector(new Cartesian1D( + 2, new Cartesian1D(3), + 5, new Cartesian1D(7), + 11, new Cartesian1D(13), + -17, new Cartesian1D(19)), -139); } @Test public void testSpace() { + // act Space space = new Cartesian1D(1).getSpace(); + + // assert Assert.assertEquals(1, space.getDimension()); } @Test public void testZero() { - Assert.assertEquals(0, new Cartesian1D(1).getZero().getNorm(), 1.0e-15); - } + // act + Cartesian1D zero = new Cartesian1D(1).getZero(); - @Test - public void testEquals() { - Cartesian1D u1 = new Cartesian1D(1); - Cartesian1D u2 = new Cartesian1D(1); - Assert.assertTrue(u1.equals(u1)); - Assert.assertTrue(u1.equals(u2)); - Assert.assertFalse(u1.equals(new Cartesian1D(1 + 10 * Precision.EPSILON))); - Assert.assertTrue(new Cartesian1D(Double.NaN).equals(new Cartesian1D(Double.NaN))); - } - - @Test - public void testHash() { - Assert.assertEquals(new Cartesian1D(Double.NaN).hashCode(), new Cartesian1D(Double.NaN).hashCode()); - Cartesian1D u = new Cartesian1D(1); - Cartesian1D v = new Cartesian1D(1 + 10 * Precision.EPSILON); - Assert.assertTrue(u.hashCode() != v.hashCode()); - } - - @Test - public void testInfinite() { - Assert.assertTrue(new Cartesian1D(Double.NEGATIVE_INFINITY).isInfinite()); - Assert.assertTrue(new Cartesian1D(Double.POSITIVE_INFINITY).isInfinite()); - Assert.assertFalse(new Cartesian1D(1).isInfinite()); - Assert.assertFalse(new Cartesian1D(Double.NaN).isInfinite()); - } - - @Test - public void testNaN() { - Assert.assertTrue(new Cartesian1D(Double.NaN).isNaN()); - Assert.assertFalse(new Cartesian1D(1).isNaN()); - Assert.assertFalse(new Cartesian1D(Double.NEGATIVE_INFINITY).isNaN()); - } - - @Test - public void testToString() { - Assert.assertEquals("{3}", new Cartesian1D(3).toString()); - NumberFormat format = new DecimalFormat("0.000", new DecimalFormatSymbols(Locale.US)); - Assert.assertEquals("{3.000}", new Cartesian1D(3).toString(format)); - } - - @Test - public void testCoordinates() { - Cartesian1D v = new Cartesian1D(1); - Assert.assertTrue(FastMath.abs(v.getX() - 1) < 1.0e-12); + // assert + Assert.assertEquals(0, zero.getX(), TEST_TOLERANCE); } @Test public void testNorm1() { - Assert.assertEquals(0.0, Cartesian1D.ZERO.getNorm1(), 0); - Assert.assertEquals(6.0, new Cartesian1D(6).getNorm1(), 0); + // act/assert + Assert.assertEquals(0.0, Cartesian1D.ZERO.getNorm1(), TEST_TOLERANCE); + Assert.assertEquals(6.0, new Cartesian1D(6).getNorm1(), TEST_TOLERANCE); + Assert.assertEquals(6.0, new Cartesian1D(-6).getNorm1(), TEST_TOLERANCE); } @Test public void testNorm() { - Assert.assertEquals(0.0, Cartesian1D.ZERO.getNorm(), 0); - Assert.assertEquals(3.0, new Cartesian1D(-3).getNorm(), 1.0e-12); + // act/assert + Assert.assertEquals(0.0, Cartesian1D.ZERO.getNorm(), TEST_TOLERANCE); + Assert.assertEquals(3.0, new Cartesian1D(3).getNorm(), TEST_TOLERANCE); + Assert.assertEquals(3.0, new Cartesian1D(-3).getNorm(), TEST_TOLERANCE); } @Test public void testNormSq() { - Assert.assertEquals(0.0, new Cartesian1D(0).getNormSq(), 0); - Assert.assertEquals(9.0, new Cartesian1D(-3).getNormSq(), 1.0e-12); + // act/assert + Assert.assertEquals(0.0, new Cartesian1D(0).getNormSq(), TEST_TOLERANCE); + Assert.assertEquals(9.0, new Cartesian1D(3).getNormSq(), TEST_TOLERANCE); + Assert.assertEquals(9.0, new Cartesian1D(-3).getNormSq(), TEST_TOLERANCE); } @Test public void testNormInf() { - Assert.assertEquals(0.0, Cartesian1D.ZERO.getNormInf(), 0); - Assert.assertEquals(3.0, new Cartesian1D(-3).getNormInf(), 0); - } - - @Test - public void testDistance1() { - Cartesian1D v1 = new Cartesian1D(1); - Cartesian1D v2 = new Cartesian1D(-4); - Assert.assertEquals(0.0, new Cartesian1D(-1).distance1(new Cartesian1D(-1)), 0); - Assert.assertEquals(5.0, v1.distance1(v2), 1.0e-12); - Assert.assertEquals(v1.subtract(v2).getNorm1(), v1.distance1(v2), 1.0e-12); - } - - @Test - public void testDistance() { - Cartesian1D v1 = new Cartesian1D(1); - Cartesian1D v2 = new Cartesian1D(-4); - Assert.assertEquals(0.0, Cartesian1D.distance(new Cartesian1D(-1), new Cartesian1D(-1)), 0); - Assert.assertEquals(5.0, Cartesian1D.distance(v1, v2), 1.0e-12); - Assert.assertEquals(v1.subtract(v2).getNorm(), Cartesian1D.distance(v1, v2), 1.0e-12); - } - - @Test - public void testDistanceSq() { - Cartesian1D v1 = new Cartesian1D(1); - Cartesian1D v2 = new Cartesian1D(-4); - Assert.assertEquals(0.0, Cartesian1D.distanceSq(new Cartesian1D(-1), new Cartesian1D(-1)), 0); - Assert.assertEquals(25.0, Cartesian1D.distanceSq(v1, v2), 1.0e-12); - Assert.assertEquals(Cartesian1D.distance(v1, v2) * Cartesian1D.distance(v1, v2), - Cartesian1D.distanceSq(v1, v2), 1.0e-12); - } - - @Test - public void testDistanceInf() { - Cartesian1D v1 = new Cartesian1D(1); - Cartesian1D v2 = new Cartesian1D(-4); - Assert.assertEquals(0.0, Cartesian1D.distanceInf(new Cartesian1D(-1), new Cartesian1D(-1)), 0); - Assert.assertEquals(5.0, Cartesian1D.distanceInf(v1, v2), 1.0e-12); - Assert.assertEquals(v1.subtract(v2).getNormInf(), Cartesian1D.distanceInf(v1, v2), 1.0e-12); - } - - @Test - public void testSubtract() { - Cartesian1D v1 = new Cartesian1D(1); - Cartesian1D v2 = new Cartesian1D(-3); - v1 = v1.subtract(v2); - checkVector(v1, 4); - - checkVector(v2.subtract(v1), -7); - checkVector(v2.subtract(3, v1), -15); + // act/assert + Assert.assertEquals(0.0, Cartesian1D.ZERO.getNormInf(), TEST_TOLERANCE); + Assert.assertEquals(3.0, new Cartesian1D(3).getNormInf(), TEST_TOLERANCE); + Assert.assertEquals(3.0, new Cartesian1D(-3).getNormInf(), TEST_TOLERANCE); } @Test public void testAdd() { + // arrange Cartesian1D v1 = new Cartesian1D(1); Cartesian1D v2 = new Cartesian1D(-3); + + // act/assert v1 = v1.add(v2); checkVector(v1, -2); @@ -189,31 +164,224 @@ public class Cartesian1DTest { } @Test - public void testScalarProduct() { - Cartesian1D v = new Cartesian1D(1); - v = v.scalarMultiply(3); - checkVector(v, 3); + public void testSubtract() { + // arrange + Cartesian1D v1 = new Cartesian1D(1); + Cartesian1D v2 = new Cartesian1D(-3); - checkVector(v.scalarMultiply(0.5), 1.5); + // act/assert + v1 = v1.subtract(v2); + checkVector(v1, 4); + + checkVector(v2.subtract(v1), -7); + checkVector(v2.subtract(3, v1), -15); } @Test - public void testNormalize() throws MathArithmeticException { - Assert.assertEquals(1.0, new Cartesian1D(5).normalize().getNorm(), 1.0e-12); - try { - Cartesian1D.ZERO.normalize(); - Assert.fail("an exception should have been thrown"); - } catch (MathArithmeticException ae) { - // expected behavior - } + public void testNormalize() { + // act/assert + checkVector(new Cartesian1D(1).normalize(), 1); + checkVector(new Cartesian1D(-1).normalize(), -1); + checkVector(new Cartesian1D(5).normalize(), 1); + checkVector(new Cartesian1D(-5).normalize(), -1); + } + + @Test(expected = MathArithmeticException.class) + public void testNormalize_zeroNorm() { + // act + Cartesian1D.ZERO.normalize(); } @Test public void testNegate() { + // act/assert checkVector(new Cartesian1D(0.1).negate(), -0.1); + checkVector(new Cartesian1D(-0.1).negate(), 0.1); + } + + @Test + public void testScalarMultiply() { + // act/assert + checkVector(new Cartesian1D(1).scalarMultiply(3), 3); + checkVector(new Cartesian1D(1).scalarMultiply(-3), -3); + + checkVector(new Cartesian1D(1.5).scalarMultiply(7), 10.5); + checkVector(new Cartesian1D(-1.5).scalarMultiply(7), -10.5); + } + + @Test + public void testNaN() { + // act/assert + Assert.assertTrue(new Cartesian1D(Double.NaN).isNaN()); + Assert.assertFalse(new Cartesian1D(1).isNaN()); + Assert.assertFalse(new Cartesian1D(Double.NEGATIVE_INFINITY).isNaN()); + } + + @Test + public void testInfinite() { + // act/assert + Assert.assertTrue(new Cartesian1D(Double.NEGATIVE_INFINITY).isInfinite()); + Assert.assertTrue(new Cartesian1D(Double.POSITIVE_INFINITY).isInfinite()); + Assert.assertFalse(new Cartesian1D(1).isInfinite()); + Assert.assertFalse(new Cartesian1D(Double.NaN).isInfinite()); + } + + @Test + public void testDistance1() { + // arrange + Cartesian1D v1 = new Cartesian1D(1); + Cartesian1D v2 = new Cartesian1D(-4); + + // act/assert + Assert.assertEquals(0.0, v1.distance1(v1), TEST_TOLERANCE); + + Assert.assertEquals(5.0, v1.distance1(v2), TEST_TOLERANCE); + Assert.assertEquals(v1.subtract(v2).getNorm1(), v1.distance1(v2), TEST_TOLERANCE); + + Assert.assertEquals(0.0, new Cartesian1D(-1).distance1(new Cartesian1D(-1)), TEST_TOLERANCE); + } + + @Test + public void testDistance() { + // arrange + Cartesian1D v1 = new Cartesian1D(1); + Cartesian1D v2 = new Cartesian1D(-4); + + // act/assert + Assert.assertEquals(0.0, v1.distance(v1), TEST_TOLERANCE); + + Assert.assertEquals(5.0, v1.distance(v2), TEST_TOLERANCE); + Assert.assertEquals(5.0, v1.distance((Point) v2), TEST_TOLERANCE); + Assert.assertEquals(5.0, v1.distance((Vector) v2), TEST_TOLERANCE); + Assert.assertEquals(v1.subtract(v2).getNorm(), v1.distance(v2), TEST_TOLERANCE); + + Assert.assertEquals(0.0, new Cartesian1D(-1).distance(new Cartesian1D(-1)), TEST_TOLERANCE); + } + + @Test + public void testDistance_static() { + // arrange + Cartesian1D v1 = new Cartesian1D(1); + Cartesian1D v2 = new Cartesian1D(-4); + + // act/assert + Assert.assertEquals(0.0, Cartesian1D.distance(v1, v1), TEST_TOLERANCE); + + Assert.assertEquals(5.0, Cartesian1D.distance(v1, v2), TEST_TOLERANCE); + Assert.assertEquals(v1.subtract(v2).getNorm(), Cartesian1D.distance(v1, v2), TEST_TOLERANCE); + + Assert.assertEquals(0.0, Cartesian1D.distance(new Cartesian1D(-1), new Cartesian1D(-1)), TEST_TOLERANCE); + } + + @Test + public void testDistanceInf() { + // arrange + Cartesian1D v1 = new Cartesian1D(1); + Cartesian1D v2 = new Cartesian1D(-4); + + // act/assert + Assert.assertEquals(0.0, new Cartesian1D(-1).distanceInf(new Cartesian1D(-1)), TEST_TOLERANCE); + Assert.assertEquals(5.0, v1.distanceInf(v2), TEST_TOLERANCE); + + Assert.assertEquals(v1.subtract(v2).getNormInf(), v1.distanceInf(v2), TEST_TOLERANCE); + } + + @Test + public void testDistanceInf_static() { + // arrange + Cartesian1D v1 = new Cartesian1D(1); + Cartesian1D v2 = new Cartesian1D(-4); + + // act/assert + Assert.assertEquals(0.0, Cartesian1D.distanceInf(new Cartesian1D(-1), new Cartesian1D(-1)), TEST_TOLERANCE); + Assert.assertEquals(5.0, Cartesian1D.distanceInf(v1, v2), TEST_TOLERANCE); + + Assert.assertEquals(v1.subtract(v2).getNormInf(), Cartesian1D.distanceInf(v1, v2), TEST_TOLERANCE); + } + + @Test + public void testDistanceSq() { + // arrange + Cartesian1D v1 = new Cartesian1D(1); + Cartesian1D v2 = new Cartesian1D(-4); + + // act/assert + Assert.assertEquals(0.0, new Cartesian1D(-1).distanceSq(new Cartesian1D(-1)), TEST_TOLERANCE); + Assert.assertEquals(25.0, v1.distanceSq(v2), TEST_TOLERANCE); + + Assert.assertEquals(Cartesian1D.distance(v1, v2) * Cartesian1D.distance(v1, v2), + v1.distanceSq(v2), TEST_TOLERANCE); + } + + @Test + public void testDistanceSq_static() { + // arrange + Cartesian1D v1 = new Cartesian1D(1); + Cartesian1D v2 = new Cartesian1D(-4); + + // act/assert + Assert.assertEquals(0.0, Cartesian1D.distanceSq(new Cartesian1D(-1), new Cartesian1D(-1)), TEST_TOLERANCE); + Assert.assertEquals(25.0, Cartesian1D.distanceSq(v1, v2), TEST_TOLERANCE); + + Assert.assertEquals(Cartesian1D.distance(v1, v2) * Cartesian1D.distance(v1, v2), + Cartesian1D.distanceSq(v1, v2), TEST_TOLERANCE); + } + + @Test + public void testDotProduct() { + // act/assert + Assert.assertEquals(6.0, new Cartesian1D(2).dotProduct(new Cartesian1D(3)), TEST_TOLERANCE); + Assert.assertEquals(-6.0, new Cartesian1D(2).dotProduct(new Cartesian1D(-3)), TEST_TOLERANCE); + } + + @Test + public void testEquals() { + // arrange + Cartesian1D u1 = new Cartesian1D(1); + Cartesian1D u2 = new Cartesian1D(1); + + // act/assert + Assert.assertFalse(u1.equals(null)); + Assert.assertFalse(u1.equals(new Object())); + + Assert.assertTrue(u1.equals(u1)); + Assert.assertTrue(u1.equals(u2)); + + Assert.assertFalse(u1.equals(new Cartesian1D(-1))); + Assert.assertFalse(u1.equals(new Cartesian1D(1 + 10 * Precision.EPSILON))); + + Assert.assertTrue(new Cartesian1D(Double.NaN).equals(new Cartesian1D(Double.NaN))); + } + + @Test + public void testHash() { + // arrange + Cartesian1D u = new Cartesian1D(1); + Cartesian1D v = new Cartesian1D(1 + 10 * Precision.EPSILON); + + // act/assert + Assert.assertTrue(u.hashCode() != v.hashCode()); + Assert.assertEquals(new Cartesian1D(Double.NaN).hashCode(), new Cartesian1D(Double.NaN).hashCode()); + } + + @Test + public void testToString() { + // act/assert + Assert.assertEquals("{3}", new Cartesian1D(3).toString()); + Assert.assertEquals("{-3}", new Cartesian1D(-3).toString()); + } + + @Test + public void testToString_numberFormat() { + // arrange + NumberFormat format = new DecimalFormat("0.000", new DecimalFormatSymbols(Locale.US)); + + // act/assert + Assert.assertEquals("{-1.000}", new Cartesian1D(-1).toString(format)); + Assert.assertEquals("{3.142}", new Cartesian1D(FastMath.PI).toString(format)); } private void checkVector(Cartesian1D v, double x) { - Assert.assertEquals(x, v.getX(), 1.0e-12); + Assert.assertEquals(x, v.getX(), TEST_TOLERANCE); } } diff --git a/src/test/java/org/apache/commons/math4/geometry/euclidean/oned/IntervalTest.java b/src/test/java/org/apache/commons/math4/geometry/euclidean/oned/IntervalTest.java index e4a88d356..89e8590a3 100644 --- a/src/test/java/org/apache/commons/math4/geometry/euclidean/oned/IntervalTest.java +++ b/src/test/java/org/apache/commons/math4/geometry/euclidean/oned/IntervalTest.java @@ -26,56 +26,158 @@ import org.junit.Test; public class IntervalTest { + private static final double TEST_TOLERANCE = 1e-10; + @Test - public void testInterval() { + public void testBasicProperties() { + // arrange Interval interval = new Interval(2.3, 5.7); - Assert.assertEquals(3.4, interval.getSize(), 1.0e-10); - Assert.assertEquals(4.0, interval.getBarycenter(), 1.0e-10); - Assert.assertEquals(Region.Location.BOUNDARY, interval.checkPoint(2.3, 1.0e-10)); - Assert.assertEquals(Region.Location.BOUNDARY, interval.checkPoint(5.7, 1.0e-10)); - Assert.assertEquals(Region.Location.OUTSIDE, interval.checkPoint(1.2, 1.0e-10)); - Assert.assertEquals(Region.Location.OUTSIDE, interval.checkPoint(8.7, 1.0e-10)); - Assert.assertEquals(Region.Location.INSIDE, interval.checkPoint(3.0, 1.0e-10)); - Assert.assertEquals(2.3, interval.getInf(), 1.0e-10); - Assert.assertEquals(5.7, interval.getSup(), 1.0e-10); + + // act/assert + Assert.assertEquals(3.4, interval.getSize(), TEST_TOLERANCE); + Assert.assertEquals(4.0, interval.getBarycenter(), TEST_TOLERANCE); + Assert.assertEquals(2.3, interval.getInf(), TEST_TOLERANCE); + Assert.assertEquals(5.7, interval.getSup(), TEST_TOLERANCE); } @Test - public void testTolerance() { - Interval interval = new Interval(2.3, 5.7); - Assert.assertEquals(Region.Location.OUTSIDE, interval.checkPoint(1.2, 1.0)); - Assert.assertEquals(Region.Location.BOUNDARY, interval.checkPoint(1.2, 1.2)); - Assert.assertEquals(Region.Location.OUTSIDE, interval.checkPoint(8.7, 2.9)); - Assert.assertEquals(Region.Location.BOUNDARY, interval.checkPoint(8.7, 3.1)); - Assert.assertEquals(Region.Location.INSIDE, interval.checkPoint(3.0, 0.6)); - Assert.assertEquals(Region.Location.BOUNDARY, interval.checkPoint(3.0, 0.8)); + public void testBasicProperties_negativeValues() { + // arrange + Interval interval = new Interval(-5.7, -2.3); + + // act/assert + Assert.assertEquals(3.4, interval.getSize(), TEST_TOLERANCE); + Assert.assertEquals(-4.0, interval.getBarycenter(), TEST_TOLERANCE); + Assert.assertEquals(-5.7, interval.getInf(), TEST_TOLERANCE); + Assert.assertEquals(-2.3, interval.getSup(), TEST_TOLERANCE); + } + + // MATH-1256 + @Test(expected = NumberIsTooSmallException.class) + public void testStrictOrdering() { + new Interval(0, -1); } @Test - public void testInfinite() { - Interval interval = new Interval(9.0, Double.POSITIVE_INFINITY); - Assert.assertEquals(Region.Location.BOUNDARY, interval.checkPoint(9.0, 1.0e-10)); - Assert.assertEquals(Region.Location.OUTSIDE, interval.checkPoint(8.4, 1.0e-10)); + public void testCheckPoint() { + // arrange + Interval interval = new Interval(2.3, 5.7); + + // act/assert + Assert.assertEquals(Region.Location.OUTSIDE, interval.checkPoint(1.2, TEST_TOLERANCE)); + + Assert.assertEquals(Region.Location.OUTSIDE, interval.checkPoint(2.2, TEST_TOLERANCE)); + Assert.assertEquals(Region.Location.BOUNDARY, interval.checkPoint(2.3, TEST_TOLERANCE)); + Assert.assertEquals(Region.Location.INSIDE, interval.checkPoint(2.4, TEST_TOLERANCE)); + + Assert.assertEquals(Region.Location.INSIDE, interval.checkPoint(3.0, TEST_TOLERANCE)); + + Assert.assertEquals(Region.Location.INSIDE, interval.checkPoint(5.6, TEST_TOLERANCE)); + Assert.assertEquals(Region.Location.BOUNDARY, interval.checkPoint(5.7, TEST_TOLERANCE)); + Assert.assertEquals(Region.Location.OUTSIDE, interval.checkPoint(5.8, TEST_TOLERANCE)); + + Assert.assertEquals(Region.Location.OUTSIDE, interval.checkPoint(8.7, TEST_TOLERANCE)); + + Assert.assertEquals(Region.Location.OUTSIDE, interval.checkPoint(Double.NEGATIVE_INFINITY, TEST_TOLERANCE)); + Assert.assertEquals(Region.Location.OUTSIDE, interval.checkPoint(Double.POSITIVE_INFINITY, TEST_TOLERANCE)); + Assert.assertEquals(Region.Location.BOUNDARY, interval.checkPoint(Double.NaN, TEST_TOLERANCE)); + } + + @Test + public void testCheckPoint_tolerance() { + // arrange + Interval interval = new Interval(2.3, 5.7); + + // act/assert + Assert.assertEquals(Region.Location.OUTSIDE, interval.checkPoint(2.29, 1e-3)); + Assert.assertEquals(Region.Location.BOUNDARY, interval.checkPoint(2.29, 1e-2)); + Assert.assertEquals(Region.Location.BOUNDARY, interval.checkPoint(2.29, 1e-1)); + Assert.assertEquals(Region.Location.BOUNDARY, interval.checkPoint(2.29, 1)); + Assert.assertEquals(Region.Location.BOUNDARY, interval.checkPoint(2.29, 2)); + + Assert.assertEquals(Region.Location.INSIDE, interval.checkPoint(4.0, 1e-3)); + Assert.assertEquals(Region.Location.INSIDE, interval.checkPoint(4.0, 1e-2)); + Assert.assertEquals(Region.Location.INSIDE, interval.checkPoint(4.0, 1e-1)); + Assert.assertEquals(Region.Location.INSIDE, interval.checkPoint(4.0, 1)); + Assert.assertEquals(Region.Location.BOUNDARY, interval.checkPoint(4.0, 2)); + } + + @Test + public void testInfinite_inf() { + // act + Interval interval = new Interval(Double.NEGATIVE_INFINITY, 9); + + // assert + Assert.assertEquals(Region.Location.BOUNDARY, interval.checkPoint(9.0, TEST_TOLERANCE)); + Assert.assertEquals(Region.Location.OUTSIDE, interval.checkPoint(9.4, TEST_TOLERANCE)); for (double e = 1.0; e <= 6.0; e += 1.0) { Assert.assertEquals(Region.Location.INSIDE, - interval.checkPoint(FastMath.pow(10.0, e), 1.0e-10)); + interval.checkPoint(-1 * FastMath.pow(10.0, e), TEST_TOLERANCE)); } - Assert.assertTrue(Double.isInfinite(interval.getSize())); - Assert.assertEquals(9.0, interval.getInf(), 1.0e-10); - Assert.assertTrue(Double.isInfinite(interval.getSup())); + Assert.assertEquals(Double.POSITIVE_INFINITY, interval.getSize(), TEST_TOLERANCE); + Assert.assertEquals(Double.NEGATIVE_INFINITY, interval.getInf(), TEST_TOLERANCE); + Assert.assertEquals(9.0, interval.getSup(), TEST_TOLERANCE); + } + @Test + public void testInfinite_sup() { + // act + Interval interval = new Interval(9.0, Double.POSITIVE_INFINITY); + + // assert + Assert.assertEquals(Region.Location.BOUNDARY, interval.checkPoint(9.0, TEST_TOLERANCE)); + Assert.assertEquals(Region.Location.OUTSIDE, interval.checkPoint(8.4, TEST_TOLERANCE)); + for (double e = 1.0; e <= 6.0; e += 1.0) { + Assert.assertEquals(Region.Location.INSIDE, + interval.checkPoint(FastMath.pow(10.0, e), TEST_TOLERANCE)); + } + Assert.assertEquals(Double.POSITIVE_INFINITY, interval.getSize(), TEST_TOLERANCE); + Assert.assertEquals(9.0, interval.getInf(), TEST_TOLERANCE); + Assert.assertEquals(Double.POSITIVE_INFINITY, interval.getSup(), TEST_TOLERANCE); + } + + @Test + public void testInfinite_infAndSup() { + // act + Interval interval = new Interval(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY); + + // assert + for (double e = 1.0; e <= 6.0; e += 1.0) { + Assert.assertEquals(Region.Location.INSIDE, + interval.checkPoint(FastMath.pow(10.0, e), TEST_TOLERANCE)); + } + Assert.assertEquals(Double.POSITIVE_INFINITY, interval.getSize(), TEST_TOLERANCE); + Assert.assertEquals(Double.NEGATIVE_INFINITY, interval.getInf(), TEST_TOLERANCE); + Assert.assertEquals(Double.POSITIVE_INFINITY, interval.getSup(), TEST_TOLERANCE); } @Test public void testSinglePoint() { + // act Interval interval = new Interval(1.0, 1.0); + + // assert Assert.assertEquals(0.0, interval.getSize(), Precision.SAFE_MIN); Assert.assertEquals(1.0, interval.getBarycenter(), Precision.EPSILON); } - // MATH-1256 - @Test(expected=NumberIsTooSmallException.class) - public void testStrictOrdering() { - new Interval(0, -1); + @Test + public void testSingleInfinitePoint_positive() { + // act + Interval interval = new Interval(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY); + + // assert + Assert.assertEquals(Double.NaN, interval.getSize(), Precision.SAFE_MIN); // inf - inf = NaN according to floating point spec + Assert.assertEquals(Double.POSITIVE_INFINITY, interval.getBarycenter(), Precision.EPSILON); + } + + @Test + public void testSingleInfinitePoint_negative() { + // act + Interval interval = new Interval(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY); + + // assert + Assert.assertEquals(Double.NaN, interval.getSize(), Precision.SAFE_MIN); // inf - inf = NaN according to floating point spec + Assert.assertEquals(Double.NEGATIVE_INFINITY, interval.getBarycenter(), Precision.EPSILON); } } diff --git a/src/test/java/org/apache/commons/math4/geometry/euclidean/oned/IntervalsSetTest.java b/src/test/java/org/apache/commons/math4/geometry/euclidean/oned/IntervalsSetTest.java index 17b1ee329..20c8790c5 100644 --- a/src/test/java/org/apache/commons/math4/geometry/euclidean/oned/IntervalsSetTest.java +++ b/src/test/java/org/apache/commons/math4/geometry/euclidean/oned/IntervalsSetTest.java @@ -16,10 +16,15 @@ */ package org.apache.commons.math4.geometry.euclidean.oned; +import java.util.ArrayList; import java.util.List; +import org.apache.commons.math4.geometry.GeometryTestUtils; +import org.apache.commons.math4.geometry.partitioning.BSPTree; +import org.apache.commons.math4.geometry.partitioning.BoundaryProjection; import org.apache.commons.math4.geometry.partitioning.Region; import org.apache.commons.math4.geometry.partitioning.RegionFactory; +import org.apache.commons.math4.geometry.partitioning.SubHyperplane; import org.apache.commons.math4.util.FastMath; import org.apache.commons.numbers.core.Precision; import org.junit.Assert; @@ -27,6 +32,463 @@ import org.junit.Test; public class IntervalsSetTest { + private static final double TEST_TOLERANCE = 1e-15; + + @Test + public void testInterval_wholeNumberLine() { + // act + IntervalsSet set = new IntervalsSet(TEST_TOLERANCE); + + // assert + Assert.assertEquals(TEST_TOLERANCE, set.getTolerance(), Precision.SAFE_MIN); + Assert.assertEquals(Double.NEGATIVE_INFINITY, set.getInf(), TEST_TOLERANCE); + Assert.assertEquals(Double.POSITIVE_INFINITY, set.getSup(), TEST_TOLERANCE); + Assert.assertEquals(Double.POSITIVE_INFINITY, set.getSize(), TEST_TOLERANCE); + Assert.assertEquals(0.0, set.getBoundarySize(), TEST_TOLERANCE); + GeometryTestUtils.assertVectorEquals(Cartesian1D.NaN, (Cartesian1D) set.getBarycenter(), TEST_TOLERANCE); + + BSPTree tree = set.getTree(true); + Assert.assertEquals(Boolean.TRUE, tree.getAttribute()); + Assert.assertNull(tree.getCut()); + Assert.assertNull(tree.getMinus()); + Assert.assertNull(tree.getPlus()); + + List intervals = set.asList(); + Assert.assertEquals(1, intervals.size()); + assertInterval(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, intervals.get(0), TEST_TOLERANCE); + + assertLocation(Region.Location.INSIDE, set, Double.NEGATIVE_INFINITY); + assertLocation(Region.Location.INSIDE, set, 0.0); + assertLocation(Region.Location.INSIDE, set, Double.POSITIVE_INFINITY); + } + + @Test + public void testInterval_doubleOpenInterval() { + // act + IntervalsSet set = new IntervalsSet(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, TEST_TOLERANCE); + + // assert + Assert.assertEquals(TEST_TOLERANCE, set.getTolerance(), Precision.SAFE_MIN); + Assert.assertEquals(Double.NEGATIVE_INFINITY, set.getInf(), TEST_TOLERANCE); + Assert.assertEquals(Double.POSITIVE_INFINITY, set.getSup(), TEST_TOLERANCE); + Assert.assertEquals(Double.POSITIVE_INFINITY, set.getSize(), TEST_TOLERANCE); + Assert.assertEquals(0.0, set.getBoundarySize(), TEST_TOLERANCE); + GeometryTestUtils.assertVectorEquals(Cartesian1D.NaN, (Cartesian1D) set.getBarycenter(), TEST_TOLERANCE); + + BSPTree tree = set.getTree(true); + Assert.assertEquals(Boolean.TRUE, tree.getAttribute()); + Assert.assertNull(tree.getCut()); + Assert.assertNull(tree.getMinus()); + Assert.assertNull(tree.getPlus()); + + List intervals = set.asList(); + Assert.assertEquals(1, intervals.size()); + assertInterval(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, intervals.get(0), TEST_TOLERANCE); + + assertLocation(Region.Location.INSIDE, set, Double.NEGATIVE_INFINITY); + assertLocation(Region.Location.INSIDE, set, 0.0); + assertLocation(Region.Location.INSIDE, set, Double.POSITIVE_INFINITY); + } + + @Test + public void testInterval_openInterval_positive() { + // act + IntervalsSet set = new IntervalsSet(9.0, Double.POSITIVE_INFINITY, TEST_TOLERANCE); + + // assert + Assert.assertEquals(TEST_TOLERANCE, set.getTolerance(), Precision.SAFE_MIN); + Assert.assertEquals(9.0, set.getInf(), TEST_TOLERANCE); + Assert.assertEquals(Double.POSITIVE_INFINITY, set.getSup(), TEST_TOLERANCE); + Assert.assertEquals(Double.POSITIVE_INFINITY, set.getSize(), TEST_TOLERANCE); + Assert.assertEquals(0.0, set.getBoundarySize(), TEST_TOLERANCE); + GeometryTestUtils.assertVectorEquals(Cartesian1D.NaN, (Cartesian1D) set.getBarycenter(), TEST_TOLERANCE); + + List intervals = set.asList(); + Assert.assertEquals(1, intervals.size()); + assertInterval(9.0, Double.POSITIVE_INFINITY, intervals.get(0), TEST_TOLERANCE); + + assertLocation(Region.Location.OUTSIDE, set, Double.NEGATIVE_INFINITY); + assertLocation(Region.Location.OUTSIDE, set, 0.0); + assertLocation(Region.Location.BOUNDARY, set, 9.0 - 1e-16); + assertLocation(Region.Location.BOUNDARY, set, 9.0 + 1e-16); + assertLocation(Region.Location.INSIDE, set, 10.0); + assertLocation(Region.Location.INSIDE, set, Double.POSITIVE_INFINITY); + } + + @Test + public void testInterval_openInterval_negative() { + // act + IntervalsSet set = new IntervalsSet(Double.NEGATIVE_INFINITY, 9.0, TEST_TOLERANCE); + + // assert + Assert.assertEquals(TEST_TOLERANCE, set.getTolerance(), Precision.SAFE_MIN); + Assert.assertEquals(Double.NEGATIVE_INFINITY, set.getInf(), TEST_TOLERANCE); + Assert.assertEquals(9.0, set.getSup(), TEST_TOLERANCE); + Assert.assertEquals(Double.POSITIVE_INFINITY, set.getSize(), TEST_TOLERANCE); + Assert.assertEquals(0.0, set.getBoundarySize(), TEST_TOLERANCE); + GeometryTestUtils.assertVectorEquals(Cartesian1D.NaN, (Cartesian1D) set.getBarycenter(), TEST_TOLERANCE); + + List intervals = set.asList(); + Assert.assertEquals(1, intervals.size()); + assertInterval(Double.NEGATIVE_INFINITY, 9.0, intervals.get(0), TEST_TOLERANCE); + + assertLocation(Region.Location.INSIDE, set, Double.NEGATIVE_INFINITY); + assertLocation(Region.Location.INSIDE, set, 0.0); + assertLocation(Region.Location.BOUNDARY, set, 9.0 - 1e-16); + assertLocation(Region.Location.BOUNDARY, set, 9.0 + 1e-16); + assertLocation(Region.Location.OUTSIDE, set, 10.0); + assertLocation(Region.Location.OUTSIDE, set, Double.POSITIVE_INFINITY); + } + + @Test + public void testInterval_singleClosedInterval() { + // act + IntervalsSet set = new IntervalsSet(-1.0, 9.0, TEST_TOLERANCE); + + // assert + Assert.assertEquals(TEST_TOLERANCE, set.getTolerance(), Precision.SAFE_MIN); + Assert.assertEquals(-1.0, set.getInf(), TEST_TOLERANCE); + Assert.assertEquals(9.0, set.getSup(), TEST_TOLERANCE); + Assert.assertEquals(10.0, set.getSize(), TEST_TOLERANCE); + Assert.assertEquals(0.0, set.getBoundarySize(), TEST_TOLERANCE); + GeometryTestUtils.assertVectorEquals(new Cartesian1D(4.0), (Cartesian1D) set.getBarycenter(), TEST_TOLERANCE); + + List intervals = set.asList(); + Assert.assertEquals(1, intervals.size()); + assertInterval(-1.0, 9.0, intervals.get(0), TEST_TOLERANCE); + + assertLocation(Region.Location.OUTSIDE, set, Double.NEGATIVE_INFINITY); + assertLocation(Region.Location.OUTSIDE, set, -2.0); + assertLocation(Region.Location.INSIDE, set, 0.0); + assertLocation(Region.Location.BOUNDARY, set, 9.0 - 1e-16); + assertLocation(Region.Location.BOUNDARY, set, 9.0 + 1e-16); + assertLocation(Region.Location.OUTSIDE, set, 10.0); + assertLocation(Region.Location.OUTSIDE, set, Double.POSITIVE_INFINITY); + } + + @Test + public void testInterval_singlePoint() { + // act + IntervalsSet set = new IntervalsSet(1.0, 1.0, TEST_TOLERANCE); + + // assert + Assert.assertEquals(TEST_TOLERANCE, set.getTolerance(), Precision.SAFE_MIN); + Assert.assertEquals(1.0, set.getInf(), TEST_TOLERANCE); + Assert.assertEquals(1.0, set.getSup(), TEST_TOLERANCE); + Assert.assertEquals(0.0, set.getSize(), TEST_TOLERANCE); + Assert.assertEquals(0.0, set.getBoundarySize(), TEST_TOLERANCE); + GeometryTestUtils.assertVectorEquals(new Cartesian1D(1.0), (Cartesian1D) set.getBarycenter(), TEST_TOLERANCE); + + List intervals = set.asList(); + Assert.assertEquals(1, intervals.size()); + assertInterval(1.0, 1.0, intervals.get(0), TEST_TOLERANCE); + + assertLocation(Region.Location.OUTSIDE, set, Double.NEGATIVE_INFINITY); + assertLocation(Region.Location.OUTSIDE, set, 0.0); + assertLocation(Region.Location.BOUNDARY, set, 1.0); + assertLocation(Region.Location.OUTSIDE, set, 2.0); + assertLocation(Region.Location.OUTSIDE, set, Double.POSITIVE_INFINITY); + } + + @Test + public void testFromBoundaries_wholeNumberLine() { + // arrange + List> boundaries = new ArrayList<>(); + + // act + IntervalsSet set = new IntervalsSet(boundaries, TEST_TOLERANCE); + + // assert + Assert.assertEquals(TEST_TOLERANCE, set.getTolerance(), Precision.SAFE_MIN); + Assert.assertEquals(Double.NEGATIVE_INFINITY, set.getInf(), TEST_TOLERANCE); + Assert.assertEquals(Double.POSITIVE_INFINITY, set.getSup(), TEST_TOLERANCE); + Assert.assertEquals(Double.POSITIVE_INFINITY, set.getSize(), TEST_TOLERANCE); + Assert.assertEquals(0.0, set.getBoundarySize(), TEST_TOLERANCE); + GeometryTestUtils.assertVectorEquals(Cartesian1D.NaN, (Cartesian1D) set.getBarycenter(), TEST_TOLERANCE); + + BSPTree tree = set.getTree(true); + Assert.assertEquals(Boolean.TRUE, tree.getAttribute()); + Assert.assertNull(tree.getCut()); + Assert.assertNull(tree.getMinus()); + Assert.assertNull(tree.getPlus()); + + List intervals = set.asList(); + Assert.assertEquals(1, intervals.size()); + assertInterval(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, intervals.get(0), TEST_TOLERANCE); + + assertLocation(Region.Location.INSIDE, set, Double.NEGATIVE_INFINITY); + assertLocation(Region.Location.INSIDE, set, 0.0); + assertLocation(Region.Location.INSIDE, set, Double.POSITIVE_INFINITY); + } + + @Test + public void testFromBoundaries_openInterval_positive() { + // arrange + List> boundaries = new ArrayList<>(); + boundaries.add(subOrientedPoint(9.0, false)); + + // act + IntervalsSet set = new IntervalsSet(boundaries, TEST_TOLERANCE); + + // assert + Assert.assertEquals(TEST_TOLERANCE, set.getTolerance(), Precision.SAFE_MIN); + Assert.assertEquals(9.0, set.getInf(), TEST_TOLERANCE); + Assert.assertEquals(Double.POSITIVE_INFINITY, set.getSup(), TEST_TOLERANCE); + Assert.assertEquals(Double.POSITIVE_INFINITY, set.getSize(), TEST_TOLERANCE); + Assert.assertEquals(0.0, set.getBoundarySize(), TEST_TOLERANCE); + GeometryTestUtils.assertVectorEquals(Cartesian1D.NaN, (Cartesian1D) set.getBarycenter(), TEST_TOLERANCE); + + List intervals = set.asList(); + Assert.assertEquals(1, intervals.size()); + assertInterval(9.0, Double.POSITIVE_INFINITY, intervals.get(0), TEST_TOLERANCE); + + assertLocation(Region.Location.OUTSIDE, set, Double.NEGATIVE_INFINITY); + assertLocation(Region.Location.OUTSIDE, set, 0.0); + assertLocation(Region.Location.BOUNDARY, set, 9.0 - 1e-16); + assertLocation(Region.Location.BOUNDARY, set, 9.0 + 1e-16); + assertLocation(Region.Location.INSIDE, set, 10.0); + assertLocation(Region.Location.INSIDE, set, Double.POSITIVE_INFINITY); + } + + @Test + public void testFromBoundaries_openInterval_negative() { + // arrange + List> boundaries = new ArrayList<>(); + boundaries.add(subOrientedPoint(9.0, true)); + + // act + IntervalsSet set = new IntervalsSet(boundaries, TEST_TOLERANCE); + + // assert + Assert.assertEquals(TEST_TOLERANCE, set.getTolerance(), Precision.SAFE_MIN); + Assert.assertEquals(Double.NEGATIVE_INFINITY, set.getInf(), TEST_TOLERANCE); + Assert.assertEquals(9.0, set.getSup(), TEST_TOLERANCE); + Assert.assertEquals(Double.POSITIVE_INFINITY, set.getSize(), TEST_TOLERANCE); + Assert.assertEquals(0.0, set.getBoundarySize(), TEST_TOLERANCE); + GeometryTestUtils.assertVectorEquals(Cartesian1D.NaN, (Cartesian1D) set.getBarycenter(), TEST_TOLERANCE); + + List intervals = set.asList(); + Assert.assertEquals(1, intervals.size()); + assertInterval(Double.NEGATIVE_INFINITY, 9.0, intervals.get(0), TEST_TOLERANCE); + + assertLocation(Region.Location.INSIDE, set, Double.NEGATIVE_INFINITY); + assertLocation(Region.Location.INSIDE, set, 0.0); + assertLocation(Region.Location.BOUNDARY, set, 9.0 - 1e-16); + assertLocation(Region.Location.BOUNDARY, set, 9.0 + 1e-16); + assertLocation(Region.Location.OUTSIDE, set, 10.0); + assertLocation(Region.Location.OUTSIDE, set, Double.POSITIVE_INFINITY); + } + + @Test + public void testFromBoundaries_singleClosedInterval() { + // arrange + List> boundaries = new ArrayList<>(); + boundaries.add(subOrientedPoint(-1.0, false)); + boundaries.add(subOrientedPoint(9.0, true)); + + // act + IntervalsSet set = new IntervalsSet(boundaries, TEST_TOLERANCE); + + // assert + Assert.assertEquals(TEST_TOLERANCE, set.getTolerance(), Precision.SAFE_MIN); + Assert.assertEquals(-1.0, set.getInf(), TEST_TOLERANCE); + Assert.assertEquals(9.0, set.getSup(), TEST_TOLERANCE); + Assert.assertEquals(10.0, set.getSize(), TEST_TOLERANCE); + Assert.assertEquals(0.0, set.getBoundarySize(), TEST_TOLERANCE); + GeometryTestUtils.assertVectorEquals(new Cartesian1D(4.0), (Cartesian1D) set.getBarycenter(), TEST_TOLERANCE); + + List intervals = set.asList(); + Assert.assertEquals(1, intervals.size()); + assertInterval(-1.0, 9.0, intervals.get(0), TEST_TOLERANCE); + + assertLocation(Region.Location.OUTSIDE, set, Double.NEGATIVE_INFINITY); + assertLocation(Region.Location.OUTSIDE, set, -2.0); + assertLocation(Region.Location.INSIDE, set, 0.0); + assertLocation(Region.Location.BOUNDARY, set, 9.0 - 1e-16); + assertLocation(Region.Location.BOUNDARY, set, 9.0 + 1e-16); + assertLocation(Region.Location.OUTSIDE, set, 10.0); + assertLocation(Region.Location.OUTSIDE, set, Double.POSITIVE_INFINITY); + } + + @Test + public void testFromBoundaries_multipleClosedIntervals() { + // arrange + List> boundaries = new ArrayList<>(); + boundaries.add(subOrientedPoint(-1.0, false)); + boundaries.add(subOrientedPoint(2.0, true)); + boundaries.add(subOrientedPoint(5.0, false)); + boundaries.add(subOrientedPoint(9.0, true)); + + // act + IntervalsSet set = new IntervalsSet(boundaries, TEST_TOLERANCE); + + // assert + Assert.assertEquals(TEST_TOLERANCE, set.getTolerance(), Precision.SAFE_MIN); + Assert.assertEquals(-1.0, set.getInf(), TEST_TOLERANCE); + Assert.assertEquals(9.0, set.getSup(), TEST_TOLERANCE); + Assert.assertEquals(7.0, set.getSize(), TEST_TOLERANCE); + Assert.assertEquals(0.0, set.getBoundarySize(), TEST_TOLERANCE); + GeometryTestUtils.assertVectorEquals(new Cartesian1D(29.5 / 7.0), (Cartesian1D) set.getBarycenter(), TEST_TOLERANCE); + + List intervals = set.asList(); + Assert.assertEquals(2, intervals.size()); + assertInterval(-1.0, 2.0, intervals.get(0), TEST_TOLERANCE); + assertInterval(5.0, 9.0, intervals.get(1), TEST_TOLERANCE); + + assertLocation(Region.Location.OUTSIDE, set, Double.NEGATIVE_INFINITY); + assertLocation(Region.Location.OUTSIDE, set, -2.0); + assertLocation(Region.Location.INSIDE, set, 0.0); + assertLocation(Region.Location.OUTSIDE, set, 3.0); + assertLocation(Region.Location.INSIDE, set, 6.0); + assertLocation(Region.Location.BOUNDARY, set, 9.0 - 1e-16); + assertLocation(Region.Location.BOUNDARY, set, 9.0 + 1e-16); + assertLocation(Region.Location.OUTSIDE, set, 10.0); + assertLocation(Region.Location.OUTSIDE, set, Double.POSITIVE_INFINITY); + } + + @Test + public void testFromBoundaries_mixedOpenAndClosedIntervals() { + // arrange + List> boundaries = new ArrayList<>(); + boundaries.add(subOrientedPoint(-2.0, true)); + boundaries.add(subOrientedPoint(-1.0, false)); + boundaries.add(subOrientedPoint(2.0, true)); + boundaries.add(subOrientedPoint(5.0, false)); + boundaries.add(subOrientedPoint(9.0, true)); + boundaries.add(subOrientedPoint(10.0, false)); + + // act + IntervalsSet set = new IntervalsSet(boundaries, TEST_TOLERANCE); + + // assert + Assert.assertEquals(TEST_TOLERANCE, set.getTolerance(), Precision.SAFE_MIN); + Assert.assertEquals(Double.NEGATIVE_INFINITY, set.getInf(), TEST_TOLERANCE); + Assert.assertEquals(Double.POSITIVE_INFINITY, set.getSup(), TEST_TOLERANCE); + Assert.assertEquals(Double.POSITIVE_INFINITY, set.getSize(), TEST_TOLERANCE); + Assert.assertEquals(0.0, set.getBoundarySize(), TEST_TOLERANCE); + GeometryTestUtils.assertVectorEquals(new Cartesian1D(Double.NaN), (Cartesian1D) set.getBarycenter(), TEST_TOLERANCE); + + List intervals = set.asList(); + Assert.assertEquals(4, intervals.size()); + assertInterval(Double.NEGATIVE_INFINITY, -2.0, intervals.get(0), TEST_TOLERANCE); + assertInterval(-1.0, 2.0, intervals.get(1), TEST_TOLERANCE); + assertInterval(5.0, 9.0, intervals.get(2), TEST_TOLERANCE); + assertInterval(10.0, Double.POSITIVE_INFINITY, intervals.get(3), TEST_TOLERANCE); + + assertLocation(Region.Location.INSIDE, set, Double.NEGATIVE_INFINITY); + assertLocation(Region.Location.INSIDE, set, -3); + assertLocation(Region.Location.OUTSIDE, set, -1.5); + assertLocation(Region.Location.INSIDE, set, 0.0); + assertLocation(Region.Location.OUTSIDE, set, 3.0); + assertLocation(Region.Location.INSIDE, set, 6.0); + assertLocation(Region.Location.BOUNDARY, set, 9.0 - 1e-16); + assertLocation(Region.Location.BOUNDARY, set, 9.0 + 1e-16); + assertLocation(Region.Location.OUTSIDE, set, 9.5); + assertLocation(Region.Location.INSIDE, set, 11.0); + assertLocation(Region.Location.INSIDE, set, Double.POSITIVE_INFINITY); + } + + @Test + public void testFromBoundaries_intervalEqualToTolerance_onlyFirstBoundaryUsed() { + // arrange + double tolerance = 1e-3; + double first = 1.0; + double second = 1.0 + tolerance; + List> boundaries = new ArrayList<>(); + boundaries.add(subOrientedPoint(first, true, tolerance)); + boundaries.add(subOrientedPoint(second, false, tolerance)); + + // act + IntervalsSet set = new IntervalsSet(boundaries, tolerance); + + // assert + Assert.assertEquals(tolerance, set.getTolerance(), Precision.SAFE_MIN); + Assert.assertEquals(Double.NEGATIVE_INFINITY, set.getInf(), TEST_TOLERANCE); + Assert.assertEquals(first, set.getSup(), TEST_TOLERANCE); + Assert.assertEquals(Double.POSITIVE_INFINITY, set.getSize(), TEST_TOLERANCE); + Assert.assertEquals(0.0, set.getBoundarySize(), TEST_TOLERANCE); + GeometryTestUtils.assertVectorEquals(Cartesian1D.NaN, (Cartesian1D) set.getBarycenter(), TEST_TOLERANCE); + + List intervals = set.asList(); + Assert.assertEquals(1, intervals.size()); + assertInterval(Double.NEGATIVE_INFINITY, first, intervals.get(0), TEST_TOLERANCE); + + assertLocation(Region.Location.INSIDE, set, 0.0); + assertLocation(Region.Location.BOUNDARY, set, 1.0); + assertLocation(Region.Location.OUTSIDE, set, 2.0); + } + + @Test + public void testFromBoundaries_intervalSmallerThanTolerance_onlyFirstBoundaryUsed() { + // arrange + double tolerance = 1e-3; + double first = 1.0; + double second = 1.0 - 1e-4; + List> boundaries = new ArrayList<>(); + boundaries.add(subOrientedPoint(first, false, tolerance)); + boundaries.add(subOrientedPoint(second, true, tolerance)); + + // act + IntervalsSet set = new IntervalsSet(boundaries, tolerance); + + // assert + Assert.assertEquals(tolerance, set.getTolerance(), Precision.SAFE_MIN); + Assert.assertEquals(first, set.getInf(), TEST_TOLERANCE); + Assert.assertEquals(Double.POSITIVE_INFINITY, set.getSup(), TEST_TOLERANCE); + Assert.assertEquals(Double.POSITIVE_INFINITY, set.getSize(), TEST_TOLERANCE); + Assert.assertEquals(0.0, set.getBoundarySize(), TEST_TOLERANCE); + GeometryTestUtils.assertVectorEquals(Cartesian1D.NaN, (Cartesian1D) set.getBarycenter(), TEST_TOLERANCE); + + List intervals = set.asList(); + Assert.assertEquals(1, intervals.size()); + assertInterval(first, Double.POSITIVE_INFINITY, intervals.get(0), TEST_TOLERANCE); + + assertLocation(Region.Location.OUTSIDE, set, 0.0); + assertLocation(Region.Location.BOUNDARY, set, 1.0); + assertLocation(Region.Location.INSIDE, set, 2.0); + } + + @Test + public void testProjectToBoundary() { + // arrange + List> boundaries = new ArrayList<>(); + boundaries.add(subOrientedPoint(-2.0, true)); + boundaries.add(subOrientedPoint(-1.0, false)); + boundaries.add(subOrientedPoint(2.0, true)); + boundaries.add(subOrientedPoint(5.0, false)); + boundaries.add(subOrientedPoint(9.0, true)); + boundaries.add(subOrientedPoint(10.0, false)); + + IntervalsSet set = new IntervalsSet(boundaries, TEST_TOLERANCE); + + // act/assert + assertProjection(new Cartesian1D(-2), -1, set, new Cartesian1D(-3)); + assertProjection(new Cartesian1D(-2), 0, set, new Cartesian1D(-2)); + assertProjection(new Cartesian1D(-2), 0.1, set, new Cartesian1D(-1.9)); + + assertProjection(new Cartesian1D(-1), 0.5, set, new Cartesian1D(-1.5)); + assertProjection(new Cartesian1D(-1), 0.1, set, new Cartesian1D(-1.1)); + assertProjection(new Cartesian1D(-1), 0, set, new Cartesian1D(-1)); + assertProjection(new Cartesian1D(-1), -1, set, new Cartesian1D(0)); + + assertProjection(new Cartesian1D(2), -1, set, new Cartesian1D(1)); + assertProjection(new Cartesian1D(2), 0, set, new Cartesian1D(2)); + assertProjection(new Cartesian1D(2), 1, set, new Cartesian1D(3)); + + assertProjection(new Cartesian1D(5), 1, set, new Cartesian1D(4)); + assertProjection(new Cartesian1D(5), 0, set, new Cartesian1D(5)); + + assertProjection(new Cartesian1D(5), -1, set, new Cartesian1D(6)); + assertProjection(new Cartesian1D(5), -2, set, new Cartesian1D(7)); + + assertProjection(new Cartesian1D(9), -1, set, new Cartesian1D(8)); + assertProjection(new Cartesian1D(9), 0, set, new Cartesian1D(9)); + assertProjection(new Cartesian1D(9), 0.1, set, new Cartesian1D(9.1)); + + assertProjection(new Cartesian1D(10), 0, set, new Cartesian1D(10)); + assertProjection(new Cartesian1D(10), -1, set, new Cartesian1D(11)); + } + @Test public void testInterval() { IntervalsSet set = new IntervalsSet(2.3, 5.7, 1.0e-10); @@ -61,44 +523,65 @@ public class IntervalsSetTest { } @Test - public void testMultiple() { + public void testBooleanOperations() { + // arrange RegionFactory factory = new RegionFactory<>(); + + // act IntervalsSet set = (IntervalsSet) - factory.intersection(factory.union(factory.difference(new IntervalsSet(1.0, 6.0, 1.0e-10), - new IntervalsSet(3.0, 5.0, 1.0e-10)), - new IntervalsSet(9.0, Double.POSITIVE_INFINITY, 1.0e-10)), - new IntervalsSet(Double.NEGATIVE_INFINITY, 11.0, 1.0e-10)); - Assert.assertEquals(5.0, set.getSize(), 1.0e-10); - Assert.assertEquals(5.9, ((Cartesian1D) set.getBarycenter()).getX(), 1.0e-10); - Assert.assertEquals(Region.Location.OUTSIDE, set.checkPoint(new Cartesian1D(0.0))); - Assert.assertEquals(Region.Location.OUTSIDE, set.checkPoint(new Cartesian1D(4.0))); - Assert.assertEquals(Region.Location.OUTSIDE, set.checkPoint(new Cartesian1D(8.0))); - Assert.assertEquals(Region.Location.OUTSIDE, set.checkPoint(new Cartesian1D(12.0))); - Assert.assertEquals(Region.Location.INSIDE, set.checkPoint(new Cartesian1D(1.2))); - Assert.assertEquals(Region.Location.INSIDE, set.checkPoint(new Cartesian1D(5.9))); - Assert.assertEquals(Region.Location.INSIDE, set.checkPoint(new Cartesian1D(9.01))); - Assert.assertEquals(Region.Location.BOUNDARY, set.checkPoint(new Cartesian1D(5.0))); - Assert.assertEquals(Region.Location.BOUNDARY, set.checkPoint(new Cartesian1D(11.0))); - Assert.assertEquals( 1.0, set.getInf(), 1.0e-10); - Assert.assertEquals(11.0, set.getSup(), 1.0e-10); + factory.intersection(factory.union(factory.difference(new IntervalsSet(1.0, 6.0, TEST_TOLERANCE), + new IntervalsSet(3.0, 5.0, TEST_TOLERANCE)), + new IntervalsSet(9.0, Double.POSITIVE_INFINITY, TEST_TOLERANCE)), + new IntervalsSet(Double.NEGATIVE_INFINITY, 11.0, TEST_TOLERANCE)); + + // arrange + Assert.assertEquals(1.0, set.getInf(), TEST_TOLERANCE); + Assert.assertEquals(11.0, set.getSup(), TEST_TOLERANCE); + + Assert.assertEquals(5.0, set.getSize(), TEST_TOLERANCE); + Assert.assertEquals(5.9, ((Cartesian1D) set.getBarycenter()).getX(), TEST_TOLERANCE); + + assertLocation(Region.Location.OUTSIDE, set, 0.0); + assertLocation(Region.Location.OUTSIDE, set, 4.0); + assertLocation(Region.Location.OUTSIDE, set, 8.0); + assertLocation(Region.Location.OUTSIDE, set, 12.0); + assertLocation(Region.Location.INSIDE, set, 1.2); + assertLocation(Region.Location.INSIDE, set, 5.9); + assertLocation(Region.Location.INSIDE, set, 9.01); + assertLocation(Region.Location.BOUNDARY, set, 5.0); + assertLocation(Region.Location.BOUNDARY, set, 11.0); List list = set.asList(); Assert.assertEquals(3, list.size()); - Assert.assertEquals( 1.0, list.get(0).getInf(), 1.0e-10); - Assert.assertEquals( 3.0, list.get(0).getSup(), 1.0e-10); - Assert.assertEquals( 5.0, list.get(1).getInf(), 1.0e-10); - Assert.assertEquals( 6.0, list.get(1).getSup(), 1.0e-10); - Assert.assertEquals( 9.0, list.get(2).getInf(), 1.0e-10); - Assert.assertEquals(11.0, list.get(2).getSup(), 1.0e-10); - + assertInterval(1.0, 3.0, list.get(0), TEST_TOLERANCE); + assertInterval(5.0, 6.0, list.get(1), TEST_TOLERANCE); + assertInterval(9.0, 11.0, list.get(2), TEST_TOLERANCE); } - @Test - public void testSinglePoint() { - IntervalsSet set = new IntervalsSet(1.0, 1.0, 1.0e-10); - Assert.assertEquals(0.0, set.getSize(), Precision.SAFE_MIN); - Assert.assertEquals(0.0, set.getBoundarySize(), Precision.SAFE_MIN); - Assert.assertEquals(1.0, ((Cartesian1D) set.getBarycenter()).getX(), Precision.EPSILON); + private void assertLocation(Region.Location location, IntervalsSet set, double pt) { + Assert.assertEquals(location, set.checkPoint(new Cartesian1D(pt))); } + private void assertInterval(double expectedInf, double expectedSup, Interval actual, double tolerance) { + Assert.assertEquals(expectedInf, actual.getInf(), tolerance); + Assert.assertEquals(expectedSup, actual.getSup(), tolerance); + } + + private void assertProjection(Cartesian1D expectedProjection, double expectedOffset, + IntervalsSet set, Cartesian1D toProject) { + BoundaryProjection proj = set.projectToBoundary(toProject); + + GeometryTestUtils.assertVectorEquals(toProject, (Cartesian1D) proj.getOriginal(), TEST_TOLERANCE); + GeometryTestUtils.assertVectorEquals(expectedProjection, (Cartesian1D) proj.getProjected(), TEST_TOLERANCE); + Assert.assertEquals(expectedOffset, proj.getOffset(), TEST_TOLERANCE); + } + + private SubOrientedPoint subOrientedPoint(double location, boolean direct) { + return subOrientedPoint(location, direct, TEST_TOLERANCE); + } + + private SubOrientedPoint subOrientedPoint(double location, boolean direct, double tolerance) { + // the remaining region isn't necessary for creating 1D boundaries so we can set it to null here + return new SubOrientedPoint(new OrientedPoint(new Cartesian1D(location), direct, tolerance), null); + } } diff --git a/src/test/java/org/apache/commons/math4/geometry/euclidean/oned/OrientedPointTest.java b/src/test/java/org/apache/commons/math4/geometry/euclidean/oned/OrientedPointTest.java new file mode 100644 index 000000000..4a0711391 --- /dev/null +++ b/src/test/java/org/apache/commons/math4/geometry/euclidean/oned/OrientedPointTest.java @@ -0,0 +1,188 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.math4.geometry.euclidean.oned; + +import org.junit.Test; +import org.apache.commons.math3.util.Precision; +import org.apache.commons.math4.geometry.Point; +import org.apache.commons.math4.geometry.Vector; +import org.junit.Assert; + +public class OrientedPointTest { + + @Test + public void testConstructor() { + // act + OrientedPoint pt = new OrientedPoint(new Cartesian1D(2.0), true, 1e-5); + + // assert + Assert.assertEquals(2.0, pt.getLocation().getX(), Precision.EPSILON); + Assert.assertEquals(true, pt.isDirect()); + Assert.assertEquals(1e-5, pt.getTolerance(), Precision.EPSILON); + } + + @Test + public void testCopySelf() { + // arrange + OrientedPoint orig = new OrientedPoint(new Cartesian1D(2.0), true, 1e-5); + + // act + OrientedPoint copy = orig.copySelf(); + + // assert + Assert.assertSame(orig, copy); + Assert.assertEquals(2.0, copy.getLocation().getX(), Precision.EPSILON); + Assert.assertEquals(true, copy.isDirect()); + Assert.assertEquals(1e-5, copy.getTolerance(), Precision.EPSILON); + } + + @Test + public void testGetOffset_direct_point() { + // arrange + OrientedPoint pt = new OrientedPoint(new Cartesian1D(-1.0), true, 1e-5); + + // act/assert + Assert.assertEquals(-99, pt.getOffset((Point) new Cartesian1D(-100)), Precision.EPSILON); + Assert.assertEquals(-1, pt.getOffset((Point) new Cartesian1D(-2)), Precision.EPSILON); + Assert.assertEquals(-0.01, pt.getOffset((Point) new Cartesian1D(-1.01)), Precision.EPSILON); + Assert.assertEquals(0.0, pt.getOffset((Point) new Cartesian1D(-1.0)), Precision.EPSILON); + Assert.assertEquals(0.01, pt.getOffset((Point) new Cartesian1D(-0.99)), Precision.EPSILON); + Assert.assertEquals(1, pt.getOffset((Point) new Cartesian1D(0)), Precision.EPSILON); + Assert.assertEquals(101, pt.getOffset((Point) new Cartesian1D(100)), Precision.EPSILON); + } + + @Test + public void testGetOffset_notDirect_point() { + // arrange + OrientedPoint pt = new OrientedPoint(new Cartesian1D(-1.0), false, 1e-5); + + // act/assert + Assert.assertEquals(99, pt.getOffset((Point) new Cartesian1D(-100)), Precision.EPSILON); + Assert.assertEquals(1, pt.getOffset((Point) new Cartesian1D(-2)), Precision.EPSILON); + Assert.assertEquals(0.01, pt.getOffset((Point) new Cartesian1D(-1.01)), Precision.EPSILON); + Assert.assertEquals(0.0, pt.getOffset((Point) new Cartesian1D(-1.0)), Precision.EPSILON); + Assert.assertEquals(-0.01, pt.getOffset((Point) new Cartesian1D(-0.99)), Precision.EPSILON); + Assert.assertEquals(-1, pt.getOffset((Point) new Cartesian1D(0)), Precision.EPSILON); + Assert.assertEquals(-101, pt.getOffset((Point) new Cartesian1D(100)), Precision.EPSILON); + } + + @Test + public void testGetOffset_direct_vector() { + // arrange + OrientedPoint pt = new OrientedPoint(new Cartesian1D(-1.0), true, 1e-5); + + // act/assert + Assert.assertEquals(-99, pt.getOffset((Vector) new Cartesian1D(-100)), Precision.EPSILON); + Assert.assertEquals(-1, pt.getOffset((Vector) new Cartesian1D(-2)), Precision.EPSILON); + Assert.assertEquals(-0.01, pt.getOffset((Vector) new Cartesian1D(-1.01)), Precision.EPSILON); + Assert.assertEquals(-0.0, pt.getOffset((Vector) new Cartesian1D(-1.0)), Precision.EPSILON); + Assert.assertEquals(0.01, pt.getOffset((Vector) new Cartesian1D(-0.99)), Precision.EPSILON); + Assert.assertEquals(1, pt.getOffset((Vector) new Cartesian1D(0)), Precision.EPSILON); + Assert.assertEquals(101, pt.getOffset((Vector) new Cartesian1D(100)), Precision.EPSILON); + } + + @Test + public void testGetOffset_notDirect_vector() { + // arrange + OrientedPoint pt = new OrientedPoint(new Cartesian1D(-1.0), false, 1e-5); + + // act/assert + Assert.assertEquals(99, pt.getOffset((Vector) new Cartesian1D(-100)), Precision.EPSILON); + Assert.assertEquals(1, pt.getOffset((Vector) new Cartesian1D(-2)), Precision.EPSILON); + Assert.assertEquals(0.01, pt.getOffset((Vector) new Cartesian1D(-1.01)), Precision.EPSILON); + Assert.assertEquals(0.0, pt.getOffset((Vector) new Cartesian1D(-1.0)), Precision.EPSILON); + Assert.assertEquals(-0.01, pt.getOffset((Vector) new Cartesian1D(-0.99)), Precision.EPSILON); + Assert.assertEquals(-1, pt.getOffset((Vector) new Cartesian1D(0)), Precision.EPSILON); + Assert.assertEquals(-101, pt.getOffset((Vector) new Cartesian1D(100)), Precision.EPSILON); + } + + @Test + public void testWholeHyperplane() { + // arrange + OrientedPoint pt = new OrientedPoint(new Cartesian1D(1.0), false, 1e-5); + + // act + SubOrientedPoint subPt = pt.wholeHyperplane(); + + // assert + Assert.assertSame(pt, subPt.getHyperplane()); + Assert.assertNull(subPt.getRemainingRegion()); + } + + @Test + public void testWholeSpace() { + // arrange + OrientedPoint pt = new OrientedPoint(new Cartesian1D(1.0), false, 1e-5); + + // act + IntervalsSet set = pt.wholeSpace(); + + // assert + Assert.assertEquals(Double.NEGATIVE_INFINITY, set.getInf(), Precision.EPSILON); + Assert.assertEquals(Double.POSITIVE_INFINITY, set.getSup(), Precision.EPSILON); + } + + @Test + public void testSameOrientationAs() { + // arrange + OrientedPoint notDirect1 = new OrientedPoint(new Cartesian1D(1.0), false, 1e-5); + OrientedPoint notDirect2 = new OrientedPoint(new Cartesian1D(1.0), false, 1e-5); + OrientedPoint direct1 = new OrientedPoint(new Cartesian1D(1.0), true, 1e-5); + OrientedPoint direct2 = new OrientedPoint(new Cartesian1D(1.0), true, 1e-5); + + // act/assert + Assert.assertEquals(true, notDirect1.sameOrientationAs(notDirect1)); + Assert.assertEquals(true, notDirect1.sameOrientationAs(notDirect2)); + Assert.assertEquals(true, notDirect2.sameOrientationAs(notDirect1)); + + Assert.assertEquals(true, direct1.sameOrientationAs(direct1)); + Assert.assertEquals(true, direct1.sameOrientationAs(direct2)); + Assert.assertEquals(true, direct2.sameOrientationAs(direct1)); + + Assert.assertEquals(false, notDirect1.sameOrientationAs(direct1)); + Assert.assertEquals(false, direct1.sameOrientationAs(notDirect1)); + } + + @Test + public void testProject() { + // arrange + OrientedPoint pt = new OrientedPoint(new Cartesian1D(1.0), true, 1e-5); + + // act/assert + Assert.assertEquals(1.0, ((Cartesian1D) pt.project(new Cartesian1D(-1.0))).getX(), Precision.EPSILON); + Assert.assertEquals(1.0, ((Cartesian1D) pt.project(new Cartesian1D(0.0))).getX(), Precision.EPSILON); + Assert.assertEquals(1.0, ((Cartesian1D) pt.project(new Cartesian1D(1.0))).getX(), Precision.EPSILON); + Assert.assertEquals(1.0, ((Cartesian1D) pt.project(new Cartesian1D(100.0))).getX(), Precision.EPSILON); + } + + @Test + public void testRevertSelf() { + // arrange + OrientedPoint pt = new OrientedPoint(new Cartesian1D(2.0), true, 1e-5); + + // act + pt.revertSelf(); + + // assert + Assert.assertEquals(2.0, pt.getLocation().getX(), Precision.EPSILON); + Assert.assertEquals(false, pt.isDirect()); + Assert.assertEquals(1e-5, pt.getTolerance(), Precision.EPSILON); + + Assert.assertEquals(1, pt.getOffset((Vector) new Cartesian1D(1.0)), Precision.EPSILON); + Assert.assertEquals(-1, pt.getOffset((Vector) new Cartesian1D(3.0)), Precision.EPSILON); + } +} diff --git a/src/test/java/org/apache/commons/math4/geometry/euclidean/oned/SubOrientedPointTest.java b/src/test/java/org/apache/commons/math4/geometry/euclidean/oned/SubOrientedPointTest.java index 6888d519a..2843cac9e 100644 --- a/src/test/java/org/apache/commons/math4/geometry/euclidean/oned/SubOrientedPointTest.java +++ b/src/test/java/org/apache/commons/math4/geometry/euclidean/oned/SubOrientedPointTest.java @@ -16,8 +16,6 @@ */ package org.apache.commons.math4.geometry.euclidean.oned; -import java.util.List; - import org.apache.commons.math4.geometry.partitioning.Side; import org.apache.commons.math4.geometry.partitioning.SubHyperplane; import org.apache.commons.math4.geometry.partitioning.SubHyperplane.SplitSubHyperplane; @@ -31,7 +29,7 @@ public class SubOrientedPointTest { public void testGetSize() { // arrange OrientedPoint hyperplane = new OrientedPoint(new Cartesian1D(1), true, TEST_TOLERANCE); - SubOrientedPoint pt = (SubOrientedPoint) hyperplane.wholeHyperplane(); + SubOrientedPoint pt = hyperplane.wholeHyperplane(); // act/assert Assert.assertEquals(0.0, pt.getSize(), TEST_TOLERANCE); @@ -41,7 +39,7 @@ public class SubOrientedPointTest { public void testIsEmpty() { // arrange OrientedPoint hyperplane = new OrientedPoint(new Cartesian1D(1), true, TEST_TOLERANCE); - SubOrientedPoint pt = (SubOrientedPoint) hyperplane.wholeHyperplane(); + SubOrientedPoint pt = hyperplane.wholeHyperplane(); // act/assert Assert.assertFalse(pt.isEmpty()); @@ -51,7 +49,7 @@ public class SubOrientedPointTest { public void testBuildNew() { // arrange OrientedPoint originalHyperplane = new OrientedPoint(new Cartesian1D(1), true, TEST_TOLERANCE); - SubOrientedPoint pt = (SubOrientedPoint) originalHyperplane.wholeHyperplane(); + SubOrientedPoint pt = originalHyperplane.wholeHyperplane(); OrientedPoint hyperplane = new OrientedPoint(new Cartesian1D(2), true, TEST_TOLERANCE); IntervalsSet intervals = new IntervalsSet(2, 3, TEST_TOLERANCE); @@ -67,86 +65,80 @@ public class SubOrientedPointTest { @Test public void testSplit_resultOnMinusSide() { - // arrange - OrientedPoint hyperplane = new OrientedPoint(new Cartesian1D(1), true, TEST_TOLERANCE); - IntervalsSet interval = new IntervalsSet(4, 5, TEST_TOLERANCE); - SubOrientedPoint pt = new SubOrientedPoint(hyperplane, interval); + // arrange + OrientedPoint hyperplane = new OrientedPoint(new Cartesian1D(1), true, TEST_TOLERANCE); + IntervalsSet interval = new IntervalsSet(TEST_TOLERANCE); + SubOrientedPoint pt = new SubOrientedPoint(hyperplane, interval); - OrientedPoint splitter = new OrientedPoint(new Cartesian1D(2), true, TEST_TOLERANCE); + OrientedPoint splitter = new OrientedPoint(new Cartesian1D(2), true, TEST_TOLERANCE); - // act - SplitSubHyperplane split = pt.split(splitter); + // act + SplitSubHyperplane split = pt.split(splitter); - // assert - Assert.assertEquals(Side.MINUS, split.getSide()); + // assert + Assert.assertEquals(Side.MINUS, split.getSide()); - SubOrientedPoint minusSub = ((SubOrientedPoint) split.getMinus()); - Assert.assertNotNull(minusSub); + SubOrientedPoint minusSub = ((SubOrientedPoint) split.getMinus()); + Assert.assertNotNull(minusSub); - OrientedPoint minusHyper = (OrientedPoint) minusSub.getHyperplane(); - Assert.assertEquals(1, minusHyper.getLocation().getX(), TEST_TOLERANCE); + OrientedPoint minusHyper = (OrientedPoint) minusSub.getHyperplane(); + Assert.assertEquals(1, minusHyper.getLocation().getX(), TEST_TOLERANCE); - List minusIntervals = ((IntervalsSet) minusSub.getRemainingRegion()).asList(); - Assert.assertEquals(1, minusIntervals.size()); - Assert.assertEquals(4, minusIntervals.get(0).getInf(), TEST_TOLERANCE); - Assert.assertEquals(5, minusIntervals.get(0).getSup(), TEST_TOLERANCE); + Assert.assertSame(interval, minusSub.getRemainingRegion()); - Assert.assertNull(split.getPlus()); + Assert.assertNull(split.getPlus()); } @Test public void testSplit_resultOnPlusSide() { - // arrange - OrientedPoint hyperplane = new OrientedPoint(new Cartesian1D(1), true, TEST_TOLERANCE); - IntervalsSet interval = new IntervalsSet(4, 5, TEST_TOLERANCE); - SubOrientedPoint pt = new SubOrientedPoint(hyperplane, interval); + // arrange + OrientedPoint hyperplane = new OrientedPoint(new Cartesian1D(1), true, TEST_TOLERANCE); + IntervalsSet interval = new IntervalsSet(TEST_TOLERANCE); + SubOrientedPoint pt = new SubOrientedPoint(hyperplane, interval); - OrientedPoint splitter = new OrientedPoint(new Cartesian1D(0), true, TEST_TOLERANCE); + OrientedPoint splitter = new OrientedPoint(new Cartesian1D(0), true, TEST_TOLERANCE); - // act - SplitSubHyperplane split = pt.split(splitter); + // act + SplitSubHyperplane split = pt.split(splitter); - // assert - Assert.assertEquals(Side.PLUS, split.getSide()); + // assert + Assert.assertEquals(Side.PLUS, split.getSide()); - Assert.assertNull(split.getMinus()); + Assert.assertNull(split.getMinus()); - SubOrientedPoint plusSub = ((SubOrientedPoint) split.getPlus()); - Assert.assertNotNull(plusSub); + SubOrientedPoint plusSub = ((SubOrientedPoint) split.getPlus()); + Assert.assertNotNull(plusSub); - OrientedPoint plusHyper = (OrientedPoint) plusSub.getHyperplane(); - Assert.assertEquals(1, plusHyper.getLocation().getX(), TEST_TOLERANCE); + OrientedPoint plusHyper = (OrientedPoint) plusSub.getHyperplane(); + Assert.assertEquals(1, plusHyper.getLocation().getX(), TEST_TOLERANCE); - List plusIntervals = ((IntervalsSet) plusSub.getRemainingRegion()).asList(); - Assert.assertEquals(1, plusIntervals.size()); - Assert.assertEquals(4, plusIntervals.get(0).getInf(), TEST_TOLERANCE); - Assert.assertEquals(5, plusIntervals.get(0).getSup(), TEST_TOLERANCE); + Assert.assertSame(interval, plusSub.getRemainingRegion()); } @Test public void testSplit_equivalentHyperplanes() { - // arrange - OrientedPoint hyperplane = new OrientedPoint(new Cartesian1D(1), true, TEST_TOLERANCE); - IntervalsSet interval = new IntervalsSet(4, 5, TEST_TOLERANCE); - SubOrientedPoint pt = new SubOrientedPoint(hyperplane, interval); + // arrange + OrientedPoint hyperplane = new OrientedPoint(new Cartesian1D(1), true, TEST_TOLERANCE); + IntervalsSet interval = new IntervalsSet(TEST_TOLERANCE); + SubOrientedPoint pt = new SubOrientedPoint(hyperplane, interval); - OrientedPoint splitter = new OrientedPoint(new Cartesian1D(1), true, TEST_TOLERANCE); + OrientedPoint splitter = new OrientedPoint(new Cartesian1D(1), true, TEST_TOLERANCE); - // act - SplitSubHyperplane split = pt.split(splitter); + // act + SplitSubHyperplane split = pt.split(splitter); - // assert - Assert.assertEquals(Side.HYPER, split.getSide()); + // assert + Assert.assertEquals(Side.HYPER, split.getSide()); - Assert.assertNull(split.getMinus()); - Assert.assertNull(split.getPlus()); + Assert.assertNull(split.getMinus()); + Assert.assertNull(split.getPlus()); } @Test public void testSplit_usesToleranceFromParentHyperplane() { // arrange OrientedPoint hyperplane = new OrientedPoint(new Cartesian1D(1), true, 0.1); - SubOrientedPoint pt = (SubOrientedPoint) hyperplane.wholeHyperplane(); + SubOrientedPoint pt = hyperplane.wholeHyperplane(); // act/assert SplitSubHyperplane plusSplit = pt.split(new OrientedPoint(new Cartesian1D(0.899), true, 1e-10)); diff --git a/src/test/java/org/apache/commons/math4/geometry/euclidean/threed/OBJWriter.java b/src/test/java/org/apache/commons/math4/geometry/euclidean/threed/OBJWriter.java new file mode 100644 index 000000000..4d13f0ba7 --- /dev/null +++ b/src/test/java/org/apache/commons/math4/geometry/euclidean/threed/OBJWriter.java @@ -0,0 +1,337 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.math4.geometry.euclidean.threed; + +import java.io.File; +import java.io.IOException; +import java.io.Writer; +import java.nio.file.Files; +import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import org.apache.commons.math4.geometry.euclidean.twod.Cartesian2D; +import org.apache.commons.math4.geometry.euclidean.twod.Euclidean2D; +import org.apache.commons.math4.geometry.euclidean.twod.PolygonsSet; +import org.apache.commons.math4.geometry.partitioning.BSPTree; +import org.apache.commons.math4.geometry.partitioning.BSPTreeVisitor; +import org.apache.commons.math4.geometry.partitioning.BoundaryAttribute; + +/** This class creates simple OBJ files from {@link PolyhedronsSet} instances. + * The output files can be opened in a 3D viewer for visual debugging of 3D + * regions. This class is only intended for use in testing. + * + * @see https://en.wikipedia.org/wiki/Wavefront_.obj_file + * @since 4.0 + */ +public class OBJWriter { + + /** Writes an OBJ file representing the given {@link PolyhedronsSet}. Only + * finite boundaries are written. Infinite boundaries are ignored. + * @param file The path of the file to write + * @param poly The input PolyhedronsSet + * @throws IOException + */ + public static void write(String file, PolyhedronsSet poly) throws IOException { + write(new File(file), poly); + } + + /** Writes an OBJ file representing the given {@link PolyhedronsSet}. Only + * finite boundaries are written. Infinite boundaries are ignored. + * @param file The file to write + * @param poly The input PolyhedronsSet + * @throws IOException + */ + public static void write(File file, PolyhedronsSet poly) throws IOException { + // get the vertices and faces + MeshBuilder meshBuilder = new MeshBuilder(poly.getTolerance()); + poly.getTree(true).visit(meshBuilder); + + // write them to the file + try (Writer writer = Files.newBufferedWriter(file.toPath())) { + writer.write("# Generated by " + OBJWriter.class.getName() + " on " + new Date() + "\n"); + writeVertices(writer, meshBuilder.getVertices()); + writeFaces(writer, meshBuilder.getFaces()); + } + } + + /** Writes the given list of vertices to the file in the OBJ format. + * @param writer + * @param vertices + * @throws IOException + */ + private static void writeVertices(Writer writer, List vertices) throws IOException { + DecimalFormat df = new DecimalFormat("0.######"); + + for (Cartesian3D v : vertices) { + writer.write("v "); + writer.write(df.format(v.getX())); + writer.write(" "); + writer.write(df.format(v.getY())); + writer.write(" "); + writer.write(df.format(v.getZ())); + writer.write("\n"); + } + } + + /** Writes the given list of face vertex indices to the file in the OBJ format. The indices + * are expected to be 0-based and are converted to the 1-based format used by OBJ. + * @param writer + * @param faces + * @throws IOException + */ + private static void writeFaces(Writer writer, List faces) throws IOException { + for (int[] face : faces) { + writer.write("f "); + for (int idx : face) { + writer.write(String.valueOf(idx + 1)); // obj indices are 1-based + writer.write(" "); + } + writer.write("\n"); + } + } + + /** Class used to impose a strict sorting on 3D vertices. + * If all of the components of two vertices are within tolerance of each + * other, then the vertices are considered equal. This helps to avoid + * writing duplicate vertices in the OBJ output. + */ + private static class VertexComparator implements Comparator { + + /** Geometric tolerance value */ + private double tolerance; + + /** Creates a new instance with the given tolerance value. + * @param tolerance + */ + public VertexComparator(double tolerance) { + this.tolerance = tolerance; + } + + /** {@inheritDoc} */ + @Override + public int compare(Cartesian3D a, Cartesian3D b) { + int result = compareDoubles(a.getX(), b.getX()); + if (result == 0) { + result = compareDoubles(a.getY(), b.getY()); + if (result == 0) { + result = compareDoubles(a.getZ(), b.getZ()); + } + } + return result; + } + + /** Helper method to compare two double values using the + * configured tolerance value. If the values are within + * tolerance of each other, then they are considered equal. + * @param a + * @param b + * @return + */ + private int compareDoubles(double a, double b) { + double diff = a - b; + if (diff < -tolerance) { + return -1; + } + else if (diff > tolerance) { + return 1; + } + return 0; + } + } + + /** Class for converting a 3D BSPTree into a list of vertices + * and face vertex indices. + */ + private static class MeshBuilder implements BSPTreeVisitor { + + /** Geometric tolerance */ + private final double tolerance; + + /** Map of vertices to their index in the vertices list */ + private Map vertexIndexMap; + + /** List of unique vertices in the BSPTree boundary */ + private List vertices; + + /** + * List of face vertex indices. Each face will have 3 indices. Indices + * are 0-based. + * */ + private List faces; + + /** Creates a new instance with the given tolerance. + * @param tolerance + */ + public MeshBuilder(double tolerance) { + this.tolerance = tolerance; + this.vertexIndexMap = new TreeMap<>(new VertexComparator(tolerance)); + this.vertices = new ArrayList<>(); + this.faces = new ArrayList<>(); + } + + /** Returns the list of unique vertices found in the BSPTree. + * @return + */ + public List getVertices() { + return vertices; + } + + /** Returns the list of 0-based face vertex indices for the BSPTree. Each face is + * a triangle with 3 indices. + * @return + */ + public List getFaces() { + return faces; + } + + /** {@inheritDoc} */ + @Override + public Order visitOrder(BSPTree node) { + return Order.SUB_MINUS_PLUS; + } + + /** {@inheritDoc} */ + @SuppressWarnings("unchecked") + @Override + public void visitInternalNode(BSPTree node) { + BoundaryAttribute attr = (BoundaryAttribute) node.getAttribute(); + + if (attr.getPlusOutside() != null) { + addBoundary((SubPlane) attr.getPlusOutside()); + } + else if (attr.getPlusInside() != null) { + addBoundary((SubPlane) attr.getPlusInside()); + } + } + + /** {@inheritDoc} */ + @Override + public void visitLeafNode(BSPTree node) { + // do nothing + } + + /** Adds the region boundary defined by the given {@link SubPlane} + * to the mesh. + * @param subplane + */ + private void addBoundary(SubPlane subplane) { + Plane plane = (Plane) subplane.getHyperplane(); + PolygonsSet poly = (PolygonsSet) subplane.getRemainingRegion(); + + TriangleExtractor triExtractor = new TriangleExtractor(tolerance); + poly.getTree(true).visit(triExtractor); + + Cartesian3D v1, v2, v3; + for (Cartesian2D[] tri : triExtractor.getTriangles()) { + v1 = plane.toSpace(tri[0]); + v2 = plane.toSpace(tri[1]); + v3 = plane.toSpace(tri[2]); + + faces.add(new int[] { + getVertexIndex(v1), + getVertexIndex(v2), + getVertexIndex(v3) + }); + } + } + + /** Returns the 0-based index of the given vertex in the vertices + * list. If the vertex has not been encountered before, it is added + * to the list. + * @param vertex + * @return + */ + private int getVertexIndex(Cartesian3D vertex) { + Integer idx = vertexIndexMap.get(vertex); + if (idx == null) { + idx = vertices.size(); + + vertices.add(vertex); + vertexIndexMap.put(vertex, idx); + } + return idx.intValue(); + } + } + + /** Visitor for extracting a collection of triangles from a 2D BSPTree. + */ + private static class TriangleExtractor implements BSPTreeVisitor { + + /** Geometric tolerance */ + private double tolerance; + + /** List of extracted triangles */ + private List triangles = new ArrayList<>(); + + /** Creates a new instance with the given geometric tolerance. + * @param tolerance + */ + public TriangleExtractor(double tolerance) { + this.tolerance = tolerance; + } + + /** Returns the list of extracted triangles. + * @return + */ + public List getTriangles() { + return triangles; + } + + /** {@inheritDoc} */ + @Override + public Order visitOrder(BSPTree node) { + return Order.SUB_MINUS_PLUS; + } + + /** {@inheritDoc} */ + @Override + public void visitInternalNode(BSPTree node) { + // do nothing + } + + /** {@inheritDoc} */ + @Override + public void visitLeafNode(BSPTree node) { + if ((Boolean) node.getAttribute()) { + PolygonsSet convexPoly = new PolygonsSet(node.pruneAroundConvexCell(Boolean.TRUE, + Boolean.FALSE, null), tolerance); + + for (Cartesian2D[] loop : convexPoly.getVertices()) { + if (loop.length > 0 && loop[0] != null) { // skip unclosed loops + addTriangles(loop); + } + } + } + } + + /** Splits the 2D convex area defined by the given vertices into + * triangles and adds them to the internal list. + * @param vertices + */ + private void addTriangles(Cartesian2D[] vertices) { + // use a triangle fan to add the convex region + for (int i=2; i(Boolean.FALSE), 1e-10); + PolyhedronsSet polySet = new PolyhedronsSet(new BSPTree(Boolean.FALSE), TEST_TOLERANCE); // assert - Assert.assertEquals(0.0, tree.getSize(), 1.0e-10); - Assert.assertEquals(0.0, tree.getBoundarySize(), 1.0e-10); - Cartesian3D center = (Cartesian3D) tree.getBarycenter(); - Assert.assertEquals(Double.NaN, center.getX(), 1e-10); - Assert.assertEquals(Double.NaN, center.getY(), 1e-10); - Assert.assertEquals(Double.NaN, center.getZ(), 1e-10); - Assert.assertEquals(false, tree.isFull()); - Assert.assertEquals(true, tree.isEmpty()); + Assert.assertEquals(TEST_TOLERANCE, polySet.getTolerance(), Precision.EPSILON); + Assert.assertEquals(0.0, polySet.getSize(), TEST_TOLERANCE); + Assert.assertEquals(0.0, polySet.getBoundarySize(), TEST_TOLERANCE); + GeometryTestUtils.assertVectorEquals(Cartesian3D.NaN, (Cartesian3D) polySet.getBarycenter(), TEST_TOLERANCE); + Assert.assertEquals(true, polySet.isEmpty()); + Assert.assertEquals(false, polySet.isFull()); - checkPoints(Region.Location.OUTSIDE, tree, new Cartesian3D[] { - new Cartesian3D(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY), - Cartesian3D.ZERO, - new Cartesian3D(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY) - }); + checkPoints(Region.Location.OUTSIDE, polySet, + new Cartesian3D(-Double.MAX_VALUE, -Double.MAX_VALUE, -Double.MAX_VALUE), + new Cartesian3D(-100, -100, -100), + new Cartesian3D(0, 0, 0), + new Cartesian3D(100, 100, 100), + new Cartesian3D(Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE)); } @Test - public void testSingleInfiniteBoundary() { + public void testHalfSpace() { // arrange - Plane plane = new Plane(Cartesian3D.ZERO, Cartesian3D.PLUS_K, 1e-10); - List> boundaries = new ArrayList<>(); - boundaries.add(plane.wholeHyperplane()); + boundaries.add(new SubPlane(new Plane(Cartesian3D.ZERO, Cartesian3D.PLUS_J, TEST_TOLERANCE), + new PolygonsSet(TEST_TOLERANCE))); // act - PolyhedronsSet tree = new PolyhedronsSet(boundaries, 1e-10); + PolyhedronsSet polySet = new PolyhedronsSet(boundaries, TEST_TOLERANCE); // assert - Assert.assertEquals(Double.POSITIVE_INFINITY, tree.getSize(), 1.0e-10); - Assert.assertEquals(Double.POSITIVE_INFINITY, tree.getBoundarySize(), 1.0e-10); - Cartesian3D center = (Cartesian3D) tree.getBarycenter(); - Assert.assertEquals(Double.NaN, center.getX(), 1e-10); - Assert.assertEquals(Double.NaN, center.getY(), 1e-10); - Assert.assertEquals(Double.NaN, center.getZ(), 1e-10); - Assert.assertEquals(false, tree.isFull()); - Assert.assertEquals(false, tree.isEmpty()); + Assert.assertEquals(TEST_TOLERANCE, polySet.getTolerance(), Precision.EPSILON); + Assert.assertEquals(Double.POSITIVE_INFINITY, polySet.getSize(), TEST_TOLERANCE); + Assert.assertEquals(Double.POSITIVE_INFINITY, polySet.getBoundarySize(), TEST_TOLERANCE); + GeometryTestUtils.assertVectorEquals(Cartesian3D.NaN, (Cartesian3D) polySet.getBarycenter(), TEST_TOLERANCE); + Assert.assertEquals(false, polySet.isEmpty()); + Assert.assertEquals(false, polySet.isFull()); - checkPoints(Region.Location.BOUNDARY, tree, new Cartesian3D[] { - Cartesian3D.ZERO - }); + checkPoints(Region.Location.INSIDE, polySet, + new Cartesian3D(-Double.MAX_VALUE, -Double.MAX_VALUE, -Double.MAX_VALUE), + new Cartesian3D(-100, -100, -100)); + checkPoints(Region.Location.BOUNDARY, polySet, new Cartesian3D(0, 0, 0)); + checkPoints(Region.Location.OUTSIDE, polySet, + new Cartesian3D(100, 100, 100), + new Cartesian3D(Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE)); } @Test - public void testBox() { - PolyhedronsSet tree = new PolyhedronsSet(0, 1, 0, 1, 0, 1, 1.0e-10); - Assert.assertEquals(1.0, tree.getSize(), 1.0e-10); - Assert.assertEquals(6.0, tree.getBoundarySize(), 1.0e-10); - Cartesian3D barycenter = (Cartesian3D) tree.getBarycenter(); - Assert.assertEquals(0.5, barycenter.getX(), 1.0e-10); - Assert.assertEquals(0.5, barycenter.getY(), 1.0e-10); - Assert.assertEquals(0.5, barycenter.getZ(), 1.0e-10); + public void testInvertedRegion() { + // arrange + List> boundaries = createBoxBoundaries(Cartesian3D.ZERO, 1.0, TEST_TOLERANCE); + PolyhedronsSet box = new PolyhedronsSet(boundaries, TEST_TOLERANCE);; + + // act + PolyhedronsSet polySet = (PolyhedronsSet) new RegionFactory().getComplement(box); + + // assert + Assert.assertEquals(TEST_TOLERANCE, polySet.getTolerance(), Precision.EPSILON); + Assert.assertEquals(Double.POSITIVE_INFINITY, polySet.getSize(), TEST_TOLERANCE); + Assert.assertEquals(6, polySet.getBoundarySize(), TEST_TOLERANCE); + GeometryTestUtils.assertVectorEquals(Cartesian3D.NaN, (Cartesian3D) polySet.getBarycenter(), TEST_TOLERANCE); + Assert.assertEquals(false, polySet.isEmpty()); + Assert.assertEquals(false, polySet.isFull()); + + checkPoints(Region.Location.INSIDE, polySet, + new Cartesian3D(-Double.MAX_VALUE, -Double.MAX_VALUE, -Double.MAX_VALUE), + new Cartesian3D(-100, -100, -100), + new Cartesian3D(100, 100, 100), + new Cartesian3D(Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE)); + checkPoints(Region.Location.OUTSIDE, polySet, + new Cartesian3D(0, 0, 0)); + } + + @Test + public void testCreateFromBoundaries_noBoundaries_treeRepresentsWholeSpace() { + // arrange + List> boundaries = new ArrayList<>(); + + // act + PolyhedronsSet polySet = new PolyhedronsSet(boundaries, TEST_TOLERANCE); + + // assert + Assert.assertEquals(TEST_TOLERANCE, polySet.getTolerance(), Precision.EPSILON); + Assert.assertEquals(Double.POSITIVE_INFINITY, polySet.getSize(), TEST_TOLERANCE); + Assert.assertEquals(0.0, polySet.getBoundarySize(), TEST_TOLERANCE); + GeometryTestUtils.assertVectorEquals(Cartesian3D.NaN, (Cartesian3D) polySet.getBarycenter(), TEST_TOLERANCE); + Assert.assertEquals(false, polySet.isEmpty()); + Assert.assertEquals(true, polySet.isFull()); + } + + @Test + public void testCreateFromBoundaries_unitBox() { + // arrange + List> boundaries = createBoxBoundaries(Cartesian3D.ZERO, 1.0, TEST_TOLERANCE); + + // act + PolyhedronsSet polySet = new PolyhedronsSet(boundaries, TEST_TOLERANCE); + + // assert + Assert.assertEquals(TEST_TOLERANCE, polySet.getTolerance(), Precision.EPSILON); + Assert.assertEquals(1.0, polySet.getSize(), TEST_TOLERANCE); + Assert.assertEquals(6.0, polySet.getBoundarySize(), TEST_TOLERANCE); + GeometryTestUtils.assertVectorEquals(Cartesian3D.ZERO, (Cartesian3D) polySet.getBarycenter(), TEST_TOLERANCE); + Assert.assertEquals(false, polySet.isEmpty()); + Assert.assertEquals(false, polySet.isFull()); + + checkPoints(Region.Location.OUTSIDE, polySet, + new Cartesian3D(-1, 0, 0), + new Cartesian3D(1, 0, 0), + new Cartesian3D(0, -1, 0), + new Cartesian3D(0, 1, 0), + new Cartesian3D(0, 0, -1), + new Cartesian3D(0, 0, 1), + + new Cartesian3D(1, 1, 1), + new Cartesian3D(1, 1, -1), + new Cartesian3D(1, -1, 1), + new Cartesian3D(1, -1, -1), + new Cartesian3D(-1, 1, 1), + new Cartesian3D(-1, 1, -1), + new Cartesian3D(-1, -1, 1), + new Cartesian3D(-1, -1, -1)); + + checkPoints(Region.Location.BOUNDARY, polySet, + new Cartesian3D(0.5, 0, 0), + new Cartesian3D(-0.5, 0, 0), + new Cartesian3D(0, 0.5, 0), + new Cartesian3D(0, -0.5, 0), + new Cartesian3D(0, 0, 0.5), + new Cartesian3D(0, 0, -0.5), + + new Cartesian3D(0.5, 0.5, 0.5), + new Cartesian3D(0.5, 0.5, -0.5), + new Cartesian3D(0.5, -0.5, 0.5), + new Cartesian3D(0.5, -0.5, -0.5), + new Cartesian3D(-0.5, 0.5, 0.5), + new Cartesian3D(-0.5, 0.5, -0.5), + new Cartesian3D(-0.5, -0.5, 0.5), + new Cartesian3D(-0.5, -0.5, -0.5)); + + checkPoints(Region.Location.INSIDE, polySet, + new Cartesian3D(0, 0, 0), + + new Cartesian3D(0.4, 0.4, 0.4), + new Cartesian3D(0.4, 0.4, -0.4), + new Cartesian3D(0.4, -0.4, 0.4), + new Cartesian3D(0.4, -0.4, -0.4), + new Cartesian3D(-0.4, 0.4, 0.4), + new Cartesian3D(-0.4, 0.4, -0.4), + new Cartesian3D(-0.4, -0.4, 0.4), + new Cartesian3D(-0.4, -0.4, -0.4)); + } + + @Test + public void testCreateFromBoundaries_twoBoxes_disjoint() { + // arrange + List> boundaries = new ArrayList<>(); + boundaries.addAll(createBoxBoundaries(Cartesian3D.ZERO, 1.0, TEST_TOLERANCE)); + boundaries.addAll(createBoxBoundaries(new Cartesian3D(2, 0, 0), 1.0, TEST_TOLERANCE)); + + // act + PolyhedronsSet polySet = new PolyhedronsSet(boundaries, TEST_TOLERANCE); + + // assert + Assert.assertEquals(TEST_TOLERANCE, polySet.getTolerance(), Precision.EPSILON); + Assert.assertEquals(2.0, polySet.getSize(), TEST_TOLERANCE); + Assert.assertEquals(12.0, polySet.getBoundarySize(), TEST_TOLERANCE); + GeometryTestUtils.assertVectorEquals(new Cartesian3D(1, 0, 0), (Cartesian3D) polySet.getBarycenter(), TEST_TOLERANCE); + Assert.assertEquals(false, polySet.isEmpty()); + Assert.assertEquals(false, polySet.isFull()); + + checkPoints(Region.Location.OUTSIDE, polySet, + new Cartesian3D(-1, 0, 0), + new Cartesian3D(1, 0, 0), + new Cartesian3D(3, 0, 0)); + + checkPoints(Region.Location.INSIDE, polySet, + new Cartesian3D(0, 0, 0), + new Cartesian3D(2, 0, 0)); + } + + @Test + public void testCreateFromBoundaries_twoBoxes_sharedSide() { + // arrange + List> boundaries = new ArrayList<>(); + boundaries.addAll(createBoxBoundaries(new Cartesian3D(0, 0, 0), 1.0, TEST_TOLERANCE)); + boundaries.addAll(createBoxBoundaries(new Cartesian3D(1, 0, 0), 1.0, TEST_TOLERANCE)); + + // act + PolyhedronsSet polySet = new PolyhedronsSet(boundaries, TEST_TOLERANCE); + + // assert + Assert.assertEquals(TEST_TOLERANCE, polySet.getTolerance(), Precision.EPSILON); + Assert.assertEquals(2.0, polySet.getSize(), TEST_TOLERANCE); + Assert.assertEquals(10.0, polySet.getBoundarySize(), TEST_TOLERANCE); + GeometryTestUtils.assertVectorEquals(new Cartesian3D(0.5, 0, 0), (Cartesian3D) polySet.getBarycenter(), TEST_TOLERANCE); + Assert.assertEquals(false, polySet.isEmpty()); + Assert.assertEquals(false, polySet.isFull()); + + checkPoints(Region.Location.OUTSIDE, polySet, + new Cartesian3D(-1, 0, 0), + new Cartesian3D(2, 0, 0)); + + checkPoints(Region.Location.INSIDE, polySet, + new Cartesian3D(0, 0, 0), + new Cartesian3D(1, 0, 0)); + } + + @Test + public void testCreateFromBoundaries_twoBoxes_separationLessThanTolerance() { + // arrange + double tolerance = 1e-6; + List> boundaries = new ArrayList<>(); + boundaries.addAll(createBoxBoundaries(new Cartesian3D(0, 0, 0), 1.0, tolerance)); + boundaries.addAll(createBoxBoundaries(new Cartesian3D(1 + 1e-7, 0, 0), 1.0, tolerance)); + + // act + PolyhedronsSet polySet = new PolyhedronsSet(boundaries, tolerance); + + // assert + Assert.assertEquals(tolerance, polySet.getTolerance(), Precision.EPSILON); + Assert.assertEquals(2.0, polySet.getSize(), tolerance); + Assert.assertEquals(10.0, polySet.getBoundarySize(), tolerance); + GeometryTestUtils.assertVectorEquals(new Cartesian3D(0.5 + 5e-8, 0, 0), (Cartesian3D) polySet.getBarycenter(), TEST_TOLERANCE); + Assert.assertEquals(false, polySet.isEmpty()); + Assert.assertEquals(false, polySet.isFull()); + + checkPoints(Region.Location.OUTSIDE, polySet, + new Cartesian3D(-1, 0, 0), + new Cartesian3D(2, 0, 0)); + + checkPoints(Region.Location.INSIDE, polySet, + new Cartesian3D(0, 0, 0), + new Cartesian3D(1, 0, 0)); + } + + @Test + public void testCreateFromBoundaries_twoBoxes_sharedEdge() { + // arrange + List> boundaries = new ArrayList<>(); + boundaries.addAll(createBoxBoundaries(new Cartesian3D(0, 0, 0), 1.0, TEST_TOLERANCE)); + boundaries.addAll(createBoxBoundaries(new Cartesian3D(1, 1, 0), 1.0, TEST_TOLERANCE)); + + // act + PolyhedronsSet polySet = new PolyhedronsSet(boundaries, TEST_TOLERANCE); + + // assert + Assert.assertEquals(TEST_TOLERANCE, polySet.getTolerance(), Precision.EPSILON); + Assert.assertEquals(2.0, polySet.getSize(), TEST_TOLERANCE); + Assert.assertEquals(12.0, polySet.getBoundarySize(), TEST_TOLERANCE); + GeometryTestUtils.assertVectorEquals(new Cartesian3D(0.5, 0.5, 0), (Cartesian3D) polySet.getBarycenter(), TEST_TOLERANCE); + Assert.assertEquals(false, polySet.isEmpty()); + Assert.assertEquals(false, polySet.isFull()); + + checkPoints(Region.Location.OUTSIDE, polySet, + new Cartesian3D(-1, 0, 0), + new Cartesian3D(1, 0, 0), + new Cartesian3D(0, 1, 0), + new Cartesian3D(2, 1, 0)); + + checkPoints(Region.Location.INSIDE, polySet, + new Cartesian3D(0, 0, 0), + new Cartesian3D(1, 1, 0)); + } + + @Test + public void testCreateFromBoundaries_twoBoxes_sharedPoint() { + // arrange + List> boundaries = new ArrayList<>(); + boundaries.addAll(createBoxBoundaries(new Cartesian3D(0, 0, 0), 1.0, TEST_TOLERANCE)); + boundaries.addAll(createBoxBoundaries(new Cartesian3D(1, 1, 1), 1.0, TEST_TOLERANCE)); + + // act + PolyhedronsSet polySet = new PolyhedronsSet(boundaries, TEST_TOLERANCE); + + // assert + Assert.assertEquals(TEST_TOLERANCE, polySet.getTolerance(), Precision.EPSILON); + Assert.assertEquals(2.0, polySet.getSize(), TEST_TOLERANCE); + Assert.assertEquals(12.0, polySet.getBoundarySize(), TEST_TOLERANCE); + GeometryTestUtils.assertVectorEquals(new Cartesian3D(0.5, 0.5, 0.5), (Cartesian3D) polySet.getBarycenter(), TEST_TOLERANCE); + Assert.assertEquals(false, polySet.isEmpty()); + Assert.assertEquals(false, polySet.isFull()); + + checkPoints(Region.Location.OUTSIDE, polySet, + new Cartesian3D(-1, 0, 0), + new Cartesian3D(1, 0, 0), + new Cartesian3D(0, 1, 1), + new Cartesian3D(2, 1, 1)); + + checkPoints(Region.Location.INSIDE, polySet, + new Cartesian3D(0, 0, 0), + new Cartesian3D(1, 1, 1)); + } + + @Test + public void testCreateBox() { + // act + PolyhedronsSet tree = new PolyhedronsSet(0, 1, 0, 1, 0, 1, TEST_TOLERANCE); + + // assert + Assert.assertEquals(1.0, tree.getSize(), TEST_TOLERANCE); + Assert.assertEquals(6.0, tree.getBoundarySize(), TEST_TOLERANCE); + GeometryTestUtils.assertVectorEquals(new Cartesian3D(0.5, 0.5, 0.5), (Cartesian3D) tree.getBarycenter(), TEST_TOLERANCE); + for (double x = -0.25; x < 1.25; x += 0.1) { boolean xOK = (x >= 0.0) && (x <= 1.0); for (double y = -0.25; y < 1.25; y += 0.1) { @@ -216,22 +461,25 @@ public class PolyhedronsSetTest { @Test public void testTetrahedron() throws MathArithmeticException { + // arrange Cartesian3D vertex1 = new Cartesian3D(1, 2, 3); Cartesian3D vertex2 = new Cartesian3D(2, 2, 4); Cartesian3D vertex3 = new Cartesian3D(2, 3, 3); Cartesian3D vertex4 = new Cartesian3D(1, 3, 4); + + // act PolyhedronsSet tree = (PolyhedronsSet) new RegionFactory().buildConvex( - new Plane(vertex3, vertex2, vertex1, 1.0e-10), - new Plane(vertex2, vertex3, vertex4, 1.0e-10), - new Plane(vertex4, vertex3, vertex1, 1.0e-10), - new Plane(vertex1, vertex2, vertex4, 1.0e-10)); - Assert.assertEquals(1.0 / 3.0, tree.getSize(), 1.0e-10); - Assert.assertEquals(2.0 * FastMath.sqrt(3.0), tree.getBoundarySize(), 1.0e-10); - Cartesian3D barycenter = (Cartesian3D) tree.getBarycenter(); - Assert.assertEquals(1.5, barycenter.getX(), 1.0e-10); - Assert.assertEquals(2.5, barycenter.getY(), 1.0e-10); - Assert.assertEquals(3.5, barycenter.getZ(), 1.0e-10); + new Plane(vertex3, vertex2, vertex1, TEST_TOLERANCE), + new Plane(vertex2, vertex3, vertex4, TEST_TOLERANCE), + new Plane(vertex4, vertex3, vertex1, TEST_TOLERANCE), + new Plane(vertex1, vertex2, vertex4, TEST_TOLERANCE)); + + // assert + Assert.assertEquals(1.0 / 3.0, tree.getSize(), TEST_TOLERANCE); + Assert.assertEquals(2.0 * FastMath.sqrt(3.0), tree.getBoundarySize(), TEST_TOLERANCE); + GeometryTestUtils.assertVectorEquals(new Cartesian3D(1.5, 2.5, 3.5), (Cartesian3D) tree.getBarycenter(), TEST_TOLERANCE); + double third = 1.0 / 3.0; checkPoints(Region.Location.BOUNDARY, tree, new Cartesian3D[] { vertex1, vertex2, vertex3, vertex4, @@ -248,18 +496,60 @@ public class PolyhedronsSetTest { }); } + @Test + public void testSphere() { + // arrange + // (use a high tolerance value here since the sphere is only an approximation) + double approximationTolerance = 0.2; + double radius = 1.0; + + // act + PolyhedronsSet polySet = createSphere(new Cartesian3D(1, 2, 3), radius, 8, 16); + + // assert + Assert.assertEquals(sphereVolume(radius), polySet.getSize(), approximationTolerance); + Assert.assertEquals(sphereSurface(radius), polySet.getBoundarySize(), approximationTolerance); + GeometryTestUtils.assertVectorEquals(new Cartesian3D(1, 2, 3), (Cartesian3D) polySet.getBarycenter(), TEST_TOLERANCE); + Assert.assertEquals(false, polySet.isEmpty()); + Assert.assertEquals(false, polySet.isFull()); + + checkPoints(Region.Location.OUTSIDE, polySet, + new Cartesian3D(-0.1, 2, 3), + new Cartesian3D(2.1, 2, 3), + new Cartesian3D(1, 0.9, 3), + new Cartesian3D(1, 3.1, 3), + new Cartesian3D(1, 2, 1.9), + new Cartesian3D(1, 2, 4.1), + new Cartesian3D(1.6, 2.6, 3.6)); + + checkPoints(Region.Location.INSIDE, polySet, + new Cartesian3D(1, 2, 3), + new Cartesian3D(0.1, 2, 3), + new Cartesian3D(1.9, 2, 3), + new Cartesian3D(1, 2.1, 3), + new Cartesian3D(1, 2.9, 3), + new Cartesian3D(1, 2, 2.1), + new Cartesian3D(1, 2, 3.9), + new Cartesian3D(1.5, 2.5, 3.5)); + } + @Test public void testIsometry() throws MathArithmeticException, MathIllegalArgumentException { + // arrange Cartesian3D vertex1 = new Cartesian3D(1.1, 2.2, 3.3); Cartesian3D vertex2 = new Cartesian3D(2.0, 2.4, 4.2); Cartesian3D vertex3 = new Cartesian3D(2.8, 3.3, 3.7); Cartesian3D vertex4 = new Cartesian3D(1.0, 3.6, 4.5); + + // act PolyhedronsSet tree = (PolyhedronsSet) new RegionFactory().buildConvex( - new Plane(vertex3, vertex2, vertex1, 1.0e-10), - new Plane(vertex2, vertex3, vertex4, 1.0e-10), - new Plane(vertex4, vertex3, vertex1, 1.0e-10), - new Plane(vertex1, vertex2, vertex4, 1.0e-10)); + new Plane(vertex3, vertex2, vertex1, TEST_TOLERANCE), + new Plane(vertex2, vertex3, vertex4, TEST_TOLERANCE), + new Plane(vertex4, vertex3, vertex1, TEST_TOLERANCE), + new Plane(vertex1, vertex2, vertex4, TEST_TOLERANCE)); + + // assert Cartesian3D barycenter = (Cartesian3D) tree.getBarycenter(); Cartesian3D s = new Cartesian3D(10.2, 4.3, -6.7); Cartesian3D c = new Cartesian3D(-0.2, 2.1, -3.2); @@ -272,8 +562,8 @@ public class PolyhedronsSetTest { 1.0, c, 1.0, r.applyTo(barycenter.subtract(c))); Assert.assertEquals(0.0, - newB.subtract((Vector) tree.getBarycenter()).getNorm(), - 1.0e-10); + newB.subtract((Cartesian3D) tree.getBarycenter()).getNorm(), + TEST_TOLERANCE); final Cartesian3D[] expectedV = new Cartesian3D[] { new Cartesian3D(1.0, s, @@ -324,7 +614,7 @@ public class PolyhedronsSetTest { for (int k = 0; k < expectedV.length; ++k) { d = FastMath.min(d, v.subtract(expectedV[k]).getNorm()); } - Assert.assertEquals(0, d, 1.0e-10); + Assert.assertEquals(0, d, TEST_TOLERANCE); } } @@ -334,49 +624,53 @@ public class PolyhedronsSetTest { @Test public void testBuildBox() { + // arrange double x = 1.0; double y = 2.0; double z = 3.0; double w = 0.1; double l = 1.0; + + // act PolyhedronsSet tree = - new PolyhedronsSet(x - l, x + l, y - w, y + w, z - w, z + w, 1.0e-10); - Cartesian3D barycenter = (Cartesian3D) tree.getBarycenter(); - Assert.assertEquals(x, barycenter.getX(), 1.0e-10); - Assert.assertEquals(y, barycenter.getY(), 1.0e-10); - Assert.assertEquals(z, barycenter.getZ(), 1.0e-10); - Assert.assertEquals(8 * l * w * w, tree.getSize(), 1.0e-10); - Assert.assertEquals(8 * w * (2 * l + w), tree.getBoundarySize(), 1.0e-10); + new PolyhedronsSet(x - l, x + l, y - w, y + w, z - w, z + w, TEST_TOLERANCE); + + // assert + GeometryTestUtils.assertVectorEquals(new Cartesian3D(x, y, z), (Cartesian3D) tree.getBarycenter(), TEST_TOLERANCE); + Assert.assertEquals(8 * l * w * w, tree.getSize(), TEST_TOLERANCE); + Assert.assertEquals(8 * w * (2 * l + w), tree.getBoundarySize(), TEST_TOLERANCE); } @Test public void testCross() { - + // arrange double x = 1.0; double y = 2.0; double z = 3.0; double w = 0.1; double l = 1.0; PolyhedronsSet xBeam = - new PolyhedronsSet(x - l, x + l, y - w, y + w, z - w, z + w, 1.0e-10); + new PolyhedronsSet(x - l, x + l, y - w, y + w, z - w, z + w, TEST_TOLERANCE); PolyhedronsSet yBeam = - new PolyhedronsSet(x - w, x + w, y - l, y + l, z - w, z + w, 1.0e-10); + new PolyhedronsSet(x - w, x + w, y - l, y + l, z - w, z + w, TEST_TOLERANCE); PolyhedronsSet zBeam = - new PolyhedronsSet(x - w, x + w, y - w, y + w, z - l, z + l, 1.0e-10); + new PolyhedronsSet(x - w, x + w, y - w, y + w, z - l, z + l, TEST_TOLERANCE); RegionFactory factory = new RegionFactory<>(); + + // act PolyhedronsSet tree = (PolyhedronsSet) factory.union(xBeam, factory.union(yBeam, zBeam)); - Cartesian3D barycenter = (Cartesian3D) tree.getBarycenter(); - - Assert.assertEquals(x, barycenter.getX(), 1.0e-10); - Assert.assertEquals(y, barycenter.getY(), 1.0e-10); - Assert.assertEquals(z, barycenter.getZ(), 1.0e-10); - Assert.assertEquals(8 * w * w * (3 * l - 2 * w), tree.getSize(), 1.0e-10); - Assert.assertEquals(24 * w * (2 * l - w), tree.getBoundarySize(), 1.0e-10); + // assert + GeometryTestUtils.assertVectorEquals(new Cartesian3D(x, y, z), (Cartesian3D) tree.getBarycenter(), TEST_TOLERANCE); + Assert.assertEquals(8 * w * w * (3 * l - 2 * w), tree.getSize(), TEST_TOLERANCE); + Assert.assertEquals(24 * w * (2 * l - w), tree.getBoundarySize(), TEST_TOLERANCE); } + // Issue MATH-780 + // See https://issues.apache.org/jira/browse/MATH-780 @Test - public void testIssue780() throws MathArithmeticException { + public void testCreateFromBoundaries_handlesSmallBoundariesCreatedDuringConstruction() throws MathArithmeticException { + // arrange float[] coords = { 1.000000f, -1.000000f, -1.000000f, 1.000000f, -1.000000f, 1.000000f, @@ -402,7 +696,7 @@ public class PolyhedronsSetTest { Cartesian3D v_2 = new Cartesian3D(coords[idxB], coords[idxB + 1], coords[idxB + 2]); Cartesian3D v_3 = new Cartesian3D(coords[idxC], coords[idxC + 1], coords[idxC + 2]); Cartesian3D[] vertices = {v_1, v_2, v_3}; - Plane polyPlane = new Plane(v_1, v_2, v_3, 1.0e-10); + Plane polyPlane = new Plane(v_1, v_2, v_3, TEST_TOLERANCE); ArrayList> lines = new ArrayList<>(); Cartesian2D[] projPts = new Cartesian2D[vertices.length]; @@ -412,23 +706,29 @@ public class PolyhedronsSetTest { SubLine lineInPlane = null; for (int ptIdx = 0; ptIdx < projPts.length; ptIdx++) { - lineInPlane = new SubLine(projPts[ptIdx], projPts[(ptIdx + 1) % projPts.length], 1.0e-10); + lineInPlane = new SubLine(projPts[ptIdx], projPts[(ptIdx + 1) % projPts.length], TEST_TOLERANCE); lines.add(lineInPlane); } - Region polyRegion = new PolygonsSet(lines, 1.0e-10); + Region polyRegion = new PolygonsSet(lines, TEST_TOLERANCE); SubPlane polygon = new SubPlane(polyPlane, polyRegion); subHyperplaneList.add(polygon); } - PolyhedronsSet polyhedronsSet = new PolyhedronsSet(subHyperplaneList, 1.0e-10); - Assert.assertEquals( 8.0, polyhedronsSet.getSize(), 3.0e-6); + + // act + PolyhedronsSet polyhedronsSet = new PolyhedronsSet(subHyperplaneList, TEST_TOLERANCE); + + // assert + Assert.assertEquals(8.0, polyhedronsSet.getSize(), 3.0e-6); Assert.assertEquals(24.0, polyhedronsSet.getBoundarySize(), 5.0e-6); } @Test public void testTooThinBox() { - Assert.assertEquals(0.0, - new PolyhedronsSet(0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0e-10).getSize(), - 1.0e-10); + // act + PolyhedronsSet polyhedronsSet = new PolyhedronsSet(0.0, 0.0, 0.0, 1.0, 0.0, 1.0, TEST_TOLERANCE); + + // assert + Assert.assertEquals(0.0, polyhedronsSet.getSize(), TEST_TOLERANCE); } @Test @@ -436,7 +736,7 @@ public class PolyhedronsSetTest { // the following is a wrong usage of the constructor. // as explained in the javadoc, the failure is NOT detected at construction // time but occurs later on - PolyhedronsSet ps = new PolyhedronsSet(new BSPTree(), 1.0e-10); + PolyhedronsSet ps = new PolyhedronsSet(new BSPTree(), TEST_TOLERANCE); Assert.assertNotNull(ps); try { ps.checkPoint(Cartesian3D.ZERO); @@ -448,86 +748,91 @@ public class PolyhedronsSetTest { @Test public void testDumpParse() throws IOException, ParseException { + // arrange double tol=1e-8; - Cartesian3D[] verts=new Cartesian3D[8]; - double xmin=-1,xmax=1; - double ymin=-1,ymax=1; - double zmin=-1,zmax=1; - verts[0]=new Cartesian3D(xmin,ymin,zmin); - verts[1]=new Cartesian3D(xmax,ymin,zmin); - verts[2]=new Cartesian3D(xmax,ymax,zmin); - verts[3]=new Cartesian3D(xmin,ymax,zmin); - verts[4]=new Cartesian3D(xmin,ymin,zmax); - verts[5]=new Cartesian3D(xmax,ymin,zmax); - verts[6]=new Cartesian3D(xmax,ymax,zmax); - verts[7]=new Cartesian3D(xmin,ymax,zmax); - // - int[][] faces=new int[12][]; - faces[0]=new int[]{3,1,0}; // bottom (-z) - faces[1]=new int[]{1,3,2}; // bottom (-z) - faces[2]=new int[]{5,7,4}; // top (+z) - faces[3]=new int[]{7,5,6}; // top (+z) - faces[4]=new int[]{2,5,1}; // right (+x) - faces[5]=new int[]{5,2,6}; // right (+x) - faces[6]=new int[]{4,3,0}; // left (-x) - faces[7]=new int[]{3,4,7}; // left (-x) - faces[8]=new int[]{4,1,5}; // front (-y) - faces[9]=new int[]{1,4,0}; // front (-y) - faces[10]=new int[]{3,6,2}; // back (+y) - faces[11]=new int[]{6,3,7}; // back (+y) - PolyhedronsSet polyset = new PolyhedronsSet(Arrays.asList(verts), Arrays.asList(faces), tol); - Assert.assertEquals(8.0, polyset.getSize(), 1.0e-10); - Assert.assertEquals(24.0, polyset.getBoundarySize(), 1.0e-10); - String dump = RegionDumper.dump(polyset); - PolyhedronsSet parsed = RegionParser.parsePolyhedronsSet(dump); - Assert.assertEquals(8.0, parsed.getSize(), 1.0e-10); - Assert.assertEquals(24.0, parsed.getBoundarySize(), 1.0e-10); - Assert.assertTrue(new RegionFactory().difference(polyset, parsed).isEmpty()); + Cartesian3D[] verts=new Cartesian3D[8]; + double xmin=-1,xmax=1; + double ymin=-1,ymax=1; + double zmin=-1,zmax=1; + verts[0]=new Cartesian3D(xmin,ymin,zmin); + verts[1]=new Cartesian3D(xmax,ymin,zmin); + verts[2]=new Cartesian3D(xmax,ymax,zmin); + verts[3]=new Cartesian3D(xmin,ymax,zmin); + verts[4]=new Cartesian3D(xmin,ymin,zmax); + verts[5]=new Cartesian3D(xmax,ymin,zmax); + verts[6]=new Cartesian3D(xmax,ymax,zmax); + verts[7]=new Cartesian3D(xmin,ymax,zmax); + // + int[][] faces=new int[12][]; + faces[0]=new int[]{3,1,0}; // bottom (-z) + faces[1]=new int[]{1,3,2}; // bottom (-z) + faces[2]=new int[]{5,7,4}; // top (+z) + faces[3]=new int[]{7,5,6}; // top (+z) + faces[4]=new int[]{2,5,1}; // right (+x) + faces[5]=new int[]{5,2,6}; // right (+x) + faces[6]=new int[]{4,3,0}; // left (-x) + faces[7]=new int[]{3,4,7}; // left (-x) + faces[8]=new int[]{4,1,5}; // front (-y) + faces[9]=new int[]{1,4,0}; // front (-y) + faces[10]=new int[]{3,6,2}; // back (+y) + faces[11]=new int[]{6,3,7}; // back (+y) + + PolyhedronsSet polyset = new PolyhedronsSet(Arrays.asList(verts), Arrays.asList(faces), tol); + + // act + String dump = RegionDumper.dump(polyset); + PolyhedronsSet parsed = RegionParser.parsePolyhedronsSet(dump); + + // assert + Assert.assertEquals(8.0, polyset.getSize(), TEST_TOLERANCE); + Assert.assertEquals(24.0, polyset.getBoundarySize(), TEST_TOLERANCE); + + Assert.assertEquals(8.0, parsed.getSize(), TEST_TOLERANCE); + Assert.assertEquals(24.0, parsed.getBoundarySize(), TEST_TOLERANCE); + Assert.assertTrue(new RegionFactory().difference(polyset, parsed).isEmpty()); } @Test - public void testConnectedFacets() throws IOException, ParseException { + public void testCreateFromBRep_connectedFacets() throws IOException, ParseException { InputStream stream = getClass().getResourceAsStream("pentomino-N.ply"); PLYParser parser = new PLYParser(stream); stream.close(); - PolyhedronsSet polyhedron = new PolyhedronsSet(parser.getVertices(), parser.getFaces(), 1.0e-10); - Assert.assertEquals( 5.0, polyhedron.getSize(), 1.0e-10); - Assert.assertEquals(22.0, polyhedron.getBoundarySize(), 1.0e-10); + PolyhedronsSet polyhedron = new PolyhedronsSet(parser.getVertices(), parser.getFaces(), TEST_TOLERANCE); + Assert.assertEquals( 5.0, polyhedron.getSize(), TEST_TOLERANCE); + Assert.assertEquals(22.0, polyhedron.getBoundarySize(), TEST_TOLERANCE); } @Test - public void testTooClose() throws IOException, ParseException { + public void testCreateFromBRep_verticesTooClose() throws IOException, ParseException { checkError("pentomino-N-too-close.ply", LocalizedFormats.CLOSE_VERTICES); } @Test - public void testHole() throws IOException, ParseException { + public void testCreateFromBRep_hole() throws IOException, ParseException { checkError("pentomino-N-hole.ply", LocalizedFormats.EDGE_CONNECTED_TO_ONE_FACET); } @Test - public void testNonPlanar() throws IOException, ParseException { + public void testCreateFromBRep_nonPlanar() throws IOException, ParseException { checkError("pentomino-N-out-of-plane.ply", LocalizedFormats.OUT_OF_PLANE); } @Test - public void testOrientation() throws IOException, ParseException { + public void testCreateFromBRep_badOrientation() throws IOException, ParseException { checkError("pentomino-N-bad-orientation.ply", LocalizedFormats.FACET_ORIENTATION_MISMATCH); } @Test - public void testFacet2Vertices() throws IOException, ParseException { + public void testCreateFromBRep_wrongNumberOfPoints() throws IOException, ParseException { checkError(Arrays.asList(Cartesian3D.ZERO, Cartesian3D.PLUS_I, Cartesian3D.PLUS_J, Cartesian3D.PLUS_K), Arrays.asList(new int[] { 0, 1, 2 }, new int[] {2, 3}), LocalizedFormats.WRONG_NUMBER_OF_POINTS); } private void checkError(final String resourceName, final LocalizedFormats expected) { - try { - InputStream stream = getClass().getResourceAsStream(resourceName); + try (InputStream stream = getClass().getResourceAsStream(resourceName)) { PLYParser parser = new PLYParser(stream); - stream.close(); checkError(parser.getVertices(), parser.getFaces(), expected); } catch (IOException ioe) { Assert.fail(ioe.getLocalizedMessage()); @@ -539,7 +844,7 @@ public class PolyhedronsSetTest { private void checkError(final List vertices, final List facets, final LocalizedFormats expected) { try { - new PolyhedronsSet(vertices, facets, 1.0e-10); + new PolyhedronsSet(vertices, facets, TEST_TOLERANCE); Assert.fail("an exception should have been thrown"); } catch (MathIllegalArgumentException miae) { try { @@ -557,10 +862,61 @@ public class PolyhedronsSetTest { } @Test - public void testIssue1211() throws IOException, ParseException { + public void testFirstIntersection() { + // arrange + List> boundaries = createBoxBoundaries(Cartesian3D.ZERO, 2.0, TEST_TOLERANCE); + PolyhedronsSet polySet = new PolyhedronsSet(boundaries, TEST_TOLERANCE); + Line xPlus = new Line(Cartesian3D.ZERO, Cartesian3D.PLUS_I, TEST_TOLERANCE); + Line xMinus = new Line(Cartesian3D.ZERO, Cartesian3D.MINUS_I, TEST_TOLERANCE); + + Line yPlus = new Line(Cartesian3D.ZERO, Cartesian3D.PLUS_J, TEST_TOLERANCE); + Line yMinus = new Line(Cartesian3D.ZERO, Cartesian3D.MINUS_J, TEST_TOLERANCE); + + Line zPlus = new Line(Cartesian3D.ZERO, Cartesian3D.PLUS_K, TEST_TOLERANCE); + Line zMinus = new Line(Cartesian3D.ZERO, Cartesian3D.MINUS_K, TEST_TOLERANCE); + + // act/assert + assertSubPlaneNormal(new Cartesian3D(-1, 0, 0), polySet.firstIntersection(new Cartesian3D(-1.1, 0, 0), xPlus)); + assertSubPlaneNormal(new Cartesian3D(-1, 0, 0), polySet.firstIntersection(new Cartesian3D(-1, 0, 0), xPlus)); + assertSubPlaneNormal(new Cartesian3D(1, 0, 0), polySet.firstIntersection(new Cartesian3D(-0.9, 0, 0), xPlus)); + Assert.assertEquals(null, polySet.firstIntersection(new Cartesian3D(1.1, 0, 0), xPlus)); + + assertSubPlaneNormal(new Cartesian3D(1, 0, 0), polySet.firstIntersection(new Cartesian3D(1.1, 0, 0), xMinus)); + assertSubPlaneNormal(new Cartesian3D(1, 0, 0), polySet.firstIntersection(new Cartesian3D(1, 0, 0), xMinus)); + assertSubPlaneNormal(new Cartesian3D(-1, 0, 0), polySet.firstIntersection(new Cartesian3D(0.9, 0, 0), xMinus)); + Assert.assertEquals(null, polySet.firstIntersection(new Cartesian3D(-1.1, 0, 0), xMinus)); + + assertSubPlaneNormal(new Cartesian3D(0, -1, 0), polySet.firstIntersection(new Cartesian3D(0, -1.1, 0), yPlus)); + assertSubPlaneNormal(new Cartesian3D(0, -1, 0), polySet.firstIntersection(new Cartesian3D(0, -1, 0), yPlus)); + assertSubPlaneNormal(new Cartesian3D(0, 1, 0), polySet.firstIntersection(new Cartesian3D(0, -0.9, 0), yPlus)); + Assert.assertEquals(null, polySet.firstIntersection(new Cartesian3D(0, 1.1, 0), yPlus)); + + assertSubPlaneNormal(new Cartesian3D(0, 1, 0), polySet.firstIntersection(new Cartesian3D(0, 1.1, 0), yMinus)); + assertSubPlaneNormal(new Cartesian3D(0, 1, 0), polySet.firstIntersection(new Cartesian3D(0, 1, 0), yMinus)); + assertSubPlaneNormal(new Cartesian3D(0, -1, 0), polySet.firstIntersection(new Cartesian3D(0, 0.9, 0), yMinus)); + Assert.assertEquals(null, polySet.firstIntersection(new Cartesian3D(0, -1.1, 0), yMinus)); + + assertSubPlaneNormal(new Cartesian3D(0, 0, -1), polySet.firstIntersection(new Cartesian3D(0, 0, -1.1), zPlus)); + assertSubPlaneNormal(new Cartesian3D(0, 0, -1), polySet.firstIntersection(new Cartesian3D(0, 0, -1), zPlus)); + assertSubPlaneNormal(new Cartesian3D(0, 0, 1), polySet.firstIntersection(new Cartesian3D(0, 0, -0.9), zPlus)); + Assert.assertEquals(null, polySet.firstIntersection(new Cartesian3D(0, 0, 1.1), zPlus)); + + assertSubPlaneNormal(new Cartesian3D(0, 0, 1), polySet.firstIntersection(new Cartesian3D(0, 0, 1.1), zMinus)); + assertSubPlaneNormal(new Cartesian3D(0, 0, 1), polySet.firstIntersection(new Cartesian3D(0, 0, 1), zMinus)); + assertSubPlaneNormal(new Cartesian3D(0, 0, -1), polySet.firstIntersection(new Cartesian3D(0, 0, 0.9), zMinus)); + Assert.assertEquals(null, polySet.firstIntersection(new Cartesian3D(0, 0, -1.1), zMinus)); + } + + // Issue 1211 + // See https://issues.apache.org/jira/browse/MATH-1211 + @Test + public void testFirstIntersection_onlyReturnsPointsInDirectionOfRay() throws IOException, ParseException { + // arrange PolyhedronsSet polyset = RegionParser.parsePolyhedronsSet(loadTestData("issue-1211.bsp")); UniformRandomProvider random = RandomSource.create(RandomSource.WELL_1024_A, 0xb97c9d1ade21e40al); + + // act/assert int nrays = 1000; for (int i = 0; i < nrays; i++) { Cartesian3D origin = Cartesian3D.ZERO; @@ -577,21 +933,579 @@ public class PolyhedronsSetTest { } } + @Test + public void testBoolean_union() throws IOException { + // arrange + double tolerance = 0.05; + double size = 1.0; + double radius = size * 0.5; + PolyhedronsSet box = new PolyhedronsSet(0, size, 0, size, 0, size, TEST_TOLERANCE); + PolyhedronsSet sphere = createSphere(new Cartesian3D(size * 0.5, size * 0.5, size), radius, 8, 16); + + // act + PolyhedronsSet result = (PolyhedronsSet) new RegionFactory().union(box, sphere); + + // OBJWriter.write("union.obj", result); + + // assert + Assert.assertEquals(cubeVolume(size) + (sphereVolume(radius) * 0.5), + result.getSize(), tolerance); + Assert.assertEquals(cubeSurface(size) - circleSurface(radius) + (0.5 * sphereSurface(radius)), + result.getBoundarySize(), tolerance); + Assert.assertEquals(false, result.isEmpty()); + Assert.assertEquals(false, result.isFull()); + + checkPoints(Region.Location.OUTSIDE, result, + new Cartesian3D(-0.1, 0.5, 0.5), + new Cartesian3D(1.1, 0.5, 0.5), + new Cartesian3D(0.5, -0.1, 0.5), + new Cartesian3D(0.5, 1.1, 0.5), + new Cartesian3D(0.5, 0.5, -0.1), + new Cartesian3D(0.5, 0.5, 1.6)); + + checkPoints(Region.Location.INSIDE, result, + new Cartesian3D(0.1, 0.5, 0.5), + new Cartesian3D(0.9, 0.5, 0.5), + new Cartesian3D(0.5, 0.1, 0.5), + new Cartesian3D(0.5, 0.9, 0.5), + new Cartesian3D(0.5, 0.5, 0.1), + new Cartesian3D(0.5, 0.5, 1.4)); + } + + @Test + public void testUnion_self() { + // arrange + double tolerance = 0.2; + double radius = 1.0; + + PolyhedronsSet sphere = createSphere(Cartesian3D.ZERO, radius, 8, 16); + + // act + PolyhedronsSet result = (PolyhedronsSet) new RegionFactory().union(sphere, sphere.copySelf()); + + // assert + Assert.assertEquals(sphereVolume(radius), result.getSize(), tolerance); + Assert.assertEquals(sphereSurface(radius), result.getBoundarySize(), tolerance); + GeometryTestUtils.assertVectorEquals(Cartesian3D.ZERO, (Cartesian3D) result.getBarycenter(), TEST_TOLERANCE); + Assert.assertEquals(false, result.isEmpty()); + Assert.assertEquals(false, result.isFull()); + + checkPoints(Region.Location.OUTSIDE, result, + new Cartesian3D(-1.1, 0, 0), + new Cartesian3D(1.1, 0, 0), + new Cartesian3D(0, -1.1, 0), + new Cartesian3D(0, 1.1, 0), + new Cartesian3D(0, 0, -1.1), + new Cartesian3D(0, 0, 1.1)); + + checkPoints(Region.Location.INSIDE, result, + new Cartesian3D(-0.9, 0, 0), + new Cartesian3D(0.9, 0, 0), + new Cartesian3D(0, -0.9, 0), + new Cartesian3D(0, 0.9, 0), + new Cartesian3D(0, 0, -0.9), + new Cartesian3D(0, 0, 0.9), + Cartesian3D.ZERO); + } + + @Test + public void testBoolean_intersection() throws IOException { + // arrange + double tolerance = 0.05; + double size = 1.0; + double radius = size * 0.5; + PolyhedronsSet box = new PolyhedronsSet(0, size, 0, size, 0, size, TEST_TOLERANCE); + PolyhedronsSet sphere = createSphere(new Cartesian3D(size * 0.5, size * 0.5, size), radius, 8, 16); + + // act + PolyhedronsSet result = (PolyhedronsSet) new RegionFactory().intersection(box, sphere); + + // OBJWriter.write("intersection.obj", result); + + // assert + Assert.assertEquals((sphereVolume(radius) * 0.5), result.getSize(), tolerance); + Assert.assertEquals(circleSurface(radius) + (0.5 * sphereSurface(radius)), + result.getBoundarySize(), tolerance); + Assert.assertEquals(false, result.isEmpty()); + Assert.assertEquals(false, result.isFull()); + + checkPoints(Region.Location.OUTSIDE, result, + new Cartesian3D(-0.1, 0.5, 1.0), + new Cartesian3D(1.1, 0.5, 1.0), + new Cartesian3D(0.5, -0.1, 1.0), + new Cartesian3D(0.5, 1.1, 1.0), + new Cartesian3D(0.5, 0.5, 0.4), + new Cartesian3D(0.5, 0.5, 1.1)); + + checkPoints(Region.Location.INSIDE, result, + new Cartesian3D(0.1, 0.5, 0.9), + new Cartesian3D(0.9, 0.5, 0.9), + new Cartesian3D(0.5, 0.1, 0.9), + new Cartesian3D(0.5, 0.9, 0.9), + new Cartesian3D(0.5, 0.5, 0.6), + new Cartesian3D(0.5, 0.5, 0.9)); + } + + @Test + public void testIntersection_self() { + // arrange + double tolerance = 0.2; + double radius = 1.0; + + PolyhedronsSet sphere = createSphere(Cartesian3D.ZERO, radius, 8, 16); + + // act + PolyhedronsSet result = (PolyhedronsSet) new RegionFactory().intersection(sphere, sphere.copySelf()); + + // assert + Assert.assertEquals(sphereVolume(radius), result.getSize(), tolerance); + Assert.assertEquals(sphereSurface(radius), result.getBoundarySize(), tolerance); + GeometryTestUtils.assertVectorEquals(Cartesian3D.ZERO, (Cartesian3D) result.getBarycenter(), TEST_TOLERANCE); + Assert.assertEquals(false, result.isEmpty()); + Assert.assertEquals(false, result.isFull()); + + checkPoints(Region.Location.OUTSIDE, result, + new Cartesian3D(-1.1, 0, 0), + new Cartesian3D(1.1, 0, 0), + new Cartesian3D(0, -1.1, 0), + new Cartesian3D(0, 1.1, 0), + new Cartesian3D(0, 0, -1.1), + new Cartesian3D(0, 0, 1.1)); + + checkPoints(Region.Location.INSIDE, result, + new Cartesian3D(-0.9, 0, 0), + new Cartesian3D(0.9, 0, 0), + new Cartesian3D(0, -0.9, 0), + new Cartesian3D(0, 0.9, 0), + new Cartesian3D(0, 0, -0.9), + new Cartesian3D(0, 0, 0.9), + Cartesian3D.ZERO); + } + + @Test + public void testBoolean_xor_twoCubes() throws IOException { + // arrange + double size = 1.0; + PolyhedronsSet box1 = new PolyhedronsSet( + 0, size, + 0, size, + 0, size, TEST_TOLERANCE); + PolyhedronsSet box2 = new PolyhedronsSet( + 0.5, size + 0.5, + 0.5, size + 0.5, + 0.5, size + 0.5, TEST_TOLERANCE); + + // act + PolyhedronsSet result = (PolyhedronsSet) new RegionFactory().xor(box1, box2); + + // OBJWriter.write("xor_twoCubes.obj", result); + + Assert.assertEquals((2 * cubeVolume(size)) - (2 * cubeVolume(size * 0.5)), result.getSize(), TEST_TOLERANCE); + + // assert + Assert.assertEquals(2 * cubeSurface(size), result.getBoundarySize(), TEST_TOLERANCE); + Assert.assertEquals(false, result.isEmpty()); + Assert.assertEquals(false, result.isFull()); + + checkPoints(Region.Location.OUTSIDE, result, + new Cartesian3D(-0.1, -0.1, -0.1), + new Cartesian3D(0.75, 0.75, 0.75), + new Cartesian3D(1.6, 1.6, 1.6)); + + checkPoints(Region.Location.BOUNDARY, result, + new Cartesian3D(0, 0, 0), + new Cartesian3D(0.5, 0.5, 0.5), + new Cartesian3D(1, 1, 1), + new Cartesian3D(1.5, 1.5, 1.5)); + + checkPoints(Region.Location.INSIDE, result, + new Cartesian3D(0.1, 0.1, 0.1), + new Cartesian3D(0.4, 0.4, 0.4), + new Cartesian3D(1.1, 1.1, 1.1), + new Cartesian3D(1.4, 1.4, 1.4)); + } + + @Test + public void testBoolean_xor_cubeAndSphere() throws IOException { + // arrange + double tolerance = 0.05; + double size = 1.0; + double radius = size * 0.5; + PolyhedronsSet box = new PolyhedronsSet(0, size, 0, size, 0, size, TEST_TOLERANCE); + PolyhedronsSet sphere = createSphere(new Cartesian3D(size * 0.5, size * 0.5, size), radius, 8, 16); + + // act + PolyhedronsSet result = (PolyhedronsSet) new RegionFactory().xor(box, sphere); + + // OBJWriter.write("xor_cubeAndSphere.obj", result); + + Assert.assertEquals(cubeVolume(size), result.getSize(), tolerance); + + // assert + Assert.assertEquals(cubeSurface(size) + (sphereSurface(radius)), + result.getBoundarySize(), tolerance); + Assert.assertEquals(false, result.isEmpty()); + Assert.assertEquals(false, result.isFull()); + + checkPoints(Region.Location.OUTSIDE, result, + new Cartesian3D(-0.1, 0.5, 0.5), + new Cartesian3D(1.1, 0.5, 0.5), + new Cartesian3D(0.5, -0.1, 0.5), + new Cartesian3D(0.5, 1.1, 0.5), + new Cartesian3D(0.5, 0.5, -0.1), + new Cartesian3D(0.5, 0.5, 1.6), + new Cartesian3D(0.5, 0.5, 0.9)); + + checkPoints(Region.Location.INSIDE, result, + new Cartesian3D(0.1, 0.5, 0.5), + new Cartesian3D(0.9, 0.5, 0.5), + new Cartesian3D(0.5, 0.1, 0.5), + new Cartesian3D(0.5, 0.9, 0.5), + new Cartesian3D(0.5, 0.5, 0.1), + new Cartesian3D(0.5, 0.5, 1.4)); + } + + @Test + public void testXor_self() { + // arrange + double radius = 1.0; + + PolyhedronsSet sphere = createSphere(Cartesian3D.ZERO, radius, 8, 16); + + // act + PolyhedronsSet result = (PolyhedronsSet) new RegionFactory().xor(sphere, sphere.copySelf()); + + // assert + Assert.assertEquals(0.0, result.getSize(), TEST_TOLERANCE); + Assert.assertEquals(0.0, result.getBoundarySize(), TEST_TOLERANCE); + GeometryTestUtils.assertVectorEquals(Cartesian3D.NaN, (Cartesian3D) result.getBarycenter(), TEST_TOLERANCE); + Assert.assertEquals(true, result.isEmpty()); + Assert.assertEquals(false, result.isFull()); + + checkPoints(Region.Location.OUTSIDE, result, + new Cartesian3D(-1.1, 0, 0), + new Cartesian3D(1.1, 0, 0), + new Cartesian3D(0, -1.1, 0), + new Cartesian3D(0, 1.1, 0), + new Cartesian3D(0, 0, -1.1), + new Cartesian3D(0, 0, 1.1), + new Cartesian3D(-0.9, 0, 0), + new Cartesian3D(0.9, 0, 0), + new Cartesian3D(0, -0.9, 0), + new Cartesian3D(0, 0.9, 0), + new Cartesian3D(0, 0, -0.9), + new Cartesian3D(0, 0, 0.9), + Cartesian3D.ZERO); + } + + @Test + public void testBoolean_difference() throws IOException { + // arrange + double tolerance = 0.05; + double size = 1.0; + double radius = size * 0.5; + PolyhedronsSet box = new PolyhedronsSet(0, size, 0, size, 0, size, TEST_TOLERANCE); + PolyhedronsSet sphere = createSphere(new Cartesian3D(size * 0.5, size * 0.5, size), radius, 8, 16); + + // act + PolyhedronsSet result = (PolyhedronsSet) new RegionFactory().difference(box, sphere); + + // OBJWriter.write("difference.obj", result); + + // assert + Assert.assertEquals(cubeVolume(size) - (sphereVolume(radius) * 0.5), result.getSize(), tolerance); + Assert.assertEquals(cubeSurface(size) - circleSurface(radius) + (0.5 * sphereSurface(radius)), + result.getBoundarySize(), tolerance); + Assert.assertEquals(false, result.isEmpty()); + Assert.assertEquals(false, result.isFull()); + + checkPoints(Region.Location.OUTSIDE, result, + new Cartesian3D(-0.1, 0.5, 1.0), + new Cartesian3D(1.1, 0.5, 1.0), + new Cartesian3D(0.5, -0.1, 1.0), + new Cartesian3D(0.5, 1.1, 1.0), + new Cartesian3D(0.5, 0.5, -0.1), + new Cartesian3D(0.5, 0.5, 0.6)); + + checkPoints(Region.Location.INSIDE, result, + new Cartesian3D(0.1, 0.5, 0.4), + new Cartesian3D(0.9, 0.5, 0.4), + new Cartesian3D(0.5, 0.1, 0.4), + new Cartesian3D(0.5, 0.9, 0.4), + new Cartesian3D(0.5, 0.5, 0.1), + new Cartesian3D(0.5, 0.5, 0.4)); + } + + @Test + public void testDifference_self() { + // arrange + double radius = 1.0; + + PolyhedronsSet sphere = createSphere(Cartesian3D.ZERO, radius, 8, 16); + + // act + PolyhedronsSet result = (PolyhedronsSet) new RegionFactory().difference(sphere, sphere.copySelf()); + + // assert + Assert.assertEquals(0.0, result.getSize(), TEST_TOLERANCE); + Assert.assertEquals(0.0, result.getBoundarySize(), TEST_TOLERANCE); + GeometryTestUtils.assertVectorEquals(Cartesian3D.NaN, (Cartesian3D) result.getBarycenter(), TEST_TOLERANCE); + Assert.assertEquals(true, result.isEmpty()); + Assert.assertEquals(false, result.isFull()); + + checkPoints(Region.Location.OUTSIDE, result, + new Cartesian3D(-1.1, 0, 0), + new Cartesian3D(1.1, 0, 0), + new Cartesian3D(0, -1.1, 0), + new Cartesian3D(0, 1.1, 0), + new Cartesian3D(0, 0, -1.1), + new Cartesian3D(0, 0, 1.1), + new Cartesian3D(-0.9, 0, 0), + new Cartesian3D(0.9, 0, 0), + new Cartesian3D(0, -0.9, 0), + new Cartesian3D(0, 0.9, 0), + new Cartesian3D(0, 0, -0.9), + new Cartesian3D(0, 0, 0.9), + Cartesian3D.ZERO); + } + + @Test + public void testBoolean_multiple() throws IOException { + // arrange + double tolerance = 0.05; + double size = 1.0; + double radius = size * 0.5; + PolyhedronsSet box = new PolyhedronsSet(0, size, 0, size, 0, size, TEST_TOLERANCE); + PolyhedronsSet sphereToAdd = createSphere(new Cartesian3D(size * 0.5, size * 0.5, size), radius, 8, 16); + PolyhedronsSet sphereToRemove1 = createSphere(new Cartesian3D(size * 0.5, 0, size * 0.5), radius, 8, 16); + PolyhedronsSet sphereToRemove2 = createSphere(new Cartesian3D(size * 0.5, 1, size * 0.5), radius, 8, 16); + + RegionFactory factory = new RegionFactory(); + + // act + PolyhedronsSet result = (PolyhedronsSet) factory.union(box, sphereToAdd); + result = (PolyhedronsSet) factory.difference(result, sphereToRemove1); + result = (PolyhedronsSet) factory.difference(result, sphereToRemove2); + + // OBJWriter.write("multiple.obj", result); + + // assert + Assert.assertEquals(cubeVolume(size) - (sphereVolume(radius) * 0.5), + result.getSize(), tolerance); + Assert.assertEquals(cubeSurface(size) - (3.0 * circleSurface(radius)) + (1.5 * sphereSurface(radius)), + result.getBoundarySize(), tolerance); + Assert.assertEquals(false, result.isEmpty()); + Assert.assertEquals(false, result.isFull()); + + checkPoints(Region.Location.OUTSIDE, result, + new Cartesian3D(-0.1, 0.5, 0.5), + new Cartesian3D(1.1, 0.5, 0.5), + new Cartesian3D(0.5, 0.4, 0.5), + new Cartesian3D(0.5, 0.6, 0.5), + new Cartesian3D(0.5, 0.5, -0.1), + new Cartesian3D(0.5, 0.5, 1.6)); + + checkPoints(Region.Location.INSIDE, result, + new Cartesian3D(0.1, 0.5, 0.1), + new Cartesian3D(0.9, 0.5, 0.1), + new Cartesian3D(0.5, 0.4, 0.1), + new Cartesian3D(0.5, 0.6, 0.1), + new Cartesian3D(0.5, 0.5, 0.1), + new Cartesian3D(0.5, 0.5, 1.4)); + } + + @Test + public void testProjectToBoundary() { + // arrange + PolyhedronsSet polySet = new PolyhedronsSet(0, 1, 0, 1, 0, 1, TEST_TOLERANCE); + + // act/assert + checkProjectToBoundary(polySet, new Cartesian3D(0.4, 0.5, 0.5), + new Cartesian3D(0, 0.5, 0.5), -0.4); + checkProjectToBoundary(polySet, new Cartesian3D(1.5, 0.5, 0.5), + new Cartesian3D(1, 0.5, 0.5), 0.5); + checkProjectToBoundary(polySet, new Cartesian3D(2, 2, 2), + new Cartesian3D(1, 1, 1), FastMath.sqrt(3)); + } + + @Test + public void testProjectToBoundary_invertedRegion() { + // arrange + PolyhedronsSet polySet = new PolyhedronsSet(0, 1, 0, 1, 0, 1, TEST_TOLERANCE); + polySet = (PolyhedronsSet) new RegionFactory().getComplement(polySet); + + // act/assert + checkProjectToBoundary(polySet, new Cartesian3D(0.4, 0.5, 0.5), + new Cartesian3D(0, 0.5, 0.5), 0.4); + checkProjectToBoundary(polySet, new Cartesian3D(1.5, 0.5, 0.5), + new Cartesian3D(1, 0.5, 0.5), -0.5); + checkProjectToBoundary(polySet, new Cartesian3D(2, 2, 2), + new Cartesian3D(1, 1, 1), -FastMath.sqrt(3)); + } + + private void checkProjectToBoundary(PolyhedronsSet poly, Cartesian3D toProject, + Cartesian3D expectedPoint, double expectedOffset) { + BoundaryProjection proj = poly.projectToBoundary(toProject); + + GeometryTestUtils.assertVectorEquals(toProject, (Cartesian3D) proj.getOriginal(), TEST_TOLERANCE); + GeometryTestUtils.assertVectorEquals(expectedPoint, (Cartesian3D) proj.getProjected(), TEST_TOLERANCE); + Assert.assertEquals(expectedOffset, proj.getOffset(), TEST_TOLERANCE); + } + private String loadTestData(final String resourceName) throws IOException { - InputStream stream = getClass().getResourceAsStream(resourceName); - Reader reader = new InputStreamReader(stream, "UTF-8"); + try (Reader reader = new InputStreamReader(getClass().getResourceAsStream(resourceName), "UTF-8")) { StringBuilder builder = new StringBuilder(); for (int c = reader.read(); c >= 0; c = reader.read()) { builder.append((char) c); } return builder.toString(); } + } - private void checkPoints(Region.Location expected, PolyhedronsSet tree, Cartesian3D[] points) { + private void checkPoints(Region.Location expected, PolyhedronsSet poly, Cartesian3D ... points) { for (int i = 0; i < points.length; ++i) { - Assert.assertEquals(expected, tree.checkPoint(points[i])); + Assert.assertEquals("Incorrect location for " + points[i], expected, poly.checkPoint(points[i])); } } + private List> createBoxBoundaries(Cartesian3D center, double size, double tolerance) { + List> boundaries = new ArrayList<>(); + + double offset = size * 0.5; + + Plane xMinus = new Plane(center.add(new Cartesian3D(-offset, 0, 0)), Cartesian3D.MINUS_I, tolerance); + Plane xPlus = new Plane(center.add(new Cartesian3D(offset, 0, 0)), Cartesian3D.PLUS_I, tolerance); + Plane yPlus = new Plane(center.add(new Cartesian3D(0, offset, 0)), Cartesian3D.PLUS_J, tolerance); + Plane yMinus = new Plane(center.add(new Cartesian3D(0, -offset, 0)), Cartesian3D.MINUS_J, tolerance); + Plane zPlus = new Plane(center.add(new Cartesian3D(0, 0, offset)), Cartesian3D.PLUS_K, tolerance); + Plane zMinus = new Plane(center.add(new Cartesian3D(0, 0, -offset)), Cartesian3D.MINUS_K, tolerance); + + // +x + boundaries.add(createSubPlane(xPlus, + center.add(new Cartesian3D(offset, offset, offset)), + center.add(new Cartesian3D(offset, -offset, offset)), + center.add(new Cartesian3D(offset, -offset, -offset)), + center.add(new Cartesian3D(offset, offset, -offset)))); + + // -x + boundaries.add(createSubPlane(xMinus, + center.add(new Cartesian3D(-offset, -offset, offset)), + center.add(new Cartesian3D(-offset, offset, offset)), + center.add(new Cartesian3D(-offset, offset, -offset)), + center.add(new Cartesian3D(-offset, -offset, -offset)))); + + // +y + boundaries.add(createSubPlane(yPlus, + center.add(new Cartesian3D(-offset, offset, offset)), + center.add(new Cartesian3D(offset, offset, offset)), + center.add(new Cartesian3D(offset, offset, -offset)), + center.add(new Cartesian3D(-offset, offset, -offset)))); + + // -y + boundaries.add(createSubPlane(yMinus, + center.add(new Cartesian3D(-offset, -offset, offset)), + center.add(new Cartesian3D(-offset, -offset, -offset)), + center.add(new Cartesian3D(offset, -offset, -offset)), + center.add(new Cartesian3D(offset, -offset, offset)))); + + // +z + boundaries.add(createSubPlane(zPlus, + center.add(new Cartesian3D(-offset, -offset, offset)), + center.add(new Cartesian3D(offset, -offset, offset)), + center.add(new Cartesian3D(offset, offset, offset)), + center.add(new Cartesian3D(-offset, offset, offset)))); + + // -z + boundaries.add(createSubPlane(zMinus, + center.add(new Cartesian3D(-offset, -offset, -offset)), + center.add(new Cartesian3D(-offset, offset, -offset)), + center.add(new Cartesian3D(offset, offset, -offset)), + center.add(new Cartesian3D(offset, -offset, -offset)))); + + return boundaries; + } + + private SubPlane createSubPlane(Plane plane, Cartesian3D...points) { + Cartesian2D[] points2d = new Cartesian2D[points.length]; + for (int i=0; i planes = new ArrayList<>(); + + // add top and bottom planes (+/- z) + Cartesian3D topZ = new Cartesian3D(center.getX(), center.getY(), center.getZ() + radius); + Cartesian3D bottomZ = new Cartesian3D(center.getX(), center.getY(), center.getZ() - radius); + + planes.add(new Plane(topZ, Cartesian3D.PLUS_K, TEST_TOLERANCE)); + planes.add(new Plane(bottomZ, Cartesian3D.MINUS_K, TEST_TOLERANCE)); + + // add the side planes + double vDelta = FastMath.PI / stacks; + double hDelta = FastMath.PI * 2 / slices; + + double adjustedRadius = (radius + (radius * FastMath.cos(vDelta * 0.5))) / 2.0; + + double vAngle; + double hAngle; + double stackRadius; + double stackHeight; + double x, y; + Cartesian3D norm, pt; + + vAngle = -0.5 * vDelta; + for (int v=0; v().buildConvex(planes.toArray(new Plane[0])); + } + + private void assertSubPlaneNormal(Cartesian3D expectedNormal, SubHyperplane sub) { + Cartesian3D norm = ((Plane) sub.getHyperplane()).getNormal(); + GeometryTestUtils.assertVectorEquals(expectedNormal, norm, TEST_TOLERANCE); + } + + private double cubeVolume(double size) { + return size * size * size; + } + + private double cubeSurface(double size) { + return 6.0 * size * size; + } + + private double sphereVolume(double radius) { + return 4.0 * FastMath.PI * radius * radius * radius / 3.0; + } + + private double sphereSurface(double radius) { + return 4.0 * FastMath.PI * radius * radius; + } + + private double circleSurface(double radius) { + return FastMath.PI * radius * radius; + } }