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ézier curves. <br>
15 * The class implements the cubic(...) method to generate a cubic Bé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ézier curve is implemented with the bezier(...) function.
24 * <p>
25 * Copyright (c) 2013-2023 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é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ézier curve from start to end with two control points.
49 * @param size int; the number of points of the Bézier curve
50 * @param start Point2d; the start point of the Bé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ézier curve
54 * @return PolyLine2d; an approximation of a cubic Bé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ézier curve could not be
57 * constructed
58 */
59 public static PolyLine2d cubic(final int size, final Point2d start, final 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 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é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é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ézier curve
81 * @return PolyLine2d; an approximation of a cubic Bé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ézier curve could not be
84 * constructed
85 */
86 public static PolyLine2d cubic(final double epsilon, final Point2d start, final 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é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ézier curve
96 * @param start Ray2d; the start point and start direction of the Bézier curve
97 * @param end Ray2d; the end point and end direction of the Bézier curve
98 * @return PolyLine2d; an approximation of a cubic Bé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ézier curve could not be
101 * constructed
102 */
103 public static PolyLine2d cubic(final int size, final Ray2d start, final Ray2d end) throws DrawRuntimeException
104 {
105 return cubic(size, start, end, 1.0);
106 }
107
108 /**
109 * Approximate a cubic Bé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ézier curve
113 * @param end Ray2d; the end point and end direction of the Bézier curve
114 * @return PolyLine2d; an approximation of a cubic Bé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ézier curve could not be
117 * constructed
118 */
119 public static PolyLine2d cubic(final double epsilon, final Ray2d start, final Ray2d end) throws DrawRuntimeException
120 {
121 return cubic(epsilon, start, end, 1.0);
122 }
123
124 /**
125 * Approximate a cubic Bé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ézier curve
128 * @param start Ray2d; the start point and start direction of the Bézier curve
129 * @param end Ray2d; the end point and end direction of the Bézier curve
130 * @param shape shape factor; 1 = control points at half the distance between start and end, > 1 results in a pointier
131 * shape, < 1 results in a flatter shape, value should be above 0 and finite
132 * @return PolyLine2d; an approximation of a cubic Bé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ézier curve could not be
135 * constructed
136 */
137 public static PolyLine2d cubic(final int size, final 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é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ézier curve
150 * @param end Ray2d; the end point and end direction of the Bézier curve
151 * @param shape shape factor; 1 = control points at half the distance between start and end, > 1 results in a pointier
152 * shape, < 1 results in a flatter shape, value should be above 0 and finite
153 * @return PolyLine2d; an approximation of a cubic Bé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ézier curve could not be
156 * constructed
157 */
158 public static PolyLine2d cubic(final double epsilon, final 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é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ézier curve
170 * @param start Ray2d; the start point and start direction of the Bézier curve
171 * @param end Ray2d; the end point and end direction of the Bézier curve
172 * @param shape shape factor; 1 = control points at half the distance between start and end, > 1 results in a pointier
173 * shape, < 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é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ézier curve could not be constructed
180 */
181 public static PolyLine2d cubic(final int size, final 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é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ézier curve
193 * @param end Ray2d; the end point and end direction of the Bézier curve
194 * @param shape shape factor; 1 = control points at half the distance between start and end, > 1 results in a pointier
195 * shape, < 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é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 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 Point2d UNIT_VECTOR2D = new Point2d(1, 0);
211
212 /**
213 * Create control points for a cubic Bé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é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 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é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ézier curve
265 * @param end Ray2d; the end point and end direction of the Bézier curve
266 * @return PolyLine2d; an approximation of a cubic Bé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ézier curve could not be
269 * constructed
270 */
271 public static PolyLine2d cubic(final Ray2d start, final Ray2d end) throws DrawRuntimeException
272 {
273 return cubic(DEFAULT_BEZIER_SIZE, start, end);
274 }
275
276 /**
277 * Approximate a Bézier curve of degree n.
278 * @param size int; the number of points for the Bé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é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é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 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é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é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é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é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é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é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 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ézier curve from start to end with two control points.
423 * @param size int; the number of points for the Bézier curve
424 * @param start Point3d; the start point of the Bé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ézier curve
428 * @return PolyLine3d; an approximation of a cubic Bé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ézier curve could not be
431 * constructed
432 */
433 public static PolyLine3d cubic(final int size, final Point3d start, final 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é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é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ézier curve
446 * @return PolyLine3d; an approximation of a cubic Bé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ézier curve could not be
449 * constructed
450 */
451 public static PolyLine3d cubic(final double epsilon, final Point3d start, final 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é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ézier curve
461 * @param start Ray3d; the start point and start direction of the Bézier curve
462 * @param end Ray3d; the end point and end direction of the Bézier curve
463 * @return PolyLine2d; an approximation of a cubic Bé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ézier curve could not be
466 * constructed
467 */
468 public static PolyLine3d cubic(final int size, final Ray3d start, final Ray3d end) throws DrawRuntimeException
469 {
470 return cubic(size, start, end, 1.0);
471 }
472
473 /**
474 * Approximate a cubic Bé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ézier curve
478 * @param end Ray3d; the end point and end direction of the Bézier curve
479 * @return PolyLine2d; an approximation of a cubic Bé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ézier curve could not be
482 * constructed
483 */
484 public static PolyLine3d cubic(final double epsilon, final Ray3d start, final Ray3d end) throws DrawRuntimeException
485 {
486 return cubic(epsilon, start, end, 1.0);
487 }
488
489 /**
490 * Approximate a cubic Bé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ézier curve
493 * @param start Ray3d; the start point and start direction of the Bézier curve
494 * @param end Ray3d; the end point and end direction of the Bézier curve
495 * @param shape shape factor; 1 = control points at half the distance between start and end, > 1 results in a pointier
496 * shape, < 1 results in a flatter shape, value should be above 0 and finite
497 * @return a cubic Bé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ézier curve could not be
499 * constructed
500 */
501 public static PolyLine3d cubic(final int size, final 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é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ézier curve
514 * @param end Ray3d; the end point and end direction of the Bézier curve
515 * @param shape shape factor; 1 = control points at half the distance between start and end, > 1 results in a pointier
516 * shape, < 1 results in a flatter shape, value should be above 0 and finite
517 * @return a cubic Bé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ézier curve could not be
519 * constructed
520 */
521 public static PolyLine3d cubic(final double epsilon, final 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é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ézier curve
533 * @param start Ray3d; the start point and start direction of the Bézier curve
534 * @param end Ray3d; the end point and end direction of the Bézier curve
535 * @param shape shape factor; 1 = control points at half the distance between start and end, > 1 results in a pointier
536 * shape, < 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é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ézier curve could not be constructed
542 */
543 public static PolyLine3d cubic(final int size, final 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é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ézier curve
555 * @param end Ray3d; the end point and end direction of the Bézier curve
556 * @param shape shape factor; 1 = control points at half the distance between start and end, > 1 results in a pointier
557 * shape, < 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é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ézier curve could not be constructed
564 */
565 public static PolyLine3d cubic(final double epsilon, final 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é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é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 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é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ézier curve
619 * @param end Ray3d; the end point and orientation of the Bézier curve
620 * @return a cubic Bé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ézier curve could not be
622 * constructed
623 */
624 public static PolyLine3d cubic(final Ray3d start, final Ray3d end) throws DrawRuntimeException
625 {
626 return cubic(DEFAULT_BEZIER_SIZE, start, end);
627 }
628
629 /**
630 * Calculate the cubic Bé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ézier curve of degree n.
652 * @param size int; the number of points for the Bé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é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é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 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é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é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é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é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é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é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 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é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é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 }