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