View Javadoc
1   package org.djutils.draw.curve;
2   
3   import org.djutils.draw.line.Ray3d;
4   import org.djutils.draw.point.Point3d;
5   import org.djutils.exceptions.Throw;
6   
7   /**
8    * Continuous definition of a cubic Bézier curves in 3d. This extends from the more general {@code Bezier} as certain
9    * methods are applied to calculate e.g. the roots, that are specific to cubic Bézier curves. With such information this
10   * class can also specify information to be a {@code Curve}.
11   * <p>
12   * Copyright (c) 2023-2025 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See
13   * for project information <a href="https://djutils.org" target="_blank"> https://djutils.org</a>. The DJUTILS project is
14   * distributed under a three-clause BSD-style license, which can be found at
15   * <a href="https://djutils.org/docs/license.html" target="_blank"> https://djutils.org/docs/license.html</a>.
16   * </p>
17   * @author <a href="https://www.tudelft.nl/averbraeck">Alexander Verbraeck</a>
18   * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
19   * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
20   * @see <a href="https://pomax.github.io/bezierinfo/">B&eacute;zier info</a>
21   */
22  public class BezierCubic3d extends Bezier3d implements Curve3d
23  {
24      /** Length. */
25      private final double length;
26  
27      /**
28       * Create a cubic B&eacute;zier curve.
29       * @param start start point.
30       * @param control1 first intermediate shape point.
31       * @param control2 second intermediate shape point.
32       * @param end end point.
33       * @throws NullPointerException when <code>start</code>, <code>control1</code>, <code>control2</code>, or <code>end</code>
34       *             is <code>null</code>
35       */
36      public BezierCubic3d(final Point3d start, final Point3d control1, final Point3d control2, final Point3d end)
37      {
38          super(start, control1, control2, end);
39          this.length = length();
40      }
41  
42      /**
43       * Create a cubic B&eacute;zier curve.
44       * @param points array containing four Point2d objects
45       * @throws NullPointerException when <code>points</code> is <code>null</code>, or contains a <code>null</code> value
46       * @throws IllegalArgumentException when length of <code>points</code> is not equal to <code>4</code>
47       */
48      public BezierCubic3d(final Point3d[] points)
49      {
50          this(checkArray(points)[0], points[1], points[2], points[3]);
51      }
52  
53      /**
54       * Verify that a Point3d[] contains exactly 4 elements.
55       * @param points the array to check
56       * @return the provided array
57       * @throws IllegalArgumentException when length of <code>points</code> is not <code>4</code>
58       */
59      private static Point3d[] checkArray(final Point3d[] points)
60      {
61          Throw.when(points.length != 4, IllegalArgumentException.class, "points must contain exactly 4 Point2d objects");
62          return points;
63      }
64  
65      /**
66       * Approximate a cubic B&eacute;zier curve from start to end with two generated control points at half the distance between
67       * start and end.
68       * @param start the start point and start direction of the B&eacute;zier curve
69       * @param end the end point and end direction of the B&eacute;zier curve
70       * @throws NullPointerException when <code>start</code>, or <code>end</code> is <code>null</code>
71       * @throws IllegalArgumentException when <code>start</code> and <code>end</code> are at the same location
72       */
73      public BezierCubic3d(final Ray3d start, final Ray3d end)
74      {
75          this(start, end, 1.0);
76      }
77  
78      /**
79       * Approximate a cubic B&eacute;zier curve from start to end with two generated control points at half the distance between
80       * start and end.
81       * @param start the start point and start direction of the B&eacute;zier curve
82       * @param end the end point and end direction of the B&eacute;zier curve
83       * @param shape 1 = control points at half the distance between start and end, &gt; 1 results in a pointier
84       *            shape, &lt; 1 results in a flatter shape, value should be above 0 and finite
85       * @throws NullPointerException when <code>start</code>, or <code>end</code> is <code>null</code>
86       * @throws IllegalArgumentException when <code>start</code> and <code>end</code> are at the same location,
87       *             <code>shape &le; 0</code>, <code>shape</code> is <code>NaN</code>, or infinite
88       */
89      public BezierCubic3d(final Ray3d start, final Ray3d end, final double shape)
90      {
91          this(start, end, shape, false);
92      }
93  
94      /**
95       * Approximate a cubic B&eacute;zier curve from start to end with two generated control points at half the distance between
96       * start and end.
97       * @param start the start point and start direction of the B&eacute;zier curve
98       * @param end the end point and end direction of the B&eacute;zier curve
99       * @param shape 1 = control points at half the distance between start and end, &gt; 1 results in a pointier
100      *            shape, &lt; 1 results in a flatter shape, value should be above 0 and finite
101      * @param weighted control point distance relates to distance to projected point on extended line from other end
102      * @throws NullPointerException when <code>start</code>, or <code>end</code> is <code>null</code>
103      * @throws IllegalArgumentException when <code>start</code> and <code>end</code> are at the same location,
104      *             <code>shape &le; 0</code>, <code>shape</code> is <code>NaN</code>, or infinite
105      */
106     public BezierCubic3d(final Ray3d start, final Ray3d end, final double shape, final boolean weighted)
107 
108     {
109         this(createControlPoints(start, end, shape, weighted));
110     }
111 
112     /**
113      * Create control points for a cubic B&eacute;zier curve defined by two Rays.
114      * @param start the start point (and direction)
115      * @param end the end point (and direction)
116      * @param shape the shape; higher values put the generated control points further away from end and result in a
117      *            pointier B&eacute;zier curve
118      * @param weighted whether weights will be applied 
119      * @return an array of four Point3d elements: start, the first control point, the second control point, end.
120      * @throws NullPointerException when <code>start</code>, or <code>end</code> is <code>null</code>
121      * @throws IllegalArgumentException when <code>start</code> and <code>end</code> are at the same location,
122      *             <code>shape &le; 0</code>, <code>shape</code> is <code>NaN</code>, or infinite
123      */
124     private static Point3d[] createControlPoints(final Ray3d start, final Ray3d end, final double shape, final boolean weighted)
125     {
126         Throw.whenNull(start, "start");
127         Throw.whenNull(end, "end");
128         Throw.when(start.distanceSquared(end) == 0, IllegalArgumentException.class,
129                 "Cannot create control points if start and end points coincide");
130         Throw.whenNaN(shape, "shape");
131         Throw.when(shape <= 0 || Double.isInfinite(shape), IllegalArgumentException.class,
132                 "shape must be a finite, positive value");
133 
134         Point3d control1;
135         Point3d control2;
136         if (weighted)
137         {
138             // each control point is 'w' * the distance between the end-points away from the respective end point
139             // 'w' is a weight given by the distance from the end point to the extended line of the other end point
140             double distance = shape * start.distance(end);
141             double dStart = start.distance(end.projectOrthogonalExtended(start));
142             double dEnd = end.distance(start.projectOrthogonalExtended(end));
143             double wStart = dStart / (dStart + dEnd);
144             double wEnd = dEnd / (dStart + dEnd);
145             control1 = start.getLocation(distance * wStart);
146             control2 = end.getLocationExtended(-distance * wEnd);
147         }
148         else
149         {
150             // each control point is half the distance between the end-points away from the respective end point
151             double distance = shape * start.distance(end) / 2.0;
152             control1 = start.getLocation(distance);
153             control2 = end.getLocationExtended(-distance);
154         }
155         return new Point3d[] {start, control1, control2, end};
156     }
157 
158     @Override
159     public double getLength()
160     {
161         return this.length;
162     }
163 
164 }