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 }