View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.geometry.euclidean;
18  
19  import java.io.File;
20  import java.util.Arrays;
21  import java.util.List;
22  import java.util.stream.Collectors;
23  
24  import org.apache.commons.geometry.core.RegionLocation;
25  import org.apache.commons.geometry.core.partitioning.Split;
26  import org.apache.commons.geometry.core.partitioning.bsp.RegionCutRule;
27  import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
28  import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
29  import org.apache.commons.geometry.euclidean.oned.Interval;
30  import org.apache.commons.geometry.euclidean.oned.RegionBSPTree1D;
31  import org.apache.commons.geometry.euclidean.oned.Vector1D;
32  import org.apache.commons.geometry.euclidean.testio.TestOBJWriter;
33  import org.apache.commons.geometry.euclidean.threed.AffineTransformMatrix3D;
34  import org.apache.commons.geometry.euclidean.threed.ConvexPolygon3D;
35  import org.apache.commons.geometry.euclidean.threed.Plane;
36  import org.apache.commons.geometry.euclidean.threed.PlaneConvexSubset;
37  import org.apache.commons.geometry.euclidean.threed.Planes;
38  import org.apache.commons.geometry.euclidean.threed.RegionBSPTree3D;
39  import org.apache.commons.geometry.euclidean.threed.Vector3D;
40  import org.apache.commons.geometry.euclidean.threed.line.Line3D;
41  import org.apache.commons.geometry.euclidean.threed.line.LinecastPoint3D;
42  import org.apache.commons.geometry.euclidean.threed.line.Lines3D;
43  import org.apache.commons.geometry.euclidean.threed.line.Ray3D;
44  import org.apache.commons.geometry.euclidean.threed.mesh.TriangleMesh;
45  import org.apache.commons.geometry.euclidean.threed.rotation.QuaternionRotation;
46  import org.apache.commons.geometry.euclidean.threed.shape.Parallelepiped;
47  import org.apache.commons.geometry.euclidean.threed.shape.Sphere;
48  import org.apache.commons.geometry.euclidean.twod.AffineTransformMatrix2D;
49  import org.apache.commons.geometry.euclidean.twod.Line;
50  import org.apache.commons.geometry.euclidean.twod.LinecastPoint2D;
51  import org.apache.commons.geometry.euclidean.twod.Lines;
52  import org.apache.commons.geometry.euclidean.twod.Ray;
53  import org.apache.commons.geometry.euclidean.twod.RegionBSPTree2D;
54  import org.apache.commons.geometry.euclidean.twod.Segment;
55  import org.apache.commons.geometry.euclidean.twod.Vector2D;
56  import org.apache.commons.geometry.euclidean.twod.path.LinePath;
57  import org.apache.commons.geometry.euclidean.twod.shape.Parallelogram;
58  import org.apache.commons.numbers.angle.PlaneAngleRadians;
59  import org.junit.Assert;
60  import org.junit.Test;
61  
62  /** This class contains code listed as examples in the user guide and other documentation.
63   * If any portion of this code changes, the corresponding examples in the documentation <em>must</em> be updated.
64   */
65  public class DocumentationExamplesTest {
66  
67      private static final double TEST_EPS = 1e-12;
68  
69      @Test
70      public void testIndexPageExample() {
71          // construct a precision context to handle floating-point comparisons
72          final DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-6);
73  
74          // create a BSP tree representing the unit cube
75          final RegionBSPTree3D tree = Parallelepiped.unitCube(precision).toTree();
76  
77          // create a sphere centered on the origin
78          final Sphere sphere = Sphere.from(Vector3D.ZERO, 0.65, precision);
79  
80          // subtract a BSP tree approximation of the sphere containing 512 facets
81          // from the cube, modifying the cube tree in place
82          tree.difference(sphere.toTree(3));
83  
84          // compute some properties of the resulting region
85          final double size = tree.getSize(); // 0.11509505362599505
86          final Vector3D centroid = tree.getCentroid(); // (0, 0, 0)
87  
88          // convert to a triangle mesh for output to other programs
89          final TriangleMesh mesh = tree.toTriangleMesh(precision);
90  
91          // -----------
92          Assert.assertEquals(0.11509505362599505, size, TEST_EPS);
93          EuclideanTestUtils.assertCoordinatesEqual(Vector3D.ZERO, centroid, TEST_EPS);
94  
95          TestOBJWriter.write(mesh, new File("target/index-page-example.obj"));
96      }
97  
98      @Test
99      public void testPrecisionContextExample() {
100         // create a precision context with an epsilon (aka, tolerance) value of 1e-3
101         final DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-3);
102 
103         // test for equality using the eq() method
104         precision.eq(1.0009, 1.0); // true; difference is less than epsilon
105         precision.eq(1.002, 1.0); // false; difference is greater than epsilon
106 
107         // compare
108         precision.compare(1.0009, 1.0); // 0
109         precision.compare(1.002, 1.0); // 1
110 
111         // ------------------
112         Assert.assertTrue(precision.eq(1.0009, 1.0));
113         Assert.assertFalse(precision.eq(1.002, 1.0));
114 
115         Assert.assertEquals(0, precision.compare(1.0009, 1.0));
116         Assert.assertEquals(1, precision.compare(1.002, 1.0));
117     }
118 
119     @Test
120     public void testEqualsVsEqExample() {
121         final DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-6);
122 
123         final Vector2D v1 = Vector2D.of(1, 1); // (1.0, 1.0)
124         final Vector2D v2 = Vector2D.parse("(1, 1)"); // (1.0, 1.0)
125 
126         final Vector2D v3 = Vector2D.of(Math.sqrt(2), 0).transform(
127                 AffineTransformMatrix2D.createRotation(0.25 * Math.PI)); // (1.0000000000000002, 1.0)
128 
129         v1.equals(v2); // true - exactly equal
130         v1.equals(v3); // false - not exactly equal
131 
132         v1.eq(v3, precision); // true - approximately equal according to the given precision context
133 
134         // ---------------------
135         Assert.assertEquals(v1, v2);
136         Assert.assertNotEquals(v1, v3);
137         Assert.assertTrue(v1.eq(v3, precision));
138     }
139 
140     @Test
141     public void testManualBSPTreeExample() {
142         final DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-6);
143 
144         // create a tree representing an empty space (nothing "inside")
145         final RegionBSPTree2D tree = RegionBSPTree2D.empty();
146 
147         // insert a "structural" cut, meaning a cut whose children have the same inside/outside
148         // status as the parent; this will help keep our tree balanced and limit its overall height
149         tree.getRoot().insertCut(Lines.fromPointAndDirection(Vector2D.ZERO, Vector2D.of(1, 1), precision),
150                 RegionCutRule.INHERIT);
151 
152         RegionBSPTree2D.RegionNode2D currentNode;
153 
154         // insert on the plus side of the structural diagonal cut
155         currentNode = tree.getRoot().getPlus();
156 
157         currentNode.insertCut(Lines.fromPointAndDirection(Vector2D.ZERO, Vector2D.Unit.PLUS_X, precision));
158         currentNode = currentNode.getMinus();
159 
160         currentNode.insertCut(Lines.fromPointAndDirection(Vector2D.of(1, 0), Vector2D.Unit.PLUS_Y, precision));
161 
162         // insert on the plus side of the structural diagonal cut
163         currentNode = tree.getRoot().getMinus();
164 
165         currentNode.insertCut(Lines.fromPointAndDirection(Vector2D.of(1, 1), Vector2D.Unit.MINUS_X, precision));
166         currentNode = currentNode.getMinus();
167 
168         currentNode.insertCut(Lines.fromPointAndDirection(Vector2D.of(0, 1), Vector2D.Unit.MINUS_Y, precision));
169 
170         // compute some tree properties
171         final int count = tree.count(); // number of nodes in the tree = 11
172         final int height = tree.height(); // height of the tree = 3
173         final double size = tree.getSize(); // size of the region = 1
174         final Vector2D centroid = tree.getCentroid(); // region centroid = (0.5, 0.5)
175 
176         // ---------
177         Assert.assertEquals(1, size, TEST_EPS);
178         Assert.assertEquals(11, count);
179         Assert.assertEquals(3, height);
180         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.5, 0.5), centroid, TEST_EPS);
181     }
182 
183     @Test
184     public void testHyperplaneSubsetBSPTreeExample() {
185         final DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-6);
186 
187         // create a tree representing an empty space (nothing "inside")
188         final RegionBSPTree2D tree = RegionBSPTree2D.empty();
189 
190         // insert the hyperplane subsets
191         tree.insert(Arrays.asList(
192                     Lines.segmentFromPoints(Vector2D.ZERO, Vector2D.of(1, 0), precision),
193                     Lines.segmentFromPoints(Vector2D.of(1, 0), Vector2D.of(1, 1), precision),
194                     Lines.segmentFromPoints(Vector2D.of(1, 1), Vector2D.of(0, 1), precision),
195                     Lines.segmentFromPoints(Vector2D.of(0, 1), Vector2D.ZERO, precision)
196                 ));
197 
198         // compute some tree properties
199         final int count = tree.count(); // number of nodes in the tree = 9
200         final int height = tree.height(); // height of the tree = 4
201         final double size = tree.getSize(); // size of the region = 1
202         final Vector2D centroid = tree.getCentroid(); // region centroid = (0.5, 0.5)
203 
204         // ---------
205         Assert.assertEquals(1, size, TEST_EPS);
206         Assert.assertEquals(9, count);
207         Assert.assertEquals(4, height);
208         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.5, 0.5), centroid, TEST_EPS);
209     }
210 
211     @Test
212     public void testIntervalExample() {
213         final DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-6);
214 
215         // create a closed interval and a half-open interval with a min but no max
216         final Interval closed = Interval.of(1, 2, precision);
217         final Interval halfOpen = Interval.min(1, precision);
218 
219         // classify some points against the intervals
220         closed.contains(0.0); // false
221         halfOpen.contains(Vector1D.ZERO); // false
222 
223         final RegionLocation closedOneLoc = closed.classify(Vector1D.of(1)); // RegionLocation.BOUNDARY
224         final RegionLocation halfOpenOneLoc = halfOpen.classify(Vector1D.of(1)); // RegionLocation.BOUNDARY
225 
226         final RegionLocation closedThreeLoc = closed.classify(3.0); // RegionLocation.OUTSIDE
227         final RegionLocation halfOpenThreeLoc = halfOpen.classify(3.0); // RegionLocation.INSIDE
228 
229         // --------------------
230         Assert.assertFalse(closed.contains(0));
231         Assert.assertFalse(halfOpen.contains(0));
232 
233         Assert.assertEquals(RegionLocation.BOUNDARY, closedOneLoc);
234         Assert.assertEquals(RegionLocation.BOUNDARY, halfOpenOneLoc);
235 
236         Assert.assertEquals(RegionLocation.OUTSIDE, closedThreeLoc);
237         Assert.assertEquals(RegionLocation.INSIDE, halfOpenThreeLoc);
238     }
239 
240     @Test
241     public void testRegionBSPTree1DExample() {
242         final DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-6);
243 
244         // build a bsp tree from the union of several intervals
245         final RegionBSPTree1D tree = RegionBSPTree1D.empty();
246 
247         tree.add(Interval.of(1, 2, precision));
248         tree.add(Interval.of(1.5, 3, precision));
249         tree.add(Interval.of(-1, -2, precision));
250 
251         // compute the size;
252         final double size = tree.getSize(); // 3
253 
254         // convert back to intervals
255         final List<Interval> intervals = tree.toIntervals(); // size = 2
256 
257         // ----------------------
258         Assert.assertEquals(3, size, TEST_EPS);
259         Assert.assertEquals(2, intervals.size());
260     }
261 
262     @Test
263     public void testLineIntersectionExample() {
264         final DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-6);
265 
266         // create some lines
267         final Line a = Lines.fromPoints(Vector2D.ZERO, Vector2D.of(2, 2), precision);
268         final Line b = Lines.fromPointAndDirection(Vector2D.of(1, -1), Vector2D.Unit.PLUS_Y, precision);
269 
270         // compute the intersection and angles
271         final Vector2D intersection = a.intersection(b); // (1, 1)
272         final double angleAtoB = a.angle(b); // pi/4
273         final double angleBtoA = b.angle(a); // -pi/4
274 
275         // ----------------------------
276         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 1), intersection, TEST_EPS);
277         Assert.assertEquals(0.25 * Math.PI, angleAtoB, TEST_EPS);
278         Assert.assertEquals(-0.25 * Math.PI, angleBtoA, TEST_EPS);
279     }
280 
281     @Test
282     public void testLineSegmentIntersectionExample() {
283         final DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-6);
284 
285         // create some line segments
286         final Segment segmentA = Lines.segmentFromPoints(Vector2D.of(3, -1), Vector2D.of(3, 1), precision);
287         final Segment segmentB = Lines.segmentFromPoints(Vector2D.of(-3, -1), Vector2D.of(-3, 1), precision);
288 
289         // create a ray to intersect against the segments
290         final Ray ray = Lines.rayFromPointAndDirection(Vector2D.of(2, 0), Vector2D.Unit.PLUS_X, precision);
291 
292         // compute some intersections
293         final Vector2D aIntersection = segmentA.intersection(ray); // (3, 0)
294         final Vector2D bIntersection = segmentB.intersection(ray); // null - no intersection
295 
296         // ----------------------------
297         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(3, 0), aIntersection, TEST_EPS);
298         Assert.assertNull(bIntersection);
299     }
300 
301     @Test
302     public void testRegionBSPTree2DExample() {
303         final DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-6);
304 
305         // create a connected sequence of line segments forming the unit square
306         final LinePath path = LinePath.builder(precision)
307                 .append(Vector2D.ZERO)
308                 .append(Vector2D.Unit.PLUS_X)
309                 .append(Vector2D.of(1, 1))
310                 .append(Vector2D.Unit.PLUS_Y)
311                 .build(true); // build the path, ending it with the starting point
312 
313         // convert to a tree
314         final RegionBSPTree2D tree = path.toTree();
315 
316         // copy the tree
317         final RegionBSPTree2D copy = tree.copy();
318 
319         // translate the copy
320         copy.transform(AffineTransformMatrix2D.createTranslation(Vector2D.of(0.5, 0.5)));
321 
322         // compute the union of the regions, storing the result back into the
323         // first tree
324         tree.union(copy);
325 
326         // compute some properties
327         final double size = tree.getSize(); // 1.75
328         final Vector2D centroid = tree.getCentroid(); // (0.75, 0.75)
329 
330         // get a line path representing the boundary; a list is returned since trees
331         // can represent disjoint regions
332         final List<LinePath> boundaries = tree.getBoundaryPaths(); // size = 1
333 
334         // ----------------
335         Assert.assertEquals(1.75, size, TEST_EPS);
336         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.75, 0.75), centroid, TEST_EPS);
337         Assert.assertEquals(1, boundaries.size());
338     }
339 
340     @Test
341     public void testLinecast2DExample() {
342         final DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-6);
343 
344         final Parallelogram box = Parallelogram.axisAligned(Vector2D.ZERO, Vector2D.of(2, 1), precision);
345 
346         final LinecastPoint2D pt = box.linecastFirst(
347                 Lines.segmentFromPoints(Vector2D.of(1, 0.5), Vector2D.of(4, 0.5), precision));
348 
349         final Vector2D intersection = pt.getPoint(); // (2.0, 0.5)
350         final Vector2D normal = pt.getNormal(); // (1.0, 0.0)
351 
352         // ----------------
353         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(2, 0.5), intersection, TEST_EPS);
354         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 0), normal, TEST_EPS);
355     }
356 
357     @Test
358     public void testPlaneIntersectionExample() {
359         final DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-6);
360 
361         // create two planes
362         final Plane a = Planes.fromPointAndNormal(Vector3D.of(1, 1, 1), Vector3D.Unit.PLUS_Z, precision);
363         final Plane b = Planes.fromPointAndPlaneVectors(Vector3D.of(1, 1, 1),
364                 Vector3D.Unit.PLUS_Z, Vector3D.Unit.MINUS_Y, precision);
365 
366         // compute the intersection
367         final Line3D line = a.intersection(b);
368 
369         final Vector3D dir = line.getDirection(); // (0, 1, 0)
370 
371         // ----------------------
372         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.Unit.PLUS_Y, dir, TEST_EPS);
373     }
374 
375     @Test
376     public void testTransform3DExample() {
377         final List<Vector3D> inputPts = Arrays.asList(
378                 Vector3D.ZERO,
379                 Vector3D.Unit.PLUS_X,
380                 Vector3D.Unit.PLUS_Y,
381                 Vector3D.Unit.PLUS_Z);
382 
383         // create a 4x4 transform matrix and quaternion rotation
384         final AffineTransformMatrix3D mat = AffineTransformMatrix3D.createScale(2)
385                 .translate(Vector3D.of(1, 2, 3));
386 
387         final QuaternionRotation rot = QuaternionRotation.fromAxisAngle(Vector3D.Unit.PLUS_Z,
388                 PlaneAngleRadians.PI_OVER_TWO);
389 
390         // transform the input points
391         final List<Vector3D> matOutput = inputPts.stream()
392                 .map(mat)
393                 .collect(Collectors.toList()); // [(1, 2, 3), (3, 2, 3), (1, 4, 3), (1, 2, 5)]
394 
395         final List<Vector3D> rotOutput = inputPts.stream()
396                 .map(rot)
397                 .collect(Collectors.toList()); // [(0, 0, 0), (0, 1, 0), (-1, 0, 0), (0, 0, 1)]
398 
399         // ----------------
400         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 2, 3), matOutput.get(0), TEST_EPS);
401         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(3, 2, 3), matOutput.get(1), TEST_EPS);
402         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 4, 3), matOutput.get(2), TEST_EPS);
403         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 2, 5), matOutput.get(3), TEST_EPS);
404 
405         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, 0, 0), rotOutput.get(0), TEST_EPS);
406         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, 1, 0), rotOutput.get(1), TEST_EPS);
407         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-1, 0, 0), rotOutput.get(2), TEST_EPS);
408         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, 0, 1), rotOutput.get(3), TEST_EPS);
409     }
410 
411     @Test
412     public void testRegionBSPTree3DExample() {
413         final DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-6);
414 
415         // create the faces of a pyramid with a square base and its apex pointing along the
416         // positive z axis
417         final Vector3D[] vertices = {
418             Vector3D.Unit.PLUS_Z,
419             Vector3D.of(0.5, 0.5, 0.0),
420             Vector3D.of(0.5, -0.5, 0.0),
421             Vector3D.of(-0.5, -0.5, 0.0),
422             Vector3D.of(-0.5, 0.5, 0.0)
423         };
424 
425         final int[][] faceIndices = {
426             {1, 0, 2},
427             {2, 0, 3},
428             {3, 0, 4},
429             {4, 0, 1},
430             {1, 2, 3, 4}
431         };
432 
433         // convert the vertices and faces to convex polygons and use to construct a BSP tree
434         final List<ConvexPolygon3D> faces = Planes.indexedConvexPolygons(vertices, faceIndices, precision);
435         final RegionBSPTree3D tree = RegionBSPTree3D.from(faces);
436 
437         // split the region through its centroid along a diagonal of the base
438         final Plane cutter = Planes.fromPointAndNormal(tree.getCentroid(), Vector3D.Unit.from(1, 1, 0), precision);
439         final Split<RegionBSPTree3D> split = tree.split(cutter);
440 
441         // compute some properties for the minus side of the split and convert back to hyperplane subsets
442         // (ie, boundary facets)
443         final RegionBSPTree3D minus = split.getMinus();
444 
445         final double minusSize = minus.getSize(); // 1/6
446         final List<PlaneConvexSubset> minusBoundaries = minus.getBoundaries(); // size = 4
447 
448         // ---------------------
449         Assert.assertEquals(1.0 / 6.0, minusSize, TEST_EPS);
450         Assert.assertEquals(4, minusBoundaries.size());
451     }
452 
453     @Test
454     public void testLinecast3DExample() {
455         final DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-6);
456 
457         // create a BSP tree representing an axis-aligned cube with corners at (0, 0, 0) and (1, 1, 1)
458         final RegionBSPTree3D tree = Parallelepiped.axisAligned(Vector3D.ZERO, Vector3D.of(1, 1, 1), precision)
459                 .toTree();
460 
461         // create a ray starting on one side of the cube and pointing through its center
462         final Ray3D ray = Lines3D.rayFromPointAndDirection(Vector3D.of(0.5, 0.5, -1), Vector3D.Unit.PLUS_Z, precision);
463 
464         // perform the linecast
465         final List<LinecastPoint3D> pts = tree.linecast(ray);
466 
467         // check the results
468         final int intersectionCount = pts.size(); // intersectionCount = 2
469         final Vector3D intersection = pts.get(0).getPoint(); // (0.5, 0.5, 0.0)
470         final Vector3D normal = pts.get(0).getNormal(); // (0.0, 0.0, -1.0)
471 
472         // ----------------
473         Assert.assertEquals(2, intersectionCount);
474 
475         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0.5, 0.5, 0), intersection, TEST_EPS);
476         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, 0, -1), normal, TEST_EPS);
477     }
478 }