View Javadoc
1   package org.djutils.draw.line;
2   
3   import java.util.Map;
4   import java.util.NavigableMap;
5   import java.util.TreeMap;
6   
7   import org.djutils.draw.DrawRuntimeException;
8   import org.djutils.draw.Transform2d;
9   import org.djutils.draw.point.Point2d;
10  import org.djutils.draw.point.Point3d;
11  import org.djutils.exceptions.Throw;
12  
13  /**
14   * Generation of B&eacute;zier curves. <br>
15   * The class implements the cubic(...) method to generate a cubic B&eacute;zier curve using the following formula: B(t) = (1 -
16   * t)<sup>3</sup>P<sub>0</sub> + 3t(1 - t)<sup>2</sup> P<sub>1</sub> + 3t<sup>2</sup> (1 - t) P<sub>2</sub> + t<sup>3</sup>
17   * P<sub>3</sub> where P<sub>0</sub> and P<sub>3</sub> are the end points, and P<sub>1</sub> and P<sub>2</sub> the control
18   * points. <br>
19   * For a smooth movement, one of the standard implementations if the cubic(...) function offered is the case where P<sub>1</sub>
20   * is positioned halfway between P<sub>0</sub> and P<sub>3</sub> starting from P<sub>0</sub> in the direction of P<sub>3</sub>,
21   * and P<sub>2</sub> is positioned halfway between P<sub>3</sub> and P<sub>0</sub> starting from P<sub>3</sub> in the direction
22   * of P<sub>0</sub>.<br>
23   * Finally, an n-point generalization of the B&eacute;zier curve is implemented with the bezier(...) function.
24   * <p>
25   * Copyright (c) 2013-2021 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
26   * BSD-style license. See <a href="http://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
27   * </p>
28   * @author <a href="https://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
29   * @author <a href="https://www.tudelft.nl/pknoppers">Peter Knoppers</a>
30   */
31  public final class Bezier
32  {
33      /** The default number of points to use to construct a B&eacute;zier curve. */
34      public static final int DEFAULT_BEZIER_SIZE = 64;
35  
36      /** Cached factorial values. */
37      private static long[] fact = new long[] {1L, 1L, 2L, 6L, 24L, 120L, 720L, 5040L, 40320L, 362880L, 3628800L, 39916800L,
38              479001600L, 6227020800L, 87178291200L, 1307674368000L, 20922789888000L, 355687428096000L, 6402373705728000L,
39              121645100408832000L, 2432902008176640000L};
40  
41      /** Utility class. */
42      private Bezier()
43      {
44          // do not instantiate
45      }
46  
47      /**
48       * Approximate a cubic B&eacute;zier curve from start to end with two control points.
49       * @param size int; the number of points of the B&eacute;zier curve
50       * @param start Point2d; the start point of the B&eacute;zier curve
51       * @param control1 Point2d; the first control point
52       * @param control2 Point2d; the second control point
53       * @param end Point2d; the end point of the B&eacute;zier curve
54       * @return PolyLine2d; an approximation of a cubic B&eacute;zier curve between start and end, using the two provided control
55       *         points
56       * @throws DrawRuntimeException in case the number of points is less than 2 or the B&eacute;zier curve could not be
57       *             constructed
58       */
59      public static PolyLine2d cubic(final int size, final Point2dtml#Point2d">Point2dd.html#Point2d">Point2d start, final Point2dtml#Point2d">Point2d control1, final Point2d control2,
60              final Point2d end) throws DrawRuntimeException
61      {
62          Throw.when(size < 2, DrawRuntimeException.class, "Too few points (specified %d; minimum is 2)", size);
63          Point2dhtml#Point2d">Point2d[] points = new Point2d[size];
64          for (int n = 0; n < size; n++)
65          {
66              double t = n / (size - 1.0);
67              double x = B3(t, start.x, control1.x, control2.x, end.x);
68              double y = B3(t, start.y, control1.y, control2.y, end.y);
69              points[n] = new Point2d(x, y);
70          }
71          return new PolyLine2d(points);
72      }
73  
74      /**
75       * Approximate a cubic B&eacute;zier curve from start to end with two control points with a specified precision.
76       * @param epsilon double; the precision.
77       * @param start Point2d; the start point of the B&eacute;zier curve
78       * @param control1 Point2d; the first control point
79       * @param control2 Point2d; the second control point
80       * @param end Point2d; the end point of the B&eacute;zier curve
81       * @return PolyLine2d; an approximation of a cubic B&eacute;zier curve between start and end, using the two provided control
82       *         points
83       * @throws DrawRuntimeException in case the number of points is less than 2, or the B&eacute;zier curve could not be
84       *             constructed
85       */
86      public static PolyLine2d cubic(final double epsilon, final Point2dtml#Point2d">Point2dd.html#Point2d">Point2d start, final Point2dtml#Point2d">Point2d control1, final Point2d control2,
87              final Point2d end) throws DrawRuntimeException
88      {
89          return bezier(epsilon, start, control1, control2, end);
90      }
91  
92      /**
93       * Approximate a cubic B&eacute;zier curve from start to end with two generated control points at half the distance between
94       * start and end.
95       * @param size int; the number of points of the B&eacute;zier curve
96       * @param start Ray2d; the start point and start direction of the B&eacute;zier curve
97       * @param end Ray2d; the end point and end direction of the B&eacute;zier curve
98       * @return PolyLine2d; an approximation of a cubic B&eacute;zier curve between start and end, using the directions of those
99       *         points at start and end
100      * @throws DrawRuntimeException in case the number of points is less than 2 or the B&eacute;zier curve could not be
101      *             constructed
102      */
103     public static PolyLine2d cubic(final int size, final Ray2d.html#Ray2d">Ray2d start, final Ray2d end) throws DrawRuntimeException
104     {
105         return cubic(size, start, end, 1.0);
106     }
107 
108     /**
109      * Approximate a cubic B&eacute;zier curve from start to end with two generated control points at half the distance between
110      * start and end with specified precision.
111      * @param epsilon double; the precision.
112      * @param start Ray2d; the start point and start direction of the B&eacute;zier curve
113      * @param end Ray2d; the end point and end direction of the B&eacute;zier curve
114      * @return PolyLine2d; an approximation of a cubic B&eacute;zier curve between start and end, using the directions of those
115      *         points at start and end
116      * @throws DrawRuntimeException in case the number of points is less than 2 or the B&eacute;zier curve could not be
117      *             constructed
118      */
119     public static PolyLine2d cubic(final double epsilon, final Ray2d.html#Ray2d">Ray2d start, final Ray2d end) throws DrawRuntimeException
120     {
121         return cubic(epsilon, start, end, 1.0);
122     }
123 
124     /**
125      * Approximate a cubic B&eacute;zier curve from start to end with two generated control points at half the distance between
126      * start and end.
127      * @param size int; the number of points for the B&eacute;zier curve
128      * @param start Ray2d; the start point and start direction of the B&eacute;zier curve
129      * @param end Ray2d; the end point and end direction of the B&eacute;zier curve
130      * @param shape shape factor; 1 = control points at half the distance between start and end, &gt; 1 results in a pointier
131      *            shape, &lt; 1 results in a flatter shape, value should be above 0 and finite
132      * @return PolyLine2d; an approximation of a cubic B&eacute;zier curve between start and end, using the directions of those
133      *         points at start and end
134      * @throws DrawRuntimeException in case the number of points is less than 2 or the B&eacute;zier curve could not be
135      *             constructed
136      */
137     public static PolyLine2d cubic(final int size, final Ray2d.html#Ray2d">Ray2d start, final Ray2d end, final double shape)
138             throws DrawRuntimeException
139     {
140         Throw.when(Double.isNaN(shape) || Double.isInfinite(shape) || shape <= 0, DrawRuntimeException.class,
141                 "shape must be a finite, positive value");
142         return cubic(size, start, end, shape, false);
143     }
144 
145     /**
146      * Approximate a cubic B&eacute;zier curve from start to end with two generated control points at half the distance between
147      * start and end with specified precision.
148      * @param epsilon double; the precision.
149      * @param start Ray2d; the start point and start direction of the B&eacute;zier curve
150      * @param end Ray2d; the end point and end direction of the B&eacute;zier curve
151      * @param shape shape factor; 1 = control points at half the distance between start and end, &gt; 1 results in a pointier
152      *            shape, &lt; 1 results in a flatter shape, value should be above 0 and finite
153      * @return PolyLine2d; an approximation of a cubic B&eacute;zier curve between start and end, using the directions of those
154      *         points at start and end
155      * @throws DrawRuntimeException in case the number of points is less than 2 or the B&eacute;zier curve could not be
156      *             constructed
157      */
158     public static PolyLine2d cubic(final double epsilon, final Ray2d.html#Ray2d">Ray2d start, final Ray2d end, final double shape)
159             throws DrawRuntimeException
160     {
161         Throw.when(Double.isNaN(shape) || Double.isInfinite(shape) || shape <= 0, DrawRuntimeException.class,
162                 "shape must be a finite, positive value");
163         return cubic(epsilon, start, end, shape, false);
164     }
165 
166     /**
167      * Approximate a cubic B&eacute;zier curve from start to end with two generated control points at half the distance between
168      * start and end.
169      * @param size int; the number of points for the B&eacute;zier curve
170      * @param start Ray2d; the start point and start direction of the B&eacute;zier curve
171      * @param end Ray2d; the end point and end direction of the B&eacute;zier curve
172      * @param shape shape factor; 1 = control points at half the distance between start and end, &gt; 1 results in a pointier
173      *            shape, &lt; 1 results in a flatter shape, value should be above 0, finite and not NaN
174      * @param weighted boolean; control point distance relates to distance to projected point on extended line from other end
175      * @return PolyLine2d; an approximation of a cubic B&eacute;zier curve between start and end, with the two determined
176      *         control points
177      * @throws NullPointerException when start or end is null
178      * @throws DrawRuntimeException in case size is less than 2, start is at the same location as end, shape is invalid, or the
179      *             B&eacute;zier curve could not be constructed
180      */
181     public static PolyLine2d cubic(final int size, final Ray2d.html#Ray2d">Ray2d start, final Ray2d end, final double shape,
182             final boolean weighted) throws NullPointerException, DrawRuntimeException
183     {
184         Point2d[] points = createControlPoints(start, end, shape, weighted);
185         return cubic(size, points[0], points[1], points[2], points[3]);
186     }
187 
188     /**
189      * Approximate a cubic B&eacute;zier curve from start to end with two generated control points at half the distance between
190      * start and end with specified precision.
191      * @param epsilon double; the precision.
192      * @param start Ray2d; the start point and start direction of the B&eacute;zier curve
193      * @param end Ray2d; the end point and end direction of the B&eacute;zier curve
194      * @param shape shape factor; 1 = control points at half the distance between start and end, &gt; 1 results in a pointier
195      *            shape, &lt; 1 results in a flatter shape, value should be above 0, finite and not NaN
196      * @param weighted boolean; control point distance relates to distance to projected point on extended line from other end
197      * @return PolyLine2d; an approximation of a cubic B&eacute;zier curve between start and end, with the two determined
198      *         control points
199      * @throws NullPointerException when start or end is null
200      * @throws DrawRuntimeException in case size is less than 2, start is at the same location as end, shape is invalid
201      */
202     public static PolyLine2d cubic(final double epsilon, final Ray2d.html#Ray2d">Ray2d start, final Ray2d end, final double shape,
203             final boolean weighted) throws NullPointerException, DrawRuntimeException
204     {
205         Point2d[] points = createControlPoints(start, end, shape, weighted);
206         return cubic(epsilon, points[0], points[1], points[2], points[3]);
207     }
208 
209     /** Unit vector for transformations in createControlPoints. */
210     private static final Point2dPoint2d">Point2d UNIT_VECTOR2D = new Point2d(1, 0);
211 
212     /**
213      * Create control points for a cubic B&eacute;zier curve defined by two Rays.
214      * @param start Ray2d; the start point (and direction)
215      * @param end Ray2d; the end point (and direction)
216      * @param shape double; the shape; higher values put the generated control points further away from end and result in a
217      *            pointier B&eacute;zier curve
218      * @param weighted boolean;
219      * @return Point2d[]; an array of four Point2d elements: start, the first control point, the second control point, end.
220      * @throws DrawRuntimeException when shape is invalid
221      */
222     private static Point2d[] createControlPoints(final Ray2d.html#Ray2d">Ray2d start, final Ray2d end, final double shape, final boolean weighted)
223             throws DrawRuntimeException
224     {
225         Throw.whenNull(start, "start point may not be null");
226         Throw.whenNull(end, "end point may not be null");
227         Throw.when(start.distanceSquared(end) == 0, DrawRuntimeException.class,
228                 "Cannot create control points if start and end points coincide");
229         Throw.when(Double.isNaN(shape) || shape <= 0 || Double.isInfinite(shape), DrawRuntimeException.class,
230                 "shape must be a finite, positive value");
231 
232         Point2d control1;
233         Point2d control2;
234         if (weighted)
235         {
236             // each control point is 'w' * the distance between the end-points away from the respective end point
237             // 'w' is a weight given by the distance from the end point to the extended line of the other end point
238             double distance = shape * start.distance(end);
239             double dStart = start.distance(end.projectOrthogonalExtended(start));
240             double dEnd = end.distance(start.projectOrthogonalExtended(end));
241             double wStart = dStart / (dStart + dEnd);
242             double wEnd = dEnd / (dStart + dEnd);
243             control1 = new Transform2d().translate(start).rotation(start.phi).scale(distance * wStart, distance * wStart)
244                     .transform(UNIT_VECTOR2D);
245             // - (minus) as the angle is where the line leaves, i.e. from shape point to end
246             control2 = new Transform2d().translate(end).rotation(end.phi + Math.PI).scale(distance * wEnd, distance * wEnd)
247                     .transform(UNIT_VECTOR2D);
248         }
249         else
250         {
251             // each control point is half the distance between the end-points away from the respective end point
252             double distance = shape * start.distance(end) / 2.0;
253             control1 = start.getLocation(distance);
254             // new Transform2d().translate(start).rotation(start.phi).scale(distance, distance).transform(UNIT_VECTOR2D);
255             control2 = end.getLocationExtended(-distance);
256             // new Transform2d().translate(end).rotation(end.phi + Math.PI).scale(distance, distance).transform(UNIT_VECTOR2D);
257         }
258         return new Point2d[] {start, control1, control2, end};
259     }
260 
261     /**
262      * Approximate a cubic B&eacute;zier curve from start to end with two generated control points at half the distance between
263      * start and end. The size of the constructed curve is <code>DEFAULT_BEZIER_SIZE</code>.
264      * @param start Ray2d; the start point and start direction of the B&eacute;zier curve
265      * @param end Ray2d; the end point and end direction of the B&eacute;zier curve
266      * @return PolyLine2d; an approximation of a cubic B&eacute;zier curve between start and end, following the directions of
267      *         those points at start and end
268      * @throws DrawRuntimeException in case the number of points is less than 2 or the B&eacute;zier curve could not be
269      *             constructed
270      */
271     public static PolyLine2d cubic(final Ray2d.html#Ray2d">Ray2d start, final Ray2d end) throws DrawRuntimeException
272     {
273         return cubic(DEFAULT_BEZIER_SIZE, start, end);
274     }
275 
276     /**
277      * Approximate a B&eacute;zier curve of degree n.
278      * @param size int; the number of points for the B&eacute;zier curve to be constructed
279      * @param points Point2d...; the points of the curve, where the first and last are begin and end point, and the intermediate
280      *            ones are control points. There should be at least two points.
281      * @return PolyLine2d; an approximation of a cubic B&eacute;zier curve between start and end, using the provided control
282      *         points
283      * @throws NullPointerException when points contains a null
284      * @throws DrawRuntimeException in case the number of points is less than 2, size is less than 2, or the B&eacute;zier curve
285      *             could not be constructed
286      */
287     public static PolyLine2d bezier(final int size, final Point2d... points) throws NullPointerException, DrawRuntimeException
288     {
289         Throw.when(points.length < 2, DrawRuntimeException.class, "Too few points; need at least two");
290         Throw.when(size < 2, DrawRuntimeException.class, "size too small (must be at least 2)");
291         Point2dhtml#Point2d">Point2d[] result = new Point2d[size];
292         double[] px = new double[points.length];
293         double[] py = new double[points.length];
294         for (int i = 0; i < points.length; i++)
295         {
296             Point2d p = points[i];
297             Throw.whenNull(p, "points contains a null value");
298             px[i] = p.x;
299             py[i] = p.y;
300         }
301         for (int n = 0; n < size; n++)
302         {
303             double t = n / (size - 1.0);
304             double x = Bn(t, px);
305             double y = Bn(t, py);
306             result[n] = new Point2d(x, y);
307         }
308         return new PolyLine2d(result);
309     }
310 
311     /**
312      * Approximate a B&eacute;zier curve of degree n using <code>DEFAULT_BEZIER_SIZE</code> points.
313      * @param points Point2d...; the points of the curve, where the first and last are begin and end point, and the intermediate
314      *            ones are control points. There should be at least two points.
315      * @return PolyLine2d; an approximation of a cubic B&eacute;zier curve between start and end, using the provided control
316      *         points
317      * @throws NullPointerException when points contains a null value
318      * @throws DrawRuntimeException in case the number of points is less than 2 or the B&eacute;zier curve could not be
319      *             constructed
320      */
321     public static PolyLine2d bezier(final Point2d... points) throws NullPointerException, DrawRuntimeException
322     {
323         return bezier(DEFAULT_BEZIER_SIZE, points);
324     }
325 
326     /**
327      * Approximate a B&eacute;zier curve of degree n with a specified precision.
328      * @param epsilon double; the precision.
329      * @param points Point2d...; the points of the curve, where the first and last are begin and end point, and the intermediate
330      *            ones are control points. There should be at least two points.
331      * @return PolyLine2d; an approximation of a cubic B&eacute;zier curve between start and end, with the provided control
332      *         points
333      * @throws NullPointerException when points contains a null value
334      * @throws DrawRuntimeException in case the number of points is less than 2 or the B&eacute;zier curve could not be
335      *             constructed
336      */
337     public static PolyLine2d bezier(final double epsilon, final Point2d... points)
338             throws NullPointerException, DrawRuntimeException
339     {
340         Throw.when(points.length < 2, DrawRuntimeException.class, "Too few points; need at least two");
341         Throw.when(Double.isNaN(epsilon) || epsilon <= 0, DrawRuntimeException.class,
342                 "epsilonPosition must be a positive number");
343         NavigableMap<Double, Point2d> result = new TreeMap<>();
344         double[] px = new double[points.length];
345         double[] py = new double[points.length];
346         for (int i = 0; i < points.length; i++)
347         {
348             Point2d p = points[i];
349             Throw.whenNull(p, "points contains a null value");
350             px[i] = p.x;
351             py[i] = p.y;
352         }
353         int initialSize = points.length - 1;
354         for (int n = 0; n < initialSize; n++)
355         {
356             double t = n / (initialSize - 1.0);
357             double x = Bn(t, px);
358             double y = Bn(t, py);
359             result.put(t, new Point2d(x, y));
360         }
361         // Walk along all point pairs and see if additional points need to be inserted
362         Double prevT = result.firstKey();
363         Point2d prevPoint = result.get(prevT);
364         Map.Entry<Double, Point2d> entry;
365         while ((entry = result.higherEntry(prevT)) != null)
366         {
367             Double nextT = entry.getKey();
368             Point2d nextPoint = entry.getValue();
369             double medianT = (prevT + nextT) / 2;
370             double x = Bn(medianT, px);
371             double y = Bn(medianT, py);
372             Point2dl#Point2d">Point2d medianPoint = new Point2d(x, y);
373             Point2d projectedPoint = medianPoint.closestPointOnSegment(prevPoint, nextPoint);
374             double errorPosition = medianPoint.distance(projectedPoint);
375             if (errorPosition >= epsilon)
376             {
377                 // We need to insert another point
378                 result.put(medianT, medianPoint);
379                 continue;
380             }
381             if (prevPoint.distance(nextPoint) > epsilon)
382             {
383                 // Check for an inflection point by creating additional points at one quarter and three quarters. If these
384                 // are on opposite sides of the line from prevPoint to nextPoint; there must be an inflection point.
385                 // https://stackoverflow.com/questions/1560492/how-to-tell-whether-a-point-is-to-the-right-or-left-side-of-a-line
386                 double quarterT = (prevT + medianT) / 2;
387                 double quarterX = Bn(quarterT, px);
388                 double quarterY = Bn(quarterT, py);
389                 int sign1 = (int) Math.signum((nextPoint.x - prevPoint.x) * (quarterY - prevPoint.y)
390                         - (nextPoint.y - prevPoint.y) * (quarterX - prevPoint.x));
391                 double threeQuarterT = (nextT + medianT) / 2;
392                 double threeQuarterX = Bn(threeQuarterT, px);
393                 double threeQuarterY = Bn(threeQuarterT, py);
394                 int sign2 = (int) Math.signum((nextPoint.x - prevPoint.x) * (threeQuarterY - prevPoint.y)
395                         - (nextPoint.y - prevPoint.y) * (threeQuarterX - prevPoint.x));
396                 if (sign1 != sign2)
397                 {
398                     // There is an inflection point
399                     // System.out.println("Detected inflection point between " + prevPoint + " and " + nextPoint);
400                     // Inserting the halfway point should take care of this
401                     result.put(medianT, medianPoint);
402                     continue;
403                 }
404             }
405             // TODO check angles
406             prevT = nextT;
407             prevPoint = nextPoint;
408         }
409         try
410         {
411             return new PolyLine2d(result.values().iterator());
412         }
413         catch (NullPointerException | DrawRuntimeException e)
414         {
415             // Cannot happen? Really?
416             e.printStackTrace();
417             throw new DrawRuntimeException(e);
418         }
419     }
420 
421     /**
422      * Approximate a cubic B&eacute;zier curve from start to end with two control points.
423      * @param size int; the number of points for the B&eacute;zier curve
424      * @param start Point3d; the start point of the B&eacute;zier curve
425      * @param control1 Point3d; the first control point
426      * @param control2 Point3d; the second control point
427      * @param end Point3d; the end point of the B&eacute;zier curve
428      * @return PolyLine3d; an approximation of a cubic B&eacute;zier curve between start and end, with the two provided control
429      *         points
430      * @throws DrawRuntimeException in case the number of points is less than 2 or the B&eacute;zier curve could not be
431      *             constructed
432      */
433     public static PolyLine3d cubic(final int size, final Point3dtml#Point3d">Point3dd.html#Point3d">Point3d start, final Point3dtml#Point3d">Point3d control1, final Point3d control2,
434             final Point3d end) throws DrawRuntimeException
435     {
436         return bezier(size, start, control1, control2, end);
437     }
438 
439     /**
440      * Approximate a cubic B&eacute;zier curve from start to end with two control points with a specified precision.
441      * @param epsilon double; the precision.
442      * @param start Point3d; the start point of the B&eacute;zier curve
443      * @param control1 Point3d; the first control point
444      * @param control2 Point3d; the second control point
445      * @param end Point3d; the end point of the B&eacute;zier curve
446      * @return PolyLine3d; an approximation of a cubic B&eacute;zier curve between start and end, with the two provided control
447      *         points
448      * @throws DrawRuntimeException in case the number of points is less than 2 or the B&eacute;zier curve could not be
449      *             constructed
450      */
451     public static PolyLine3d cubic(final double epsilon, final Point3dtml#Point3d">Point3dd.html#Point3d">Point3d start, final Point3dtml#Point3d">Point3d control1, final Point3d control2,
452             final Point3d end) throws DrawRuntimeException
453     {
454         return bezier(epsilon, start, control1, control2, end);
455     }
456 
457     /**
458      * Approximate a cubic B&eacute;zier curve from start to end with two generated control points at half the distance between
459      * start and end.
460      * @param size int; the number of points for the B&eacute;zier curve
461      * @param start Ray3d; the start point and start direction of the B&eacute;zier curve
462      * @param end Ray3d; the end point and end direction of the B&eacute;zier curve
463      * @return PolyLine2d; an approximation of a cubic B&eacute;zier curve between start and end, with the two provided control
464      *         points
465      * @throws DrawRuntimeException in case the number of points is less than 2 or the B&eacute;zier curve could not be
466      *             constructed
467      */
468     public static PolyLine3d cubic(final int size, final Ray3d.html#Ray3d">Ray3d start, final Ray3d end) throws DrawRuntimeException
469     {
470         return cubic(size, start, end, 1.0);
471     }
472 
473     /**
474      * Approximate a cubic B&eacute;zier curve from start to end with two generated control points at half the distance between
475      * start and end with specified precision.
476      * @param epsilon double; the precision.
477      * @param start Ray3d; the start point and start direction of the B&eacute;zier curve
478      * @param end Ray3d; the end point and end direction of the B&eacute;zier curve
479      * @return PolyLine2d; an approximation of a cubic B&eacute;zier curve between start and end, with the two provided control
480      *         points
481      * @throws DrawRuntimeException in case the number of points is less than 2 or the B&eacute;zier curve could not be
482      *             constructed
483      */
484     public static PolyLine3d cubic(final double epsilon, final Ray3d.html#Ray3d">Ray3d start, final Ray3d end) throws DrawRuntimeException
485     {
486         return cubic(epsilon, start, end, 1.0);
487     }
488 
489     /**
490      * Approximate a cubic B&eacute;zier curve from start to end with two generated control points at half the distance between
491      * start and end.
492      * @param size int; the number of points for the B&eacute;zier curve
493      * @param start Ray3d; the start point and start direction of the B&eacute;zier curve
494      * @param end Ray3d; the end point and end direction of the B&eacute;zier curve
495      * @param shape shape factor; 1 = control points at half the distance between start and end, &gt; 1 results in a pointier
496      *            shape, &lt; 1 results in a flatter shape, value should be above 0 and finite
497      * @return a cubic B&eacute;zier curve between start and end, with the two determined control points
498      * @throws DrawRuntimeException in case the number of points is less than 2 or the B&eacute;zier curve could not be
499      *             constructed
500      */
501     public static PolyLine3d cubic(final int size, final Ray3d.html#Ray3d">Ray3d start, final Ray3d end, final double shape)
502             throws DrawRuntimeException
503     {
504         Throw.when(Double.isNaN(shape) || Double.isInfinite(shape) || shape <= 0, DrawRuntimeException.class,
505                 "shape must be a finite, positive value");
506         return cubic(size, start, end, shape, false);
507     }
508 
509     /**
510      * Approximate a cubic B&eacute;zier curve from start to end with two generated control points at half the distance between
511      * start and end with specified precision.
512      * @param epsilon double; the precision.
513      * @param start Ray3d; the start point and start direction of the B&eacute;zier curve
514      * @param end Ray3d; the end point and end direction of the B&eacute;zier curve
515      * @param shape shape factor; 1 = control points at half the distance between start and end, &gt; 1 results in a pointier
516      *            shape, &lt; 1 results in a flatter shape, value should be above 0 and finite
517      * @return a cubic B&eacute;zier curve between start and end, with the two determined control points
518      * @throws DrawRuntimeException in case the number of points is less than 2 or the B&eacute;zier curve could not be
519      *             constructed
520      */
521     public static PolyLine3d cubic(final double epsilon, final Ray3d.html#Ray3d">Ray3d start, final Ray3d end, final double shape)
522             throws DrawRuntimeException
523     {
524         Throw.when(Double.isNaN(shape) || Double.isInfinite(shape) || shape <= 0, DrawRuntimeException.class,
525                 "shape must be a finite, positive value");
526         return cubic(epsilon, start, end, shape, false);
527     }
528 
529     /**
530      * Approximate a cubic B&eacute;zier curve from start to end with two generated control points at half the distance between
531      * start and end. The z-value is interpolated in a linear way.
532      * @param size int; the number of points for the B&eacute;zier curve
533      * @param start Ray3d; the start point and start direction of the B&eacute;zier curve
534      * @param end Ray3d; the end point and end direction of the B&eacute;zier curve
535      * @param shape shape factor; 1 = control points at half the distance between start and end, &gt; 1 results in a pointier
536      *            shape, &lt; 1 results in a flatter shape, value should be above 0
537      * @param weighted boolean; control point distance relates to distance to projected point on extended line from other end
538      * @return a cubic B&eacute;zier curve between start and end, with the two determined control points
539      * @throws NullPointerException when start or end is null
540      * @throws DrawRuntimeException in case size is less than 2, start is at the same location as end, shape is invalid, or the
541      *             B&eacute;zier curve could not be constructed
542      */
543     public static PolyLine3d cubic(final int size, final Ray3d.html#Ray3d">Ray3d start, final Ray3d end, final double shape,
544             final boolean weighted) throws NullPointerException, DrawRuntimeException
545     {
546         Point3d[] points = createControlPoints(start, end, shape, weighted);
547         return cubic(size, points[0], points[1], points[2], points[3]);
548     }
549 
550     /**
551      * Approximate a cubic B&eacute;zier curve from start to end with two generated control points at half the distance between
552      * start and end with specified precision.
553      * @param epsilon double; the precision.
554      * @param start Ray3d; the start point and start direction of the B&eacute;zier curve
555      * @param end Ray3d; the end point and end direction of the B&eacute;zier curve
556      * @param shape shape factor; 1 = control points at half the distance between start and end, &gt; 1 results in a pointier
557      *            shape, &lt; 1 results in a flatter shape, value should be above 0, finite and not NaN
558      * @param weighted boolean; control point distance relates to distance to projected point on extended line from other end
559      * @return PolyLine3d; an approximation of a cubic B&eacute;zier curve between start and end, with the two determined
560      *         control points
561      * @throws NullPointerException when start or end is null
562      * @throws DrawRuntimeException in case size is less than 2, start is at the same location as end, shape is invalid, or the
563      *             B&eacute;zier curve could not be constructed
564      */
565     public static PolyLine3d cubic(final double epsilon, final Ray3d.html#Ray3d">Ray3d start, final Ray3d end, final double shape,
566             final boolean weighted) throws NullPointerException, DrawRuntimeException
567     {
568         Point3d[] points = createControlPoints(start, end, shape, weighted);
569         return cubic(epsilon, points[0], points[1], points[2], points[3]);
570     }
571 
572     /**
573      * Create control points for a cubic B&eacute;zier curve defined by two Rays.
574      * @param start Ray3d; the start point (and direction)
575      * @param end Ray3d; the end point (and direction)
576      * @param shape double; the shape; higher values put the generated control points further away from end and result in a
577      *            pointier B&eacute;zier curve
578      * @param weighted boolean;
579      * @return Point3d[]; an array of four Point3d elements: start, the first control point, the second control point, end.
580      */
581     private static Point3d[] createControlPoints(final Ray3d.html#Ray3d">Ray3d start, final Ray3d end, final double shape, final boolean weighted)
582     {
583         Throw.whenNull(start, "start point may not be null");
584         Throw.whenNull(end, "end point may not be null");
585         Throw.when(start.distanceSquared(end) == 0, DrawRuntimeException.class,
586                 "Cannot create control points if start and end points coincide");
587         Throw.when(Double.isNaN(shape) || shape <= 0 || Double.isInfinite(shape), DrawRuntimeException.class,
588                 "shape must be a finite, positive value");
589 
590         Point3d control1;
591         Point3d control2;
592         if (weighted)
593         {
594             // each control point is 'w' * the distance between the end-points away from the respective end point
595             // 'w' is a weight given by the distance from the end point to the extended line of the other end point
596             double distance = shape * start.distance(end);
597             double dStart = start.distance(end.projectOrthogonalExtended(start));
598             double dEnd = end.distance(start.projectOrthogonalExtended(end));
599             double wStart = dStart / (dStart + dEnd);
600             double wEnd = dEnd / (dStart + dEnd);
601             control1 = start.getLocation(distance * wStart);
602             control2 = end.getLocationExtended(-distance * wEnd);
603         }
604         else
605         {
606             // each control point is half the distance between the end-points away from the respective end point
607             double distance = shape * start.distance(end) / 2.0;
608             control1 = start.getLocation(distance);
609             control2 = end.getLocationExtended(-distance);
610         }
611         return new Point3d[] {start, control1, control2, end};
612     }
613 
614     /**
615      * Construct a cubic B&eacute;zier curve from start to end with two generated control points at half the distance between
616      * start and end. The z-value is interpolated in a linear way. The size of the constructed curve is
617      * <code>DEFAULT_BEZIER_SIZE</code>.
618      * @param start Ray3d; the start point and orientation of the B&eacute;zier curve
619      * @param end Ray3d; the end point and orientation of the B&eacute;zier curve
620      * @return a cubic B&eacute;zier curve between start and end, with the two provided control points
621      * @throws DrawRuntimeException in case the number of points is less than 2 or the B&eacute;zier curve could not be
622      *             constructed
623      */
624     public static PolyLine3d cubic(final Ray3d.html#Ray3d">Ray3d start, final Ray3d end) throws DrawRuntimeException
625     {
626         return cubic(DEFAULT_BEZIER_SIZE, start, end);
627     }
628 
629     /**
630      * Calculate the cubic B&eacute;zier point with B(t) = (1 - t)<sup>3</sup>P<sub>0</sub> + 3t(1 - t)<sup>2</sup>
631      * P<sub>1</sub> + 3t<sup>2</sup> (1 - t) P<sub>2</sub> + t<sup>3</sup> P<sub>3</sub>.
632      * @param t double; the fraction
633      * @param p0 double; the first point of the curve
634      * @param p1 double; the first control point
635      * @param p2 double; the second control point
636      * @param p3 double; the end point of the curve
637      * @return the cubic bezier value B(t)
638      */
639     @SuppressWarnings("checkstyle:methodname")
640     private static double B3(final double t, final double p0, final double p1, final double p2, final double p3)
641     {
642         double t2 = t * t;
643         double t3 = t2 * t;
644         double m = (1.0 - t);
645         double m2 = m * m;
646         double m3 = m2 * m;
647         return m3 * p0 + 3.0 * t * m2 * p1 + 3.0 * t2 * m * p2 + t3 * p3;
648     }
649 
650     /**
651      * Construct a B&eacute;zier curve of degree n.
652      * @param size int; the number of points for the B&eacute;zier curve to be constructed
653      * @param points Point3d...; the points of the curve, where the first and last are begin and end point, and the intermediate
654      *            ones are control points. There should be at least two points.
655      * @return the B&eacute;zier value B(t) of degree n, where n is the number of points in the array
656      * @throws DrawRuntimeException in case the number of points is less than 2 or the B&eacute;zier curve could not be
657      *             constructed
658      */
659     public static PolyLine3d bezier(final int size, final Point3d... points) throws DrawRuntimeException
660     {
661         Throw.when(points.length < 2, DrawRuntimeException.class, "Too few points; need at least two");
662         Throw.when(size < 2, DrawRuntimeException.class, "size too small (must be at least 2)");
663         Point3dhtml#Point3d">Point3d[] result = new Point3d[size];
664         double[] px = new double[points.length];
665         double[] py = new double[points.length];
666         double[] pz = new double[points.length];
667         for (int i = 0; i < points.length; i++)
668         {
669             px[i] = points[i].x;
670             py[i] = points[i].y;
671             pz[i] = points[i].z;
672         }
673         for (int n = 0; n < size; n++)
674         {
675             double t = n / (size - 1.0);
676             double x = Bn(t, px);
677             double y = Bn(t, py);
678             double z = Bn(t, pz);
679             result[n] = new Point3d(x, y, z);
680         }
681         return new PolyLine3d(result);
682     }
683 
684     /**
685      * Approximate a B&eacute;zier curve of degree n using <code>DEFAULT_BEZIER_SIZE</code> points.
686      * @param points Point3d...; the points of the curve, where the first and last are begin and end point, and the intermediate
687      *            ones are control points. There should be at least two points.
688      * @return the B&eacute;zier value B(t) of degree n, where n is the number of points in the array
689      * @throws DrawRuntimeException in case the number of points is less than 2 or the B&eacute;zier curve could not be
690      *             constructed
691      */
692     public static PolyLine3d bezier(final Point3d... points) throws DrawRuntimeException
693     {
694         return bezier(DEFAULT_BEZIER_SIZE, points);
695     }
696 
697     /**
698      * Approximate a B&eacute;zier curve of degree n with a specified precision.
699      * @param epsilon double; the precision.
700      * @param points Point3d...; the points of the curve, where the first and last are begin and end point, and the intermediate
701      *            ones are control points. There should be at least two points.
702      * @return PolyLine3d; an approximation of a cubic B&eacute;zier curve between start and end, with the provided control
703      *         points
704      * @throws NullPointerException when points contains a null value
705      * @throws DrawRuntimeException in case the number of points is less than 2 or the B&eacute;zier curve could not be
706      *             constructed
707      */
708     public static PolyLine3d bezier(final double epsilon, final Point3d... points)
709             throws NullPointerException, DrawRuntimeException
710     {
711         Throw.when(points.length < 2, DrawRuntimeException.class, "Too few points; need at least two");
712         Throw.when(Double.isNaN(epsilon) || epsilon <= 0, DrawRuntimeException.class,
713                 "epsilonPosition must be a positive number");
714         NavigableMap<Double, Point3d> result = new TreeMap<>();
715         double[] px = new double[points.length];
716         double[] py = new double[points.length];
717         double[] pz = new double[points.length];
718         for (int i = 0; i < points.length; i++)
719         {
720             Point3d p = points[i];
721             Throw.whenNull(p, "points contains a null value");
722             px[i] = p.x;
723             py[i] = p.y;
724             pz[i] = p.z;
725         }
726         int initialSize = points.length - 1;
727         for (int n = 0; n < initialSize; n++)
728         {
729             double t = n / (initialSize - 1.0);
730             double x = Bn(t, px);
731             double y = Bn(t, py);
732             double z = Bn(t, pz);
733             result.put(t, new Point3d(x, y, z));
734         }
735         // Walk along all point pairs and see if additional points need to be inserted
736         Double prevT = result.firstKey();
737         Point3d prevPoint = result.get(prevT);
738         Map.Entry<Double, Point3d> entry;
739         while ((entry = result.higherEntry(prevT)) != null)
740         {
741             Double nextT = entry.getKey();
742             Point3d nextPoint = entry.getValue();
743             double medianT = (prevT + nextT) / 2;
744             double x = Bn(medianT, px);
745             double y = Bn(medianT, py);
746             double z = Bn(medianT, pz);
747             Point3dl#Point3d">Point3d medianPoint = new Point3d(x, y, z);
748             Point3d projectedPoint = medianPoint.closestPointOnSegment(prevPoint, nextPoint);
749             double errorPosition = medianPoint.distance(projectedPoint);
750             if (errorPosition >= epsilon)
751             {
752                 // We need to insert another point
753                 result.put(medianT, medianPoint);
754                 continue;
755             }
756             if (prevPoint.distance(nextPoint) > epsilon)
757             {
758                 // Check for an inflection point by creating additional points at one quarter and three quarters. If these
759                 // are on opposite sides of the line from prevPoint to nextPoint; there must be an inflection point.
760                 // https://stackoverflow.com/questions/1560492/how-to-tell-whether-a-point-is-to-the-right-or-left-side-of-a-line
761                 double quarterT = (prevT + medianT) / 2;
762                 double quarterX = Bn(quarterT, px);
763                 double quarterY = Bn(quarterT, py);
764                 int sign1 = (int) Math.signum((nextPoint.x - prevPoint.x) * (quarterY - prevPoint.y)
765                         - (nextPoint.y - prevPoint.y) * (quarterX - prevPoint.x));
766                 double threeQuarterT = (nextT + medianT) / 2;
767                 double threeQuarterX = Bn(threeQuarterT, px);
768                 double threeQuarterY = Bn(threeQuarterT, py);
769                 int sign2 = (int) Math.signum((nextPoint.x - prevPoint.x) * (threeQuarterY - prevPoint.y)
770                         - (nextPoint.y - prevPoint.y) * (threeQuarterX - prevPoint.x));
771                 if (sign1 != sign2)
772                 {
773                     // There is an inflection point
774                     System.out.println("Detected inflection point between " + prevPoint + " and " + nextPoint);
775                     // Inserting the halfway point should take care of this
776                     result.put(medianT, medianPoint);
777                     continue;
778                 }
779             }
780             // TODO check angles
781             prevT = nextT;
782             prevPoint = nextPoint;
783         }
784         try
785         {
786             return new PolyLine3d(result.values().iterator());
787         }
788         catch (NullPointerException | DrawRuntimeException e)
789         {
790             // Cannot happen? Really?
791             e.printStackTrace();
792             throw new DrawRuntimeException(e);
793         }
794     }
795 
796     /**
797      * Calculate the B&eacute;zier point of degree n, with B(t) = Sum(i = 0..n) [C(n, i) * (1 - t)<sup>n-i</sup> t<sup>i</sup>
798      * P<sub>i</sub>], where C(n, k) is the binomial coefficient defined by n! / ( k! (n-k)! ), ! being the factorial operator.
799      * @param t double; the fraction
800      * @param p double...; the points of the curve, where the first and last are begin and end point, and the intermediate ones
801      *            are control points
802      * @return the B&eacute;zier value B(t) of degree n, where n is the number of points in the array
803      */
804     @SuppressWarnings("checkstyle:methodname")
805     private static double Bn(final double t, final double... p)
806     {
807         double b = 0.0;
808         double m = (1.0 - t);
809         int n = p.length - 1;
810         double fn = factorial(n);
811         for (int i = 0; i <= n; i++)
812         {
813             double c = fn / (factorial(i) * (factorial(n - i)));
814             b += c * Math.pow(m, n - i) * Math.pow(t, i) * p[i];
815         }
816         return b;
817     }
818 
819     /**
820      * Calculate factorial(k), which is k * (k-1) * (k-2) * ... * 1. For factorials up to 20, a lookup table is used.
821      * @param k int; the parameter
822      * @return factorial(k)
823      */
824     private static double factorial(final int k)
825     {
826         if (k < fact.length)
827         {
828             return fact[k];
829         }
830         double f = 1;
831         for (int i = 2; i <= k; i++)
832         {
833             f = f * i;
834         }
835         return f;
836     }
837 
838 }