View Javadoc
1   /*
2    * Copyright (C) 2012 Alberto Irurueta Carro (alberto@irurueta.com)
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *         http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package com.irurueta.geometry;
17  
18  import java.util.ArrayList;
19  import java.util.LinkedList;
20  import java.util.List;
21  
22  /**
23   * This class defines a triangulator for 3D polygons. Triangulators divide
24   * polygons into triangles, which are the simplest geometric figure.
25   * This implementation uses Van Gogh or Ear Cutting algorithm for triangulation.
26   */
27  public class VanGoghTriangulator3D extends Triangulator3D {
28  
29      /**
30       * Default orientation threshold which is 90º.
31       */
32      public static final double DEFAULT_ORIENTATION_THRESHOLD = Math.PI / 2.0;
33  
34      /**
35       * Minimum allowed threshold.
36       */
37      public static final double MIN_THRESHOLD = 0.0;
38  
39      /**
40       * Number of 3D inhomogeneous coordinates.
41       */
42      public static final int INHOM_COORDS = 3;
43  
44      /**
45       * Returns triangulator method.
46       * Each method implementation will divide polygons into triangles using
47       * different techniques.
48       *
49       * @return Triangulator method.
50       */
51      @Override
52      public TriangulatorMethod getMethod() {
53          return TriangulatorMethod.VAN_GOGH_TRIANGULATOR;
54      }
55  
56      /**
57       * Triangulates provided polygon by dividing it into a set of triangles.
58       *
59       * @param polygon Polygon to be triangulated.
60       * @return List of triangles forming the polygon that has been triangulated.
61       * @throws TriangulatorException Raised if triangulation cannot be done.
62       *                               Usually this indicates numerical instability or polygon degeneracy.
63       */
64      @Override
65      public List<Triangle3D> triangulate(final Polygon3D polygon) throws TriangulatorException {
66          // triangulation will modify provided list of vertices, so we make a copy
67          // of it
68  
69          //original vertices
70          final var vertices = polygon.getVertices();
71          final var verticesCopy = new ArrayList<>(polygon.getVertices());
72          return internalTriangulate(verticesCopy, null, vertices);
73      }
74  
75      /**
76       * Triangulates a polygon formed by provided vertices.
77       *
78       * @param vertices List of points considered as vertices of a polygon.
79       * @return List of triangles forming the polygon that has been triangulated.
80       * @throws TriangulatorException Raised if triangulation cannot be done.
81       *                               Usually this indicates numerical instability or polygon degeneracy.
82       */
83      @Override
84      public List<Triangle3D> triangulate(final List<Point3D> vertices) throws TriangulatorException {
85          if (vertices.size() < MIN_VERTICES) {
86              throw new TriangulatorException();
87          }
88  
89          // triangulation will modify provided list of vertices, so we make a copy
90          // of it
91          final var verticesCopy = new ArrayList<>(vertices);
92          return internalTriangulate(verticesCopy, null, vertices);
93      }
94  
95      /**
96       * Triangulates a polygon formed by provided vertices.
97       *
98       * @param vertices List of points considered as vertices of a polygon.
99       * @param indices  List where indices of original vertices will be stored.
100      *                 This list can be used to refer to the original order of vertices. Notice
101      *                 that vertices indices might be repeated because vertices might appear in
102      *                 more than one triangle after triangulation. If this parameter is null,
103      *                 indices won't be stored in this list.
104      * @return List of triangles forming the polygon that has been triangulated.
105      * @throws TriangulatorException Raised if triangulation cannot be done.
106      *                               Usually this indicates numerical instability or polygon degeneracy.
107      */
108     @Override
109     public List<Triangle3D> triangulate(final List<Point3D> vertices, final List<int[]> indices)
110             throws TriangulatorException {
111         if (vertices.size() < MIN_VERTICES) {
112             throw new TriangulatorException();
113         }
114 
115         // triangulation will modify provided list of vertices, so we make a copy
116         // of it
117         final var verticesCopy = new ArrayList<>(vertices);
118         return internalTriangulate(verticesCopy, indices, vertices);
119     }
120 
121     /**
122      * Determines if provided triangle can be considered as an ear of the
123      * remaining polygon formed by provided vertices.
124      * An ear is usually a triangle located at a corner of a polygon.
125      * A triangle is considered an ear if no other vertex of the polygon lies
126      * within the triangle and if the triangle is not convex (is concave).
127      *
128      * @param triangle        A triangle.
129      * @param polygonVertices A list of points forming the remaining polygon
130      * @return true if triangle is ear, false otherwise.
131      * @throws CoincidentPointsException Raised if orientation of polygons
132      *                                   cannot be determined, usually because consecutive vertices in a polygon
133      *                                   are coincident, there are numerical instabilities or polygon degeneracies.
134      */
135     public static boolean isEar(final Triangle3D triangle, final List<Point3D> polygonVertices)
136             throws CoincidentPointsException {
137 
138         boolean isInside;
139         boolean isReversed;
140         // in a counterclockwise polygon, reversed orientation means that
141         // triangle is not convex and cannot be an ear
142 
143         final var triangleVertices = triangle.getVertices();
144 
145         // check that no points in the polygon (aside from points belonging to
146         // the triangle) lie inside the triangle
147         for (final var testPoint : polygonVertices) {
148             // Do not compare with polygon elements which are triangle points
149             // if end is reached then polygon and triangle is equal and hence the
150             // polygon is the ear
151             if (triangleVertices.contains(testPoint)) {
152                 continue;
153             }
154 
155             isInside = triangle.isInside(testPoint);
156             isReversed = isPolygonOrientationReversed(triangleVertices, polygonVertices);
157 
158             // if a point is inside the triangle or orientation is reversed, then
159             // it is not an ear
160             if (isInside || isReversed) {
161                 return false;
162             }
163         }
164 
165         // no points in the polygon where found inside the triangle and
166         // orientation is the same, so an ear is detected
167         return true;
168 
169     }
170 
171     /**
172      * Given the list of vertices of two polygons, determines if polygons have
173      * reversed orientation, or in other words, it the angle between the
174      * orientation of both polygons is larger than DEFAULT_ORIENTATION_THRESHOLD
175      * (i.e. 90 degrees or pi / 2 radians).
176      *
177      * @param vertices1 Vertices of 1st polygon.
178      * @param vertices2 Vertices of 2nd polygon.
179      * @return True if polygons have reversed orientation, false otherwise.
180      * @throws CoincidentPointsException Raised if orientation of polygons .
181      *                                   cannot be determined, usually because consecutive vertices in a polygon
182      *                                   are coincident, there are numerical instabilities or polygon degeneracies.
183      * @see Polygon3D#getAngleBetweenPolygons(List, List)
184      */
185     public static boolean isPolygonOrientationReversed(final List<Point3D> vertices1, final List<Point3D> vertices2)
186             throws CoincidentPointsException {
187         return isPolygonOrientationReversed(vertices1, vertices2, DEFAULT_ORIENTATION_THRESHOLD);
188     }
189 
190     /**
191      * Given the list of vertices of two polygons, determines if polygons have
192      * reversed orientation, or in other words, it the angle between the
193      * orientation of both polygons is larger than provided threshold.
194      *
195      * @param vertices1 Vertices of 1st polygon.
196      * @param vertices2 Vertices of 2nd polygon.
197      * @param threshold Angle threshold expressed in radians to determine if
198      *                  polygons have reversed orientation.
199      * @return True if polygons have reversed orientation, false otherwise.
200      * @throws IllegalArgumentException  Raised if provided threshold is negative.
201      * @throws CoincidentPointsException Raised if orientation of polygons
202      *                                   cannot be determined, usually because consecutive vertices in a polygon
203      *                                   are coincident, there are numerical instabilities or polygon degeneracies.
204      * @see Polygon3D#getAngleBetweenPolygons(List, List)
205      */
206     public static boolean isPolygonOrientationReversed(
207             final List<Point3D> vertices1, final List<Point3D> vertices2, final double threshold)
208             throws CoincidentPointsException {
209         if (threshold < MIN_THRESHOLD) {
210             throw new IllegalArgumentException();
211         }
212 
213         return isOrientationReversed(Polygon3D.getAngleBetweenPolygons(vertices1, vertices2), threshold);
214     }
215 
216     /**
217      * Given an angle, determines if orientation can be considered to be
218      * reversed.
219      *
220      * @param angle Angle in radians.
221      * @return True if absolute value of angle is larger than
222      * DEFAULT_ORIENTATION_THRESHOLD, false otherwise.
223      */
224     public static boolean isOrientationReversed(final double angle) {
225         return isOrientationReversed(angle, DEFAULT_ORIENTATION_THRESHOLD);
226     }
227 
228     /**
229      * Given an angle, determines if orientation can be considered to be
230      * reversed.
231      *
232      * @param angle     Angle in radians.
233      * @param threshold Threshold to determine if orientation is reversed
234      * @return True if absolute value of angle is larger than provided
235      * threshold, false otherwise.
236      */
237     public static boolean isOrientationReversed(final double angle, final double threshold) {
238         return Math.abs(angle) > Math.abs(threshold);
239     }
240 
241     /**
242      * Internal method that computes the actual triangulation.
243      *
244      * @param verticesCopy     List of points considered as verticesCopy of a
245      *                         polygon. This list will be modified after execution of this method
246      * @param indices          List where indices of original verticesCopy will be
247      *                         stored.
248      *                         This list can be used to refer to the original order of verticesCopy.
249      *                         Notice that verticesCopy indices might be repeated because verticesCopy
250      *                         might appear in more than one triangle after triangulation. If this
251      *                         parameter is null, indices won't be stored in this list.
252      * @param originalVertices Reference to original list of vertices that won't
253      *                         be modified.
254      * @return List of triangles forming the polygon that has been triangulated.
255      * @throws TriangulatorException Raised if triangulation cannot be done.
256      *                               Usually this indicates numerical instability or polygon degeneracy.
257      */
258     private static List<Triangle3D> internalTriangulate(
259             final List<Point3D> verticesCopy, final List<int[]> indices, final List<Point3D> originalVertices)
260             throws TriangulatorException {
261         if (verticesCopy.size() < MIN_VERTICES) {
262             throw new TriangulatorException();
263         }
264 
265         final var result = new LinkedList<Triangle3D>();
266 
267         boolean isEar;
268         boolean madeCut;
269 
270         Triangle3D triangle = null;
271 
272         // Second, apply algorithm
273         while (verticesCopy.size() > MIN_VERTICES) {
274             madeCut = false;
275             final var lastElement = verticesCopy.size() - 1;
276             for (int i = 0; i <= lastElement; i++) {
277                 if (i == 0) {
278                     if (triangle == null) {
279                         // instantiate triangle if not already instantiated
280                         triangle = new Triangle3D(verticesCopy.get(lastElement),
281                                 verticesCopy.get(0), verticesCopy.get(1));
282                     } else {
283                         triangle.setVertices(verticesCopy.get(lastElement),
284                                 verticesCopy.get(0), verticesCopy.get(1));
285                     }
286                 } else if (i == lastElement) {
287                     triangle.setVertices(verticesCopy.get(lastElement - 1), verticesCopy.get(lastElement),
288                             verticesCopy.get(0));
289                 } else {
290                     triangle.setVertices(verticesCopy.get(i - 1), verticesCopy.get(i), verticesCopy.get(i + 1));
291                 }
292 
293                 try {
294                     isEar = isEar(triangle, verticesCopy);
295                 } catch (CoincidentPointsException e) {
296                     isEar = false;
297                 }
298 
299                 if (isEar) {
300                     // If it is an ear, we build a face out of the triangle being
301                     // cut and remove it from polygon by cutting it
302                     result.add(triangle);
303                     // so that it cannot be reused after being added
304                     triangle = null;
305 
306                     // cut ear
307                     verticesCopy.remove(i);
308                     madeCut = true;
309 
310                     // Leave from FOR loop to loop again to new reduced verticesCopy set
311                     break;
312                 }
313             }
314 
315             // if arrived here but no cut was made and polygon size contains
316             // more than 3 verticesCopy, then the algorithm failed for some reason
317             if (!madeCut) {
318                 throw new TriangulatorException();
319             }
320         }
321 
322         // instantiate final triangle
323         triangle = new Triangle3D(verticesCopy.get(0), verticesCopy.get(1), verticesCopy.get(2));
324 
325         final var arePointsColinear = triangle.areVerticesColinear();
326 
327         // only add final triangle if not co-linear (area greater than small
328         // threshold)
329         if (!arePointsColinear) {
330             result.add(triangle);
331         }
332 
333         // add indices of triangles verticesCopy
334         computeIndices(originalVertices, result, indices);
335         return result;
336     }
337 
338     /**
339      * Computes indices of resulting triangles vertices respect to original
340      * polygon vertices. Indices are stored in provided indices list.
341      *
342      * @param vertices  Vertices of polygon.
343      * @param triangles Triangles obtained after triangulation.
344      * @param indices   Indices of original positions of resulting triangle's
345      *                  vertices.
346      */
347     private static void computeIndices(
348             final List<Point3D> vertices, final List<Triangle3D> triangles, final List<int[]> indices) {
349         if (indices != null) {
350             int vertexCounter;
351             int triangleVertexCounter;
352             int[] triangleIndices;
353             for (final var t : triangles) {
354                 triangleVertexCounter = 0;
355                 triangleIndices = new int[Triangle3D.NUM_VERTICES];
356                 for (final var p1 : t.getVertices()) {
357                     vertexCounter = 0;
358                     for (final var p2 : vertices) {
359                         if (p1 == p2) {
360                             triangleIndices[triangleVertexCounter] = vertexCounter;
361                             break;
362                         }
363                         vertexCounter++;
364                     }
365                     triangleVertexCounter++;
366                 }
367                 indices.add(triangleIndices);
368             }
369         }
370     }
371 }