View Javadoc
1   package org.djutils.draw.curve;
2   
3   import java.util.ArrayList;
4   import java.util.Iterator;
5   import java.util.List;
6   import java.util.Map.Entry;
7   import java.util.NavigableMap;
8   import java.util.NavigableSet;
9   import java.util.Set;
10  import java.util.SortedSet;
11  import java.util.TreeMap;
12  import java.util.TreeSet;
13  
14  import org.djutils.draw.Transform2d;
15  import org.djutils.draw.function.ContinuousPiecewiseLinearFunction;
16  import org.djutils.draw.line.PolyLine2d;
17  import org.djutils.draw.line.Ray2d;
18  import org.djutils.draw.point.DirectedPoint2d;
19  import org.djutils.draw.point.Point2d;
20  import org.djutils.exceptions.Throw;
21  
22  /**
23   * Continuous definition of a cubic Bézier curves in 2d. This extends from the more general {@code Bezier} as certain
24   * methods are applied to calculate e.g. the roots, that are specific to cubic Bézier curves. With such information this
25   * class can also specify information to be a {@code Curve}.
26   * <p>
27   * Copyright (c) 2023-2025 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See
28   * for project information <a href="https://djutils.org" target="_blank"> https://djutils.org</a>. The DJUTILS project is
29   * distributed under a three-clause BSD-style license, which can be found at
30   * <a href="https://djutils.org/docs/license.html" target="_blank"> https://djutils.org/docs/license.html</a>.
31   * </p>
32   * @author <a href="https://www.tudelft.nl/averbraeck">Alexander Verbraeck</a>
33   * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
34   * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
35   * @see <a href="https://pomax.github.io/bezierinfo/">B&eacute;zier info</a>
36   */
37  public class BezierCubic2d extends Bezier2d implements Curve2d, OffsetCurve2d, Curvature
38  {
39  
40      /** Angle below which segments are seen as straight. */
41      private static final double STRAIGHT = Math.PI / 36000; // 1/100th of a degree
42  
43      /** Start point with direction. */
44      private final DirectedPoint2d startPoint;
45  
46      /** End point with direction. */
47      private final DirectedPoint2d endPoint;
48  
49      /** Length. */
50      private final double length;
51  
52      /**
53       * Create a cubic B&eacute;zier curve.
54       * @param start start point.
55       * @param control1 first intermediate shape point.
56       * @param control2 second intermediate shape point.
57       * @param end end point.
58       * @throws NullPointerException when <code>start</code>, <code>control1</code>, <code>control2</code>, or <code>end</code>
59       *             is <code>null</code>
60       */
61      public BezierCubic2d(final Point2d start, final Point2d control1, final Point2d control2, final Point2d end)
62      {
63          super(start, control1, control2, end);
64          this.startPoint = new DirectedPoint2d(start, control1);
65          this.endPoint = new DirectedPoint2d(end, control2.directionTo(end));
66          this.length = super.getLength();
67      }
68  
69      /**
70       * Create a cubic B&eacute;zier curve.
71       * @param points array containing four <code>Point2d</code> objects
72       * @throws NullPointerException when <code>points</code> is <code>null</code>, or contains a <code>null</code> value
73       * @throws IllegalArgumentException when length of <code>points</code> is not equal to <code>4</code>
74       */
75      public BezierCubic2d(final Point2d[] points)
76      {
77          this(checkArray(points)[0], points[1], points[2], points[3]);
78      }
79  
80      /**
81       * Verify that a Point2d[] contains exactly 4 elements.
82       * @param points the array to check
83       * @return the provided array
84       * @throws NullPointerException when <code>points</code> is <code>null</code>, or contains a <code>null</code> value
85       * @throws IllegalArgumentException when length of <code>points</code> is not equal to <code>4</code>
86       */
87      private static Point2d[] checkArray(final Point2d[] points)
88      {
89          Throw.when(points.length != 4, IllegalArgumentException.class, "points must contain exactly 4 Point2d objects");
90          return points;
91      }
92  
93      /**
94       * Construct a BezierCubic2d from <code>start</code> to <code>end</code> with two generated control points. This constructor
95       * creates two control points. The first of these is offset from <code>start</code> in the direction of the
96       * <code>start</code> and at a distance from <code>start</code> equal to half the distance from <code>start</code> to
97       * <code>end</code>. The second is placed in a similar relation to <code>end</code>.
98       * @param start the start point and start direction of the B&eacute;zier curve
99       * @param end the end point and end direction of the B&eacute;zier curve
100      * @throws NullPointerException when <code>start</code>, or <code>end</code> is <code>null</code>
101      * @throws IllegalArgumentException when <code>start</code> and <code>end</code> are at the same location
102      */
103     public BezierCubic2d(final Ray2d start, final Ray2d end)
104     {
105         this(start, end, 1.0);
106     }
107 
108     /**
109      * Construct a BezierCubic2d from <code>start</code> to <code>end</code> with two generated control points. The control
110      * points are in placed along the direction of the <code>start</code>, resp. <code>end</code> points at a distance that
111      * depends on the <code>shape</code> parameter.
112      * @param start the start point and start direction of the B&eacute;zier curve
113      * @param end the end point and end direction of the B&eacute;zier curve
114      * @param shape 1 = control points at half the distance between start and end, &gt; 1 results in a pointier shape, &lt; 1
115      *            results in a flatter shape, value should be above 0 and finite
116      * @throws NullPointerException when <code>start</code>, or <code>end</code> is <code>null</code>
117      * @throws IllegalArgumentException when <code>start</code> and <code>end</code> are at the same location,
118      *             <code>shape &le; 0</code>, <code>shape</code> is <code>NaN</code>, or infinite
119      */
120     public BezierCubic2d(final Ray2d start, final Ray2d end, final double shape)
121     {
122         this(start, end, shape, false);
123     }
124 
125     /**
126      * Construct a BezierCubic2d from start to end with two generated control points.
127      * @param start the start point and start direction of the B&eacute;zier curve
128      * @param end the end point and end direction of the B&eacute;zier curve
129      * @param shape the shape; higher values put the generated control points further away from end and result in a pointier
130      *            B&eacute;zier curve
131      * @param weighted whether weights will be applied
132      * @throws NullPointerException when <code>start</code>, or <code>end</code> is <code>null</code>
133      * @throws IllegalArgumentException when <code>start</code> and <code>end</code> are at the same location,
134      *             <code>shape &le; 0</code>, <code>shape</code> is <code>NaN</code>, or infinite
135      */
136     public BezierCubic2d(final Ray2d start, final Ray2d end, final double shape, final boolean weighted)
137     {
138         this(createControlPoints(start, end, shape, weighted));
139     }
140 
141     /** Unit vector for transformations in createControlPoints. */
142     private static final Point2d UNIT_VECTOR2D = new Point2d(1, 0);
143 
144     /**
145      * Create control points for a cubic B&eacute;zier curve defined by two Rays.
146      * @param start the start point (and direction)
147      * @param end the end point (and direction)
148      * @param shape the shape; higher values put the generated control points further away from end and result in a pointier
149      *            B&eacute;zier curve
150      * @param weighted whether weights will be applied
151      * @return an array of four Point2d elements: start, the first control point, the second control point, end.
152      * @throws NullPointerException when <code>start</code>, or <code>end</code> is <code>null</code>
153      * @throws IllegalArgumentException when <code>start</code> and <code>end</code> are at the same location,
154      *             <code>shape &le; 0</code>, <code>shape</code> is <code>NaN</code>, or infinite
155      */
156     private static Point2d[] createControlPoints(final Ray2d start, final Ray2d end, final double shape, final boolean weighted)
157     {
158         Throw.whenNull(start, "start");
159         Throw.whenNull(end, "end");
160         Throw.when(start.distanceSquared(end) == 0, IllegalArgumentException.class,
161                 "Cannot create control points if start and end points coincide");
162         Throw.whenNaN(shape, "shape");
163         Throw.when(shape <= 0 || Double.isInfinite(shape), IllegalArgumentException.class,
164                 "shape must be a finite, positive value");
165 
166         Point2d control1;
167         Point2d control2;
168         if (weighted)
169         {
170             // each control point is 'w' * the distance between the end-points away from the respective end point
171             // 'w' is a weight given by the distance from the end point to the extended line of the other end point
172             double distance = shape * start.distance(end);
173             double dStart = start.distance(end.projectOrthogonalExtended(start));
174             double dEnd = end.distance(start.projectOrthogonalExtended(end));
175             double wStart = dStart / (dStart + dEnd);
176             double wEnd = dEnd / (dStart + dEnd);
177             control1 = new Transform2d().translate(start).rotation(start.dirZ).scale(distance * wStart, distance * wStart)
178                     .transform(UNIT_VECTOR2D);
179             // - (minus) as the angle is where the line leaves, i.e. from shape point to end
180             control2 = new Transform2d().translate(end).rotation(end.dirZ + Math.PI).scale(distance * wEnd, distance * wEnd)
181                     .transform(UNIT_VECTOR2D);
182         }
183         else
184         {
185             // each control point is at shape times half the distance between the end-points away from the respective end point
186             double distance = shape * start.distance(end) / 2.0;
187             control1 = start.getLocation(distance);
188             // new Transform2d().translate(start).rotation(start.phi).scale(distance, distance).transform(UNIT_VECTOR2D);
189             control2 = end.getLocationExtended(-distance);
190             // new Transform2d().translate(end).rotation(end.phi + Math.PI).scale(distance, distance).transform(UNIT_VECTOR2D);
191         }
192         return new Point2d[] {start, control1, control2, end};
193     }
194 
195     @Override
196     public Point2d getPoint(final double t)
197     {
198         return new Point2d(B3(t, this.x), B3(t, this.y));
199     }
200 
201     /**
202      * 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>
203      * P<sub>1</sub> + 3t<sup>2</sup> (1 - t) P<sub>2</sub> + t<sup>3</sup> P<sub>3</sub>.
204      * @param t the fraction
205      * @param p the four control values of this dimension of this curve
206      * @return the cubic B&eacute;zier value B(t)
207      */
208     @SuppressWarnings("checkstyle:methodname")
209     private static double B3(final double t, final double[] p)
210     {
211         double t2 = t * t;
212         double t3 = t2 * t;
213         double m = (1.0 - t);
214         double m2 = m * m;
215         double m3 = m2 * m;
216         return m3 * p[0] + 3.0 * t * m2 * p[1] + 3.0 * t2 * m * p[2] + t3 * p[3];
217     }
218 
219     /**
220      * Start curvature of this BezierCubic2d. TODO looks very wrong
221      * @return start curvature of this BezierCubic2d
222      */
223     @Override
224     public double getStartCurvature()
225     {
226         return curvature(0.0);
227     }
228 
229     /**
230      * End curvature of this BezierCubic2d. TODO looks very wrong
231      * @return end curvature of this BezierCubic2d
232      */
233     @Override
234     public double getEndCurvature()
235     {
236         return curvature(1.0);
237     }
238 
239     /**
240      * Returns the root t values, where each of the sub-components derivative for x and y are 0.0.
241      * @return set of root t values, sorted and in the range (0, 1).
242      */
243     private SortedSet<Double> getRoots()
244     {
245         TreeSet<Double> roots = new TreeSet<>();
246         for (double[] dimension : new double[][] {this.x, this.y})
247         {
248             // Coefficients of the derivative in this dimension (for quadaratic B&eacure;zier)
249             double a = 3.0 * (-dimension[0] + 3.0 * dimension[1] - 3.0 * dimension[2] + dimension[3]);
250             double b = 6.0 * (dimension[0] - 2.0 * dimension[1] + dimension[2]);
251             double c = 3.0 * (dimension[1] - dimension[0]);
252             // ABC formula
253             double discriminant = b * b - 4.0 * a * c;
254             if (discriminant > 0)
255             {
256                 double sqrtDiscriminant = Math.sqrt(discriminant);
257                 double a2 = 2.0 * a;
258                 roots.add((-b + sqrtDiscriminant) / a2);
259                 roots.add((-b - sqrtDiscriminant) / a2);
260             }
261         }
262         // Only roots in range (0.0 ... 1.0) are valid and useful
263         return roots.subSet(0.0, false, 1.0, false);
264     }
265 
266     /**
267      * Returns the inflection t values; these are the points where curvature changes sign.
268      * @return set of inflection t values, sorted and in the range (0, 1)
269      */
270     private SortedSet<Double> getInflections()
271     {
272         // Align: translate so first point is (0, 0), rotate so last point is on x=axis (y = 0)
273         Point2d[] aligned = new Point2d[4];
274         double ang = -Math.atan2(getY(3) - getY(0), getX(3) - getX(0));
275         double cosAng = Math.cos(ang);
276         double sinAng = Math.sin(ang);
277         for (int i = 0; i < 4; i++)
278         {
279             aligned[i] = new Point2d(cosAng * (getX(i) - getX(0)) - sinAng * (getY(i) - getY(0)),
280                     sinAng * (getX(i) - getX(0)) + cosAng * (getY(i) - getY(0)));
281         }
282 
283         // Inflection as curvature = 0, using:
284         // curvature = x'(t)*y''(t) + y'(t)*x''(t) = 0
285         // (this is highly simplified due to the alignment, removing many terms)
286         double a = aligned[2].x * aligned[1].y;
287         double b = aligned[3].x * aligned[1].y;
288         double c = aligned[1].x * aligned[2].y;
289         double d = aligned[3].x * aligned[2].y;
290 
291         double x = -3.0 * a + 2.0 * b + 3.0 * c - d;
292         double y = 3.0 * a - b - 3.0 * c;
293         double z = c - a;
294 
295         // ABC formula (on x, y, z)
296         TreeSet<Double> inflections = new TreeSet<>();
297         if (Math.abs(x) < 1.0e-6)
298         {
299             if (Math.abs(y) >= 1.0e-12)
300             {
301                 inflections.add(-z / y);
302             }
303         }
304         else
305         {
306             double det = y * y - 4.0 * x * z;
307             double sq = Math.sqrt(det);
308             double d2 = 2 * x;
309             if (det >= 0.0 && Math.abs(d2) >= 1e-12)
310             {
311                 inflections.add(-(y + sq) / d2);
312                 inflections.add((sq - y) / d2);
313             }
314         }
315 
316         // Only inflections in range (0.0 ... 1.0) are valid and useful
317         return inflections.subSet(0.0, false, 1.0, false);
318     }
319 
320     /**
321      * Returns the offset t values.
322      * @param fractions length fractions at which offsets are defined.
323      * @return set of offset t values, sorted and only those in the range <code>(0, 1)</code>
324      */
325     private SortedSet<Double> getOffsetT(final Set<Double> fractions)
326     {
327         TreeSet<Double> result = new TreeSet<>();
328         for (Double f : fractions)
329         {
330             if (f > 0.0 && f < 1.0)
331             {
332                 result.add(getT(f * getLength()));
333             }
334         }
335         return result;
336     }
337 
338     /**
339      * Returns the offset t values.
340      * @param fractions yields the fractions at which offsets are defined.
341      * @return set of offset t values, sorted and only those in the range <code>(0, 1)</code>
342      */
343     private SortedSet<Double> getOffsetT(final Iterable<ContinuousPiecewiseLinearFunction.TupleSt> fractions)
344     {
345         TreeSet<Double> result = new TreeSet<>();
346         for (ContinuousPiecewiseLinearFunction.TupleSt tuple : fractions)
347         {
348             double f = tuple.s();
349             if (f > 0.0 && f < 1.0)
350             {
351                 result.add(getT(f * getLength()));
352             }
353         }
354         return result;
355     }
356 
357     /**
358      * Returns the t value at the provided length along the B&eacute;zier curve. This method uses an iterative approach with a
359      * precision of 1e-6.
360      * @param position position along the B&eacute;zier curve.
361      * @return t value at the provided length along the B&eacute;zier curve.
362      */
363     @Override
364     public double getT(final double position)
365     {
366         if (0.0 == position)
367         {
368             return 0.0;
369         }
370         if (this.length == position)
371         {
372             return 1.0;
373         }
374         // start at 0.0 and 1.0, cut in half, see which half to use next
375         double t0 = 0.0;
376         double t2 = 1.0;
377         double t1 = 0.5;
378         while (t2 > t0 + 1.0e-6)
379         {
380             t1 = (t2 + t0) / 2.0;
381             SplitBeziers parts = split(t1);
382             double len1 = parts.first.getLength();
383             if (len1 < position)
384             {
385                 t0 = t1;
386             }
387             else
388             {
389                 t2 = t1;
390             }
391         }
392         return t1;
393     }
394 
395     /**
396      * Wrapper for two BezierCubic2d objects.
397      * @param first the part before the split point
398      * @param remainder the part after the split point
399      */
400     private record SplitBeziers(BezierCubic2d first, BezierCubic2d remainder)
401     {
402     };
403 
404     /**
405      * Splits the B&eacute;zier in two B&eacute;zier curves of the same order.
406      * @param t t value along the B&eacute;zier curve to apply the split
407      * @return the B&eacute;zier curve before t, and the B&eacute;zier curve after t
408      * @throws IllegalArgumentException when <code>t &lt; 0.0</code>, or <code>t &gt; 1.0</code>
409      */
410     public SplitBeziers split(final double t)
411     {
412         Throw.when(t < 0.0 || t > 1.0, IllegalArgumentException.class, "t value should be in the range [0.0 ... 1.0].");
413         // System.out.println("Splitting at " + t + ": " + this);
414         List<Point2d> p1 = new ArrayList<>();
415         List<Point2d> p2 = new ArrayList<>();
416         List<Point2d> all = new ArrayList<>();
417         for (int i = 0; i < size(); i++)
418         {
419             all.add(new Point2d(getX(i), getY(i)));
420         }
421         split0(t, all, p1, p2);
422         SplitBeziers result = new SplitBeziers(new BezierCubic2d(p1.get(0), p1.get(1), p1.get(2), p1.get(3)),
423                 new BezierCubic2d(p2.get(3), p2.get(2), p2.get(1), p2.get(0)));
424         return result;
425     }
426 
427     /**
428      * Performs the iterative algorithm of Casteljau to derive the split B&eacute;zier curves.
429      * @param t t value along the B&eacute;zier to apply the split.
430      * @param p shape points of B&eacute;zier still to split.
431      * @param p1 shape points of first part, accumulated in the recursion.
432      * @param p2 shape points of second part, accumulated in the recursion.
433      */
434     private void split0(final double t, final List<Point2d> p, final List<Point2d> p1, final List<Point2d> p2)
435     {
436         if (p.size() == 1)
437         {
438             p1.add(p.get(0));
439             p2.add(p.get(0));
440         }
441         else
442         {
443             List<Point2d> pNew = new ArrayList<>();
444             for (int i = 0; i < p.size() - 1; i++)
445             {
446                 if (i == 0)
447                 {
448                     p1.add(p.get(i));
449                 }
450                 if (i == p.size() - 2)
451                 {
452                     p2.add(p.get(i + 1));
453                 }
454                 double t1 = 1.0 - t;
455                 pNew.add(new Point2d(t1 * p.get(i).x + t * p.get(i + 1).x, t1 * p.get(i).y + t * p.get(i + 1).y));
456             }
457             split0(t, pNew, p1, p2);
458         }
459     }
460 
461     /** The derivative B&eacute;zier used (and cashed) to compute the direction at some t value. */
462     private Bezier2d derivative = null;
463 
464     @Override
465     public PolyLine2d toPolyLine(final Flattener2d flattener)
466     {
467         Throw.whenNull(flattener, "Flattener may not be null");
468         if (null == this.derivative)
469         {
470             this.derivative = derivative();
471         }
472         return flattener.flatten(this);
473     }
474 
475     /**
476      * A B&eacute;zier curve does not have a trivial offset. Hence, we split the B&eacute;zier along points of 3 types. 1)
477      * roots, where the derivative of either the x-component or y-component is 0, such that we obtain C-shaped scalable
478      * segments, 2) inflections, where the curvature changes sign and the offset and offset angle need to flip sign, and 3)
479      * offset fractions so that the intended offset segments can be adhered to. Note that C-shaped segments can be scaled
480      * similar to a circle arc, whereas S-shaped segments have no trivial scaling and thus must be split.
481      */
482     private NavigableMap<Double, BezierCubic2d> segments;
483 
484     /** The offset data for which the (current) segments were created. */
485     private ContinuousPiecewiseLinearFunction ofForSegments = null;
486 
487     /**
488      * Check if the current segment map matches the provided offset data. If not; rebuild the segments to match.
489      * @param of offset function
490      */
491     private void updateSegments(final ContinuousPiecewiseLinearFunction of)
492     {
493         Throw.whenNull(of, "Offsets may not be null");
494         if (of.equals(this.ofForSegments))
495         {
496             return;
497         }
498         // Detect straight line
499         double ang1 = Math.atan2(getY(1) - getY(0), getX(1) - getX(0));
500         double ang2 = Math.atan2(getY(3) - getY(1), getX(3) - getX(1));
501         double ang3 = Math.atan2(getY(3) - getY(2), getX(3) - getX(2));
502         boolean straight =
503                 Math.abs(ang1 - ang2) < STRAIGHT && Math.abs(ang2 - ang3) < STRAIGHT && Math.abs(ang3 - ang1) < STRAIGHT;
504 
505         this.segments = new TreeMap<>();
506 
507         // Gather all points to split segments, and their types (Root, Inflection, or Kink in offsets)
508         NavigableMap<Double, Boundary> splits0 = new TreeMap<>(); // splits0 & splits because splits0 must be effectively final
509         if (!straight)
510         {
511             getRoots().forEach((t) -> splits0.put(t, Boundary.ROOT));
512             getInflections().forEach((t) -> splits0.put(t, Boundary.INFLECTION));
513         }
514         getOffsetT(of).forEach((t) -> splits0.put(t, Boundary.KNOT));
515         NavigableMap<Double, Boundary> splits = splits0.subMap(1e-6, false, 1.0 - 1e-6, false);
516 
517         // Initialize loop variables
518         // Work on a copy of the offset fractions, so we can remove each we use.
519         // Skip t == 0.0 while collecting the split points -on- this B&eacute;zier
520         NavigableSet<Double> fCrossSectionRemain = new TreeSet<>();
521         for (ContinuousPiecewiseLinearFunction.TupleSt tuple : of)
522         {
523             if (tuple.s() > 0.0)
524             {
525                 fCrossSectionRemain.add(tuple.s());
526             }
527         }
528         double lengthTotal = getLength();
529         BezierCubic2d currentBezier = this;
530         // System.out.println("Current bezier is " + this);
531         double lengthSoFar = 0.0;
532         // curvature and angle sign, flips at each inflection, start based on initial curve
533         double sig = Math.signum((getY(1) - getY(0)) * (getX(2) - getX(0)) - (getX(1) - getX(0)) * (getY(2) - getY(0)));
534 
535         Iterator<Double> typeIterator = splits.navigableKeySet().iterator();
536         double tStart = 0.0;
537         if (splits.isEmpty())
538         {
539             this.segments.put(tStart, currentBezier.offset(of, lengthSoFar, lengthTotal, sig, true));
540         }
541         while (typeIterator.hasNext())
542         {
543             double tInFull = typeIterator.next();
544             Boundary type = splits.get(tInFull);
545             double t;
546             // Note: as we split the B&eacute;zier curve and work with the remainder in each loop, the resulting t value is not
547             // the same as on the full B&eacute;zier curve. Therefore we need to refind the roots, or inflections, or at least
548             // one cross-section.
549             if (type == Boundary.ROOT)
550             {
551                 t = currentBezier.getRoots().first();
552             }
553             else if (type == Boundary.INFLECTION)
554             {
555                 t = currentBezier.getInflections().first();
556             }
557             else
558             {
559                 NavigableSet<Double> fCrossSection = new TreeSet<>();
560                 double fSoFar = lengthSoFar / lengthTotal;
561                 double fFirst = fCrossSectionRemain.pollFirst(); // fraction in total B&eacute;zier curve
562                 fCrossSection.add((fFirst - fSoFar) / (1.0 - fSoFar)); // add fraction in remaining B&eacute;zier curve
563                 SortedSet<Double> offsets = currentBezier.getOffsetT(fCrossSection);
564                 t = offsets.first();
565             }
566             if (t < 1e-10)
567             {
568                 continue;
569             }
570 
571             // Split B&eacute;zier curve, and add offset of first part
572             SplitBeziers parts = currentBezier.split(t);
573             BezierCubic2d segment = parts.first.offset(of, lengthSoFar, lengthTotal, sig, false);
574             // System.out.println("Offset segment at " + tStart + ": " + segment);
575             this.segments.put(tStart, segment);
576 
577             // Update loop variables
578             lengthSoFar += parts.first.getLength();
579             if (type == Boundary.INFLECTION)
580             {
581                 sig = -sig;
582             }
583             tStart = tInFull;
584 
585             // Append last segment, or loop again with remainder
586             if (!typeIterator.hasNext())
587             {
588                 BezierCubic2d lastSegment = parts.remainder.offset(of, lengthSoFar, lengthTotal, sig, true);
589                 // System.out.println("Offset last segment at " + tStart + ": " + lastSegment);
590                 this.segments.put(tStart, lastSegment);
591             }
592             else
593             {
594                 currentBezier = parts.remainder;
595             }
596         }
597         this.segments.put(1.0, null); // so we can interpolate t values along segments
598         this.ofForSegments = of;
599     }
600 
601     @Override
602     public Point2d getPoint(final double fraction, final ContinuousPiecewiseLinearFunction of)
603     {
604         updateSegments(of);
605         Entry<Double, BezierCubic2d> entry;
606         double nextT;
607         if (fraction == 1.0)
608         {
609             entry = BezierCubic2d.this.segments.lowerEntry(fraction);
610             nextT = fraction;
611         }
612         else
613         {
614             entry = BezierCubic2d.this.segments.floorEntry(fraction);
615             nextT = BezierCubic2d.this.segments.higherKey(fraction);
616         }
617         double t = (fraction - entry.getKey()) / (nextT - entry.getKey());
618         return entry.getValue().getPoint(t);
619     }
620 
621     @Override
622     public double getDirection(final double fraction, final ContinuousPiecewiseLinearFunction of)
623     {
624         updateSegments(of);
625         Entry<Double, BezierCubic2d> entry = BezierCubic2d.this.segments.floorEntry(fraction);
626         if (entry.getValue() == null)
627         {
628             // end of line
629             entry = BezierCubic2d.this.segments.lowerEntry(fraction);
630             Point2d derivativeBezier = entry.getValue().derivative().getPoint(1.0);
631             return Math.atan2(derivativeBezier.y, derivativeBezier.x);
632         }
633         Double nextT = BezierCubic2d.this.segments.higherKey(fraction);
634         if (nextT == null)
635         {
636             nextT = 1.0;
637         }
638         double t = (fraction - entry.getKey()) / (nextT - entry.getKey());
639         Point2d derivativeBezier = entry.getValue().derivative().getPoint(t);
640         return Math.atan2(derivativeBezier.y, derivativeBezier.x);
641     }
642 
643     @Override
644     public PolyLine2d toPolyLine(final OffsetFlattener2d flattener, final ContinuousPiecewiseLinearFunction of)
645     {
646         Throw.whenNull(of, "fld");
647         Throw.whenNull(flattener, "flattener");
648 
649         return flattener.flatten(this, of);
650     }
651 
652     /**
653      * Creates the offset B&eacute;zier curve of a B&eacute;zier segment. These segments are part of the offset procedure.
654      * @param offsets offsets as defined for the entire B&eacute;zier
655      * @param lengthSoFar cumulative length of all previously split off segments
656      * @param lengthTotal length of full B&eacute;zier
657      * @param sig sign of offset and offset slope
658      * @param last must be <code>true</code> for the last B&eacute;zier segment; <code>false</code> for all other segments
659      * @return offset B&eacute;zier
660      */
661     private BezierCubic2d offset(final ContinuousPiecewiseLinearFunction offsets, final double lengthSoFar,
662             final double lengthTotal, final double sig, final boolean last)
663     {
664         double offsetStart = sig * offsets.get(lengthSoFar / lengthTotal);
665         double offsetEnd = sig * offsets.get((lengthSoFar + getLength()) / lengthTotal);
666 
667         Point2d p1 = new Point2d(getX(0) - (getY(1) - getY(0)), getY(0) + (getX(1) - getX(0)));
668         Point2d p2 = new Point2d(getX(3) - (getY(2) - getY(3)), getY(3) + (getX(2) - getX(3)));
669         Point2d center = Point2d.intersectionOfLines(new Point2d(getX(0), getY(0)), p1, p2, new Point2d(getX(3), getY(3)));
670 
671         if (center == null)
672         {
673             // Start and end have same direction, offset first two by same amount, and last two by same amount to maintain
674             // directions
675             Point2d[] newBezierPoints = new Point2d[4];
676             double ang = Math.atan2(p1.y - getY(0), p1.x - getX(0));
677             double dxStart = Math.cos(ang) * offsetStart;
678             double dyStart = -Math.sin(ang) * offsetStart;
679             newBezierPoints[0] = new Point2d(getX(0) + dxStart, getY(0) + dyStart);
680             newBezierPoints[1] = new Point2d(getX(1) + dxStart, getY(1) + dyStart);
681             double dxEnd = Math.cos(ang) * offsetEnd;
682             double dyEnd = -Math.sin(ang) * offsetEnd;
683             newBezierPoints[2] = new Point2d(getX(2) + dxEnd, getY(2) + dyEnd);
684             newBezierPoints[3] = new Point2d(getX(3) + dxEnd, getY(3) + dyEnd);
685             return new BezierCubic2d(newBezierPoints[0], newBezierPoints[1], newBezierPoints[2], newBezierPoints[3]);
686         }
687 
688         // move 1st and 4th point their respective offsets away from the center
689         Point2d[] newBezierPoints = new Point2d[4];
690         double off = offsetStart;
691         for (int i = 0; i < 4; i = i + 3)
692         {
693             double dy = getY(i) - center.y;
694             double dx = getX(i) - center.x;
695             double ang = Math.atan2(dy, dx);
696             double len = Math.hypot(dx, dy) + off;
697             newBezierPoints[i] = new Point2d(center.x + len * Math.cos(ang), center.y + len * Math.sin(ang));
698             off = offsetEnd;
699         }
700 
701         // find tangent unit vectors that account for slope in offset
702         double ang = sig * Math.atan((offsetEnd - offsetStart) / getLength());
703         double cosAng = Math.cos(ang);
704         double sinAng = Math.sin(ang);
705         double dx = getX(1) - getX(0);
706         double dy = getY(1) - getY(0);
707         double dx1;
708         double dy1;
709         if (0.0 == lengthSoFar)
710         {
711             // force same start angle
712             dx1 = dx;
713             dy1 = dy;
714         }
715         else
716         {
717             // shift angle by 'ang'
718             dx1 = cosAng * dx - sinAng * dy;
719             dy1 = sinAng * dx + cosAng * dy;
720         }
721         dx = getX(2) - getX(3);
722         dy = getY(2) - getY(3);
723         double dx2;
724         double dy2;
725         if (last)
726         {
727             // force same end angle
728             dx2 = dx;
729             dy2 = dy;
730         }
731         else
732         {
733             // shift angle by 'ang'
734             dx2 = cosAng * dx - sinAng * dy;
735             dy2 = sinAng * dx + cosAng * dy;
736         }
737 
738         // control points 2 and 3 as intersections between tangent unit vectors and line through center and original point 2 and
739         // 3 in original B&eacute;zier
740         Point2d cp2 = new Point2d(newBezierPoints[0].x + dx1, newBezierPoints[0].y + dy1);
741         newBezierPoints[1] = Point2d.intersectionOfLines(newBezierPoints[0], cp2, center, new Point2d(getX(1), getY(1)));
742         Point2d cp3 = new Point2d(newBezierPoints[3].x + dx2, newBezierPoints[3].y + dy2);
743         newBezierPoints[2] = Point2d.intersectionOfLines(newBezierPoints[3], cp3, center, new Point2d(getX(2), getY(2)));
744 
745         // create offset B&eacute;zier
746         return new BezierCubic2d(newBezierPoints[0], newBezierPoints[1], newBezierPoints[2], newBezierPoints[3]);
747     }
748 
749     @Override
750     public double getLength()
751     {
752         return this.length;
753     }
754 
755     @Override
756     public String toString()
757     {
758         return "BezierCubic2d [startPoint=" + this.startPoint + ", endPoint=" + this.endPoint + ", controlPoint1="
759                 + new Point2d(getX(1), getY(1)) + ", controlPoint2=" + new Point2d(getX(2), getY(2)) + ", length=" + this.length
760                 + ", startDirZ=" + getStartPoint().dirZ + " (" + getPoint(0).directionTo(getPoint(1)) + "), endir="
761                 + getEndPoint().dirZ + " (" + getPoint(2).directionTo(getPoint(3)) + ")]";
762     }
763 
764     /**
765      * The three types of discontinuities/boundaries of a B&eacute;zier curve.
766      */
767     private enum Boundary
768     {
769         /** Root of B&eacute;zier curve. */
770         ROOT,
771 
772         /** Inflection point of B&eacute;zier curve. */
773         INFLECTION,
774 
775         /** Knot in offsets. */
776         KNOT
777     }
778 
779 }