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é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é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é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é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ézier curve
69 * @param end the end point and end direction of the Bé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é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ézier curve
82 * @param end the end point and end direction of the Bézier curve
83 * @param shape 1 = control points at half the distance between start and end, > 1 results in a pointier
84 * shape, < 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 ≤ 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é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ézier curve
98 * @param end the end point and end direction of the Bézier curve
99 * @param shape 1 = control points at half the distance between start and end, > 1 results in a pointier
100 * shape, < 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 ≤ 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é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é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 ≤ 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 }