1######################################################################
2#                                                                    #
3#  Copyright 2009 Lucas Heitzmann Gabrielli.                         #
4#  This file is part of gdspy, distributed under the terms of the    #
5#  Boost Software License - Version 1.0.  See the accompanying       #
6#  LICENSE file or <http://www.boost.org/LICENSE_1_0.txt>            #
7#                                                                    #
8######################################################################
9
10from __future__ import division
11from __future__ import unicode_literals
12from __future__ import print_function
13from __future__ import absolute_import
14
15import sys
16
17if sys.version_info.major < 3:
18    from builtins import zip
19    from builtins import open
20    from builtins import int
21    from builtins import round
22    from builtins import range
23    from builtins import super
24
25    from future import standard_library
26
27    standard_library.install_aliases()
28else:
29    # Python 3 doesn't have basestring, as unicode is type string
30    # Python 2 doesn't equate unicode to string, but both are basestring
31    # Now isinstance(s, basestring) will be True for any python version
32    basestring = str
33
34import numpy
35from gdspy.path import _func_bezier, _hobby
36
37
38class Curve(object):
39    """
40    Generation of curves loosely based on SVG paths.
41
42    Short summary of available methods:
43
44    ====== =============================
45    Method Primitive
46    ====== =============================
47    L/l    Line segments
48    H/h    Horizontal line segments
49    V/v    Vertical line segments
50    C/c    Cubic Bezier curve
51    S/s    Smooth cubic Bezier curve
52    Q/q    Quadratic Bezier curve
53    T/t    Smooth quadratic Bezier curve
54    B/b    General degree Bezier curve
55    I/i    Smooth interpolating curve
56    arc    Elliptical arc
57    ====== =============================
58
59    The uppercase version of the methods considers that all coordinates
60    are absolute, whereas the lowercase considers that they are relative
61    to the current end point of the curve.
62
63    Parameters
64    ----------
65    x : number
66        X-coordinate of the starting point of the curve.  If this is a
67        complex number, the value of `y` is ignored and the starting
68        point becomes ``(x.real, x.imag)``.
69    y : number
70        Y-coordinate of the starting point of the curve.
71    tolerance : number
72        Tolerance used to calculate a polygonal approximation to the
73        curve.
74
75    Notes
76    -----
77
78    In all methods of this class that accept coordinate pairs, a single
79    complex number can be passed to be split into its real and imaginary
80    parts.
81    This feature can be useful in expressing coordinates in polar form.
82
83    All commands follow the SVG 2 specification, except for elliptical
84    arcs and smooth interpolating curves, which are inspired by the
85    Metapost syntax.
86
87    Examples
88    --------
89    >>> curve = gdspy.Curve(3, 4).H(1).q(0.5, 1, 2j).L(2 + 3j, 2, 2)
90    >>> pol = gdspy.Polygon(curve.get_points())
91    """
92
93    __slots__ = "points", "tol", "last_c", "last_q"
94
95    def __init__(self, x, y=0, tolerance=0.01):
96        self.last_c = self.last_q = None
97        self.tol = tolerance ** 2
98        if isinstance(x, complex):
99            self.points = [numpy.array((x.real, x.imag))]
100        else:
101            self.points = [numpy.array((x, y))]
102
103    def get_points(self):
104        """
105        Get the polygonal points that approximate this curve.
106
107        Returns
108        -------
109        out : Numpy array[N, 2]
110            Vertices of the polygon.
111        """
112        delta = (self.points[-1] - self.points[0]) ** 2
113        if delta[0] + delta[1] < self.tol:
114            return numpy.array(self.points[:-1])
115        return numpy.array(self.points)
116
117    def L(self, *xy):
118        """
119        Add straight line segments to the curve.
120
121        Parameters
122        ----------
123        xy : numbers
124            Endpoint coordinates of the line segments.
125
126        Returns
127        -------
128        out : `Curve`
129            This curve.
130        """
131        self.last_c = self.last_q = None
132        i = 0
133        while i < len(xy):
134            if isinstance(xy[i], complex):
135                self.points.append(numpy.array((xy[i].real, xy[i].imag)))
136                i += 1
137            else:
138                self.points.append(numpy.array((xy[i], xy[i + 1])))
139                i += 2
140        return self
141
142    def l(self, *xy):
143        """
144        Add straight line segments to the curve.
145
146        Parameters
147        ----------
148        xy : numbers
149            Endpoint coordinates of the line segments relative to the
150            current end point.
151
152        Returns
153        -------
154        out : `Curve`
155            This curve.
156        """
157        self.last_c = self.last_q = None
158        o = self.points[-1]
159        i = 0
160        while i < len(xy):
161            if isinstance(xy[i], complex):
162                self.points.append(o + numpy.array((xy[i].real, xy[i].imag)))
163                i += 1
164            else:
165                self.points.append(o + numpy.array((xy[i], xy[i + 1])))
166                i += 2
167        return self
168
169    def H(self, *x):
170        """
171        Add horizontal line segments to the curve.
172
173        Parameters
174        ----------
175        x : numbers
176            Endpoint x-coordinates of the line segments.
177
178        Returns
179        -------
180        out : `Curve`
181            This curve.
182        """
183        self.last_c = self.last_q = None
184        y0 = self.points[-1][1]
185        self.points.extend(numpy.array((xx, y0)) for xx in x)
186        return self
187
188    def h(self, *x):
189        """
190        Add horizontal line segments to the curve.
191
192        Parameters
193        ----------
194        x : numbers
195            Endpoint x-coordinates of the line segments relative to the
196            current end point.
197
198        Returns
199        -------
200        out : `Curve`
201            This curve.
202        """
203        self.last_c = self.last_q = None
204        x0, y0 = self.points[-1]
205        self.points.extend(numpy.array((x0 + xx, y0)) for xx in x)
206        return self
207
208    def V(self, *y):
209        """
210        Add vertical line segments to the curve.
211
212        Parameters
213        ----------
214        y : numbers
215            Endpoint y-coordinates of the line segments.
216
217        Returns
218        -------
219        out : `Curve`
220            This curve.
221        """
222        self.last_c = self.last_q = None
223        x0 = self.points[-1][0]
224        self.points.extend(numpy.array((x0, yy)) for yy in y)
225        return self
226
227    def v(self, *y):
228        """
229        Add vertical line segments to the curve.
230
231        Parameters
232        ----------
233        y : numbers
234            Endpoint y-coordinates of the line segments relative to the
235            current end point.
236
237        Returns
238        -------
239        out : `Curve`
240            This curve.
241        """
242        self.last_c = self.last_q = None
243        x0, y0 = self.points[-1]
244        self.points.extend(numpy.array((x0, y0 + yy)) for yy in y)
245        return self
246
247    def arc(self, radius, initial_angle, final_angle, rotation=0):
248        """
249        Add an elliptical arc to the curve.
250
251        Parameters
252        ----------
253        radius : number, array-like[2]
254            Arc radius.  An elliptical arc can be created by passing an
255            array with 2 radii.
256        initial_angle : number
257            Initial angle of the arc (in *radians*).
258        final_angle : number
259            Final angle of the arc (in *radians*).
260        rotation : number
261            Rotation of the axis of the ellipse.
262
263        Returns
264        -------
265        out : `Curve`
266            This curve.
267        """
268        self.last_c = self.last_q = None
269        if hasattr(radius, "__iter__"):
270            rx, ry = radius
271            radius = max(radius)
272        else:
273            rx = ry = radius
274        full_angle = abs(final_angle - initial_angle)
275        number_of_points = max(
276            3,
277            1
278            + int(0.5 * full_angle / numpy.arccos(1 - self.tol ** 0.5 / radius) + 0.5),
279        )
280        angles = numpy.linspace(
281            initial_angle - rotation, final_angle - rotation, number_of_points
282        )
283        pts = numpy.vstack((rx * numpy.cos(angles), ry * numpy.sin(angles))).T
284        if rotation != 0:
285            rot = numpy.empty_like(pts)
286            c = numpy.cos(rotation)
287            s = numpy.sin(rotation)
288            rot[:, 0] = pts[:, 0] * c - pts[:, 1] * s
289            rot[:, 1] = pts[:, 0] * s + pts[:, 1] * c
290        else:
291            rot = pts
292        pts = rot[1:] - rot[0] + self.points[-1]
293        self.points.extend(xy for xy in pts)
294        return self
295
296    def C(self, *xy):
297        """
298        Add cubic Bezier curves to the curve.
299
300        Parameters
301        ----------
302        xy : numbers
303            Coordinate pairs. Each set of 3 pairs are interpreted as
304            the control point at the beginning of the curve, the control
305            point at the end of the curve and the endpoint of the curve.
306
307        Returns
308        -------
309        out : `Curve`
310            This curve.
311        """
312        self.last_q = None
313        i = 0
314        while i < len(xy):
315            ctrl = numpy.empty((4, 2))
316            ctrl[0] = self.points[-1]
317            for j in range(1, 4):
318                if isinstance(xy[i], complex):
319                    ctrl[j, 0] = xy[i].real
320                    ctrl[j, 1] = xy[i].imag
321                    i += 1
322                else:
323                    ctrl[j, 0] = xy[i]
324                    ctrl[j, 1] = xy[i + 1]
325                    i += 2
326            f = _func_bezier(ctrl)
327            uu = [0, 0.2, 0.5, 0.8, 1]
328            fu = [f(u) for u in uu]
329            iu = 1
330            while iu < len(fu):
331                test_u = 0.5 * (uu[iu - 1] + uu[iu])
332                test_pt = f(test_u)
333                test_err = 0.5 * (fu[iu - 1] + fu[iu]) - test_pt
334                if test_err[0] ** 2 + test_err[1] ** 2 > self.tol:
335                    uu.insert(iu, test_u)
336                    fu.insert(iu, test_pt)
337                else:
338                    iu += 1
339            self.points.extend(xy for xy in fu[1:])
340        self.last_c = ctrl[2]
341        return self
342
343    def c(self, *xy):
344        """
345        Add cubic Bezier curves to the curve.
346
347        Parameters
348        ----------
349        xy : numbers
350            Coordinate pairs. Each set of 3 pairs are interpreted as
351            the control point at the beginning of the curve, the control
352            point at the end of the curve and the endpoint of the curve.
353            All coordinates are relative to the current end point.
354
355        Returns
356        -------
357        out : `Curve`
358            This curve.
359        """
360        self.last_q = None
361        x0, y0 = self.points[-1]
362        i = 0
363        while i < len(xy):
364            ctrl = numpy.empty((4, 2))
365            ctrl[0] = self.points[-1]
366            for j in range(1, 4):
367                if isinstance(xy[i], complex):
368                    ctrl[j, 0] = x0 + xy[i].real
369                    ctrl[j, 1] = y0 + xy[i].imag
370                    i += 1
371                else:
372                    ctrl[j, 0] = x0 + xy[i]
373                    ctrl[j, 1] = y0 + xy[i + 1]
374                    i += 2
375            f = _func_bezier(ctrl)
376            uu = [0, 0.2, 0.5, 0.8, 1]
377            fu = [f(u) for u in uu]
378            iu = 1
379            while iu < len(fu):
380                test_u = 0.5 * (uu[iu - 1] + uu[iu])
381                test_pt = f(test_u)
382                test_err = 0.5 * (fu[iu - 1] + fu[iu]) - test_pt
383                if test_err[0] ** 2 + test_err[1] ** 2 > self.tol:
384                    uu.insert(iu, test_u)
385                    fu.insert(iu, test_pt)
386                else:
387                    iu += 1
388            self.points.extend(xy for xy in fu[1:])
389        self.last_c = ctrl[2]
390        return self
391
392    def S(self, *xy):
393        """
394        Add smooth cubic Bezier curves to the curve.
395
396        Parameters
397        ----------
398        xy : numbers
399            Coordinate pairs. Each set of 2 pairs are interpreted as
400            the control point at the end of the curve and the endpoint
401            of the curve.  The control point at the beginning of the
402            curve is assumed to be the reflection of the control point
403            at the end of the last curve relative to the starting point
404            of the curve. If the previous curve is not a cubic Bezier,
405            the control point is coincident with the starting point.
406
407        Returns
408        -------
409        out : `Curve`
410            This curve.
411        """
412        self.last_q = None
413        if self.last_c is None:
414            self.last_c = self.points[-1]
415        i = 0
416        while i < len(xy):
417            ctrl = numpy.empty((4, 2))
418            ctrl[0] = self.points[-1]
419            ctrl[1] = 2 * ctrl[0] - self.last_c
420            for j in range(2, 4):
421                if isinstance(xy[i], complex):
422                    ctrl[j, 0] = xy[i].real
423                    ctrl[j, 1] = xy[i].imag
424                    i += 1
425                else:
426                    ctrl[j, 0] = xy[i]
427                    ctrl[j, 1] = xy[i + 1]
428                    i += 2
429            f = _func_bezier(ctrl)
430            uu = [0, 0.2, 0.5, 0.8, 1]
431            fu = [f(u) for u in uu]
432            iu = 1
433            while iu < len(fu):
434                test_u = 0.5 * (uu[iu - 1] + uu[iu])
435                test_pt = f(test_u)
436                test_err = 0.5 * (fu[iu - 1] + fu[iu]) - test_pt
437                if test_err[0] ** 2 + test_err[1] ** 2 > self.tol:
438                    uu.insert(iu, test_u)
439                    fu.insert(iu, test_pt)
440                else:
441                    iu += 1
442            self.points.extend(xy for xy in fu[1:])
443            self.last_c = ctrl[2]
444        return self
445
446    def s(self, *xy):
447        """
448        Add smooth cubic Bezier curves to the curve.
449
450        Parameters
451        ----------
452        xy : numbers
453            Coordinate pairs. Each set of 2 pairs are interpreted as
454            the control point at the end of the curve and the endpoint
455            of the curve.  The control point at the beginning of the
456            curve is assumed to be the reflection of the control point
457            at the end of the last curve relative to the starting point
458            of the curve. If the previous curve is not a cubic Bezier,
459            the control point is coincident with the starting point.
460            All coordinates are relative to the current end point.
461
462        Returns
463        -------
464        out : `Curve`
465            This curve.
466        """
467        self.last_q = None
468        if self.last_c is None:
469            self.last_c = self.points[-1]
470        x0, y0 = self.points[-1]
471        i = 0
472        while i < len(xy):
473            ctrl = numpy.empty((4, 2))
474            ctrl[0] = self.points[-1]
475            ctrl[1] = 2 * ctrl[0] - self.last_c
476            for j in range(2, 4):
477                if isinstance(xy[i], complex):
478                    ctrl[j, 0] = x0 + xy[i].real
479                    ctrl[j, 1] = y0 + xy[i].imag
480                    i += 1
481                else:
482                    ctrl[j, 0] = x0 + xy[i]
483                    ctrl[j, 1] = y0 + xy[i + 1]
484                    i += 2
485            f = _func_bezier(ctrl)
486            uu = [0, 0.2, 0.5, 0.8, 1]
487            fu = [f(u) for u in uu]
488            iu = 1
489            while iu < len(fu):
490                test_u = 0.5 * (uu[iu - 1] + uu[iu])
491                test_pt = f(test_u)
492                test_err = 0.5 * (fu[iu - 1] + fu[iu]) - test_pt
493                if test_err[0] ** 2 + test_err[1] ** 2 > self.tol:
494                    uu.insert(iu, test_u)
495                    fu.insert(iu, test_pt)
496                else:
497                    iu += 1
498            self.points.extend(xy for xy in fu[1:])
499            self.last_c = ctrl[2]
500        return self
501
502    def Q(self, *xy):
503        """
504        Add quadratic Bezier curves to the curve.
505
506        Parameters
507        ----------
508        xy : numbers
509            Coordinate pairs. Each set of 2 pairs are interpreted as
510            the control point and the endpoint of the curve.
511
512        Returns
513        -------
514        out : `Curve`
515            This curve.
516        """
517        self.last_c = None
518        i = 0
519        while i < len(xy):
520            ctrl = numpy.empty((3, 2))
521            ctrl[0] = self.points[-1]
522            for j in range(1, 3):
523                if isinstance(xy[i], complex):
524                    ctrl[j, 0] = xy[i].real
525                    ctrl[j, 1] = xy[i].imag
526                    i += 1
527                else:
528                    ctrl[j, 0] = xy[i]
529                    ctrl[j, 1] = xy[i + 1]
530                    i += 2
531            f = _func_bezier(ctrl)
532            uu = [0, 0.2, 0.5, 0.8, 1]
533            fu = [f(u) for u in uu]
534            iu = 1
535            while iu < len(fu):
536                test_u = 0.5 * (uu[iu - 1] + uu[iu])
537                test_pt = f(test_u)
538                test_err = 0.5 * (fu[iu - 1] + fu[iu]) - test_pt
539                if test_err[0] ** 2 + test_err[1] ** 2 > self.tol:
540                    uu.insert(iu, test_u)
541                    fu.insert(iu, test_pt)
542                else:
543                    iu += 1
544            self.points.extend(xy for xy in fu[1:])
545        self.last_q = ctrl[1]
546        return self
547
548    def q(self, *xy):
549        """
550        Add quadratic Bezier curves to the curve.
551
552        Parameters
553        ----------
554        xy : numbers
555            Coordinate pairs. Each set of 2 pairs are interpreted as
556            the control point and the endpoint of the curve.
557            All coordinates are relative to the current end point.
558
559        Returns
560        -------
561        out : `Curve`
562            This curve.
563        """
564        self.last_c = None
565        x0, y0 = self.points[-1]
566        i = 0
567        while i < len(xy):
568            ctrl = numpy.empty((3, 2))
569            ctrl[0] = self.points[-1]
570            for j in range(1, 3):
571                if isinstance(xy[i], complex):
572                    ctrl[j, 0] = x0 + xy[i].real
573                    ctrl[j, 1] = y0 + xy[i].imag
574                    i += 1
575                else:
576                    ctrl[j, 0] = x0 + xy[i]
577                    ctrl[j, 1] = y0 + xy[i + 1]
578                    i += 2
579            f = _func_bezier(ctrl)
580            uu = [0, 0.2, 0.5, 0.8, 1]
581            fu = [f(u) for u in uu]
582            iu = 1
583            while iu < len(fu):
584                test_u = 0.5 * (uu[iu - 1] + uu[iu])
585                test_pt = f(test_u)
586                test_err = 0.5 * (fu[iu - 1] + fu[iu]) - test_pt
587                if test_err[0] ** 2 + test_err[1] ** 2 > self.tol:
588                    uu.insert(iu, test_u)
589                    fu.insert(iu, test_pt)
590                else:
591                    iu += 1
592            self.points.extend(xy for xy in fu[1:])
593        self.last_q = ctrl[1]
594        return self
595
596    def T(self, *xy):
597        """
598        Add smooth quadratic Bezier curves to the curve.
599
600        Parameters
601        ----------
602        xy : numbers
603            Coordinates of the endpoints of the curves.  The control
604            point is assumed to be the reflection of the control point
605            of the last curve relative to the starting point of the
606            curve. If the previous curve is not a quadratic Bezier,
607            the control point is coincident with the starting point.
608
609        Returns
610        -------
611        out : `Curve`
612            This curve.
613        """
614        self.last_c = None
615        if self.last_q is None:
616            self.last_q = self.points[-1]
617        i = 0
618        while i < len(xy):
619            ctrl = numpy.empty((3, 2))
620            ctrl[0] = self.points[-1]
621            ctrl[1] = 2 * ctrl[0] - self.last_q
622            if isinstance(xy[i], complex):
623                ctrl[2, 0] = xy[i].real
624                ctrl[2, 1] = xy[i].imag
625                i += 1
626            else:
627                ctrl[2, 0] = xy[i]
628                ctrl[2, 1] = xy[i + 1]
629                i += 2
630            f = _func_bezier(ctrl)
631            uu = [0, 0.2, 0.5, 0.8, 1]
632            fu = [f(u) for u in uu]
633            iu = 1
634            while iu < len(fu):
635                test_u = 0.5 * (uu[iu - 1] + uu[iu])
636                test_pt = f(test_u)
637                test_err = 0.5 * (fu[iu - 1] + fu[iu]) - test_pt
638                if test_err[0] ** 2 + test_err[1] ** 2 > self.tol:
639                    uu.insert(iu, test_u)
640                    fu.insert(iu, test_pt)
641                else:
642                    iu += 1
643            self.points.extend(xy for xy in fu[1:])
644            self.last_q = ctrl[1]
645        return self
646
647    def t(self, *xy):
648        """
649        Add smooth quadratic Bezier curves to the curve.
650
651        Parameters
652        ----------
653        xy : numbers
654            Coordinates of the endpoints of the curves.  The control
655            point is assumed to be the reflection of the control point
656            of the last curve relative to the starting point of the
657            curve. If the previous curve is not a quadratic Bezier,
658            the control point is coincident with the starting point.
659            All coordinates are relative to the current end point.
660
661        Returns
662        -------
663        out : `Curve`
664            This curve.
665        """
666        self.last_c = None
667        if self.last_q is None:
668            self.last_q = self.points[-1]
669        x0, y0 = self.points[-1]
670        i = 0
671        while i < len(xy):
672            ctrl = numpy.empty((3, 2))
673            ctrl[0] = self.points[-1]
674            ctrl[1] = 2 * ctrl[0] - self.last_q
675            if isinstance(xy[i], complex):
676                ctrl[2, 0] = x0 + xy[i].real
677                ctrl[2, 1] = y0 + xy[i].imag
678                i += 1
679            else:
680                ctrl[2, 0] = x0 + xy[i]
681                ctrl[2, 1] = y0 + xy[i + 1]
682                i += 2
683            f = _func_bezier(ctrl)
684            uu = [0, 0.2, 0.5, 0.8, 1]
685            fu = [f(u) for u in uu]
686            iu = 1
687            while iu < len(fu):
688                test_u = 0.5 * (uu[iu - 1] + uu[iu])
689                test_pt = f(test_u)
690                test_err = 0.5 * (fu[iu - 1] + fu[iu]) - test_pt
691                if test_err[0] ** 2 + test_err[1] ** 2 > self.tol:
692                    uu.insert(iu, test_u)
693                    fu.insert(iu, test_pt)
694                else:
695                    iu += 1
696            self.points.extend(xy for xy in fu[1:])
697            self.last_q = ctrl[1]
698        return self
699
700    def B(self, *xy):
701        """
702        Add a general degree Bezier curve.
703
704        Parameters
705        ----------
706        xy : numbers
707            Coordinate pairs.  The last coordinate is the endpoint of
708            curve and all other are control points.
709
710        Returns
711        -------
712        out : `Curve`
713            This curve.
714        """
715        self.last_c = self.last_q = None
716        i = 0
717        ctrl = [self.points[-1]]
718        while i < len(xy):
719            if isinstance(xy[i], complex):
720                ctrl.append((xy[i].real, xy[i].imag))
721                i += 1
722            else:
723                ctrl.append((xy[i], xy[i + 1]))
724                i += 2
725        ctrl = numpy.array(ctrl)
726        f = _func_bezier(ctrl)
727        uu = numpy.linspace(-1, 1, ctrl.shape[0] + 1)
728        uu = list(0.5 * (1 + numpy.sign(uu) * numpy.abs(uu) ** 0.8))
729        fu = [f(u) for u in uu]
730        iu = 1
731        while iu < len(fu):
732            test_u = 0.5 * (uu[iu - 1] + uu[iu])
733            test_pt = f(test_u)
734            test_err = 0.5 * (fu[iu - 1] + fu[iu]) - test_pt
735            if test_err[0] ** 2 + test_err[1] ** 2 > self.tol:
736                uu.insert(iu, test_u)
737                fu.insert(iu, test_pt)
738            else:
739                iu += 1
740        self.points.extend(xy for xy in fu[1:])
741        return self
742
743    def b(self, *xy):
744        """
745        Add a general degree Bezier curve.
746
747        Parameters
748        ----------
749        xy : numbers
750            Coordinate pairs.  The last coordinate is the endpoint of
751            curve and all other are control points.  All coordinates are
752            relative to the current end point.
753
754        Returns
755        -------
756        out : `Curve`
757            This curve.
758        """
759        self.last_c = self.last_q = None
760        x0, y0 = self.points[-1]
761        i = 0
762        ctrl = [self.points[-1]]
763        while i < len(xy):
764            if isinstance(xy[i], complex):
765                ctrl.append((x0 + xy[i].real, y0 + xy[i].imag))
766                i += 1
767            else:
768                ctrl.append((x0 + xy[i], y0 + xy[i + 1]))
769                i += 2
770        ctrl = numpy.array(ctrl)
771        f = _func_bezier(ctrl)
772        uu = numpy.linspace(-1, 1, ctrl.shape[0] + 1)
773        uu = list(0.5 * (1 + numpy.sign(uu) * numpy.abs(uu) ** 0.8))
774        fu = [f(u) for u in uu]
775        iu = 1
776        while iu < len(fu):
777            test_u = 0.5 * (uu[iu - 1] + uu[iu])
778            test_pt = f(test_u)
779            test_err = 0.5 * (fu[iu - 1] + fu[iu]) - test_pt
780            if test_err[0] ** 2 + test_err[1] ** 2 > self.tol:
781                uu.insert(iu, test_u)
782                fu.insert(iu, test_pt)
783            else:
784                iu += 1
785        self.points.extend(xy for xy in fu[1:])
786        return self
787
788    def I(
789        self,
790        points,
791        angles=None,
792        curl_start=1,
793        curl_end=1,
794        t_in=1,
795        t_out=1,
796        cycle=False,
797    ):
798        """
799        Add a smooth interpolating curve through the given points.
800
801        Uses the Hobby algorithm [1]_ to calculate a smooth
802        interpolating curve made of cubic Bezier segments between each
803        pair of points.
804
805        Parameters
806        ----------
807        points : array-like[N][2]
808            Vertices in the interpolating curve.
809        angles : array-like[N + 1] or None
810            Tangent angles at each point (in *radians*).  Any angles
811            defined as None are automatically calculated.
812        curl_start : number
813            Ratio between the mock curvatures at the first point and at
814            its neighbor.  A value of 1 renders the first segment a good
815            approximation for a circular arc.  A value of 0 will better
816            approximate a straight segment.  It has no effect for closed
817            curves or when an angle is defined for the first point.
818        curl_end : number
819            Ratio between the mock curvatures at the last point and at
820            its neighbor.  It has no effect for closed curves or when an
821            angle is defined for the first point.
822        t_in : number or array-like[N + 1]
823            Tension parameter when arriving at each point.  One value
824            per point or a single value used for all points.
825        t_out : number or array-like[N + 1]
826            Tension parameter when leaving each point.  One value per
827            point or a single value used for all points.
828        cycle : bool
829            If True, calculates control points for a closed curve,
830            with an additional segment connecting the first and last
831            points.
832
833        Returns
834        -------
835        out : `Curve`
836            This curve.
837
838        Examples
839        --------
840        >>> c1 = gdspy.Curve(0, 1).I([(1, 1), (2, 1), (1, 0)])
841        >>> c2 = gdspy.Curve(0, 2).I([(1, 2), (2, 2), (1, 1)],
842        ...                          cycle=True)
843        >>> ps = gdspy.PolygonSet([c1.get_points(), c2.get_points()])
844
845        References
846        ----------
847        .. [1] Hobby, J.D.  *Discrete Comput. Geom.* (1986) 1: 123.
848           `DOI: 10.1007/BF02187690
849           <https://doi.org/10.1007/BF02187690>`_
850        """
851        pts = numpy.vstack((self.points[-1:], points))
852        cta, ctb = _hobby(pts, angles, curl_start, curl_end, t_in, t_out, cycle)
853        args = []
854        args.extend(
855            x
856            for i in range(pts.shape[0] - 1)
857            for x in [
858                cta[i, 0],
859                cta[i, 1],
860                ctb[i, 0],
861                ctb[i, 1],
862                pts[i + 1, 0],
863                pts[i + 1, 1],
864            ]
865        )
866        if cycle:
867            args.extend(
868                [cta[-1, 0], cta[-1, 1], ctb[-1, 0], ctb[-1, 1], pts[0, 0], pts[0, 1]]
869            )
870        return self.C(*args)
871
872    def i(
873        self,
874        points,
875        angles=None,
876        curl_start=1,
877        curl_end=1,
878        t_in=1,
879        t_out=1,
880        cycle=False,
881    ):
882        """
883        Add a smooth interpolating curve through the given points.
884
885        Uses the Hobby algorithm [1]_ to calculate a smooth
886        interpolating curve made of cubic Bezier segments between each
887        pair of points.
888
889        Parameters
890        ----------
891        points : array-like[N][2]
892            Vertices in the interpolating curve (relative to the current
893            endpoint).
894        angles : array-like[N + 1] or None
895            Tangent angles at each point (in *radians*).  Any angles
896            defined as None are automatically calculated.
897        curl_start : number
898            Ratio between the mock curvatures at the first point and at
899            its neighbor.  A value of 1 renders the first segment a good
900            approximation for a circular arc.  A value of 0 will better
901            approximate a straight segment.  It has no effect for closed
902            curves or when an angle is defined for the first point.
903        curl_end : number
904            Ratio between the mock curvatures at the last point and at
905            its neighbor.  It has no effect for closed curves or when an
906            angle is defined for the first point.
907        t_in : number or array-like[N + 1]
908            Tension parameter when arriving at each point.  One value
909            per point or a single value used for all points.
910        t_out : number or array-like[N + 1]
911            Tension parameter when leaving each point.  One value per
912            point or a single value used for all points.
913        cycle : bool
914            If True, calculates control points for a closed curve,
915            with an additional segment connecting the first and last
916            points.
917
918        Returns
919        -------
920        out : `Curve`
921            This curve.
922
923        Examples
924        --------
925        >>> c1 = gdspy.Curve(0, 1).i([(1, 0), (2, 0), (1, -1)])
926        >>> c2 = gdspy.Curve(0, 2).i([(1, 0), (2, 0), (1, -1)],
927        ...                          cycle=True)
928        >>> ps = gdspy.PolygonSet([c1.get_points(), c2.get_points()])
929
930        References
931        ----------
932        .. [1] Hobby, J.D.  *Discrete Comput. Geom.* (1986) 1: 123.
933           `DOI: 10.1007/BF02187690
934           <https://doi.org/10.1007/BF02187690>`_
935        """
936        pts = numpy.vstack((numpy.array(((0.0, 0.0),)), points)) + self.points[-1]
937        cta, ctb = _hobby(pts, angles, curl_start, curl_end, t_in, t_out, cycle)
938        args = []
939        args.extend(
940            x
941            for i in range(pts.shape[0] - 1)
942            for x in [
943                cta[i, 0],
944                cta[i, 1],
945                ctb[i, 0],
946                ctb[i, 1],
947                pts[i + 1, 0],
948                pts[i + 1, 1],
949            ]
950        )
951        if cycle:
952            args.extend(
953                [cta[-1, 0], cta[-1, 1], ctb[-1, 0], ctb[-1, 1], pts[0, 0], pts[0, 1]]
954            )
955        return self.C(*args)
956