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