http://git-wip-us.apache.org/repos/asf/commons-math/blob/24d3dd8b/src/test/java/org/apache/commons/math4/geometry/euclidean/oned/SubOrientedPointTest.java ---------------------------------------------------------------------- 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 6888d51..2843cac 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<Euclidean1D> split = pt.split(splitter); + // act + SplitSubHyperplane<Euclidean1D> 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<Interval> 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<Euclidean1D> split = pt.split(splitter); + // act + SplitSubHyperplane<Euclidean1D> 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<Interval> 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<Euclidean1D> split = pt.split(splitter); + // act + SplitSubHyperplane<Euclidean1D> 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<Euclidean1D> plusSplit = pt.split(new OrientedPoint(new Cartesian1D(0.899), true, 1e-10));
http://git-wip-us.apache.org/repos/asf/commons-math/blob/24d3dd8b/src/test/java/org/apache/commons/math4/geometry/euclidean/threed/OBJWriter.java ---------------------------------------------------------------------- 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 0000000..4d13f0b --- /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<Cartesian3D> 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<int[]> 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<Cartesian3D> { + + /** 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<Euclidean3D> { + + /** Geometric tolerance */ + private final double tolerance; + + /** Map of vertices to their index in the vertices list */ + private Map<Cartesian3D, Integer> vertexIndexMap; + + /** List of unique vertices in the BSPTree boundary */ + private List<Cartesian3D> vertices; + + /** + * List of face vertex indices. Each face will have 3 indices. Indices + * are 0-based. + * */ + private List<int[]> 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<Cartesian3D> 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<int[]> getFaces() { + return faces; + } + + /** {@inheritDoc} */ + @Override + public Order visitOrder(BSPTree<Euclidean3D> node) { + return Order.SUB_MINUS_PLUS; + } + + /** {@inheritDoc} */ + @SuppressWarnings("unchecked") + @Override + public void visitInternalNode(BSPTree<Euclidean3D> node) { + BoundaryAttribute<Euclidean3D> attr = (BoundaryAttribute<Euclidean3D>) 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<Euclidean3D> 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 <code>vertices</code> + * 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<Euclidean2D> { + + /** Geometric tolerance */ + private double tolerance; + + /** List of extracted triangles */ + private List<Cartesian2D[]> 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<Cartesian2D[]> getTriangles() { + return triangles; + } + + /** {@inheritDoc} */ + @Override + public Order visitOrder(BSPTree<Euclidean2D> node) { + return Order.SUB_MINUS_PLUS; + } + + /** {@inheritDoc} */ + @Override + public void visitInternalNode(BSPTree<Euclidean2D> node) { + // do nothing + } + + /** {@inheritDoc} */ + @Override + public void visitLeafNode(BSPTree<Euclidean2D> 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<vertices.length; ++i) { + triangles.add(new Cartesian2D[] { vertices[0], vertices[i-1], vertices[i] }); + } + } + } +}