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 }