src.acoustools.Paths.Bezier

  1import torch
  2from torch import Tensor
  3
  4import itertools, math
  5
  6try:
  7    from svgpathtools import svg2paths, Line
  8    from svgpathtools import CubicBezier as _CubicBezier
  9    svg_warning = False
 10except ImportError:
 11    svg_warning = True
 12
 13from acoustools.Utilities.Points import create_points
 14from acoustools.Paths.Interpolate import interpolate_path, interpolate_points
 15from acoustools.Paths.Distances import distance
 16
 17from acoustools.Paths.Curves import CubicBezier, Spline
 18
 19def interpolate_bezier(bezier:CubicBezier, n:int=100) -> list[Tensor]:
 20
 21    '''
 22    Create cubic Bezier curve based on positions given \n
 23    :param start: Start position
 24    :param end: End position
 25    :param offset_1: offset from start to first control point
 26    :param offset_2: offset from start to second control point
 27    :param n: number of samples
 28    :returns points:
 29    '''
 30
 31    #Make even sample?= distance
 32
 33    start,end,offset_1,offset_2 = bezier.get_data()
 34
 35    
 36    if type(offset_1) == list: offset_1 = torch.tensor(offset_1).reshape((1,3,1))
 37    if type(offset_1) == int: offset_1 = torch.ones_like(start) * offset_1
 38
 39    if type(offset_2) == list: offset_2 = torch.tensor(offset_2).reshape((1,3,1))
 40    if type(offset_2) == int: offset_2 = torch.ones_like(start) * offset_2
 41
 42
 43    P1 = start
 44    P2 = start + offset_1
 45    P3 = start + offset_2
 46    P4 = end
 47
 48    points = []
 49
 50    for i in range(n):
 51        t = i/n
 52        # THIS IS QUITE SLOW - REDUCE THIS TO ONE LINE - 
 53        P5 = (1-t)*P1 + t*P2
 54        P6 = (1-t)*P2 + t*P3
 55        P7 = (1-t)*P3 + t*P4
 56        P8 = (1-t)*P5 + t*P6
 57        P9 = (1-t)*P6 + t*P7
 58        point = (1-t)*P8 + t*P9
 59
 60        points.append(point)
 61
 62    return points
 63
 64def interpolate_bezier_velocity(bezier:CubicBezier, n:int=100) -> list[Tensor]:
 65    '''
 66    Gets the velocity of a  cubic Bezier curve based on positions given \n
 67    :param start: Start position
 68    :param end: End position
 69    :param offset_1: offset from start to first control point
 70    :param offset_2: offset from start to second control point
 71    :param n: number of samples
 72    :returns points:
 73    '''
 74
 75    start,end,offset_1,offset_2 = bezier.get_data()
 76    
 77    P0 = start
 78    P1 = start + offset_1
 79    P2 = start + offset_2
 80    P3 = end
 81
 82    points = []
 83    for i in range(n):
 84        t = i/n
 85        p = 3*(1-t)**2 *(P1-P0) + 6*(1-t)*t*(P2-P1) + 3*t**2 * (P3-P2)
 86        points.append(p)
 87    
 88    return points
 89
 90
 91def interpolate_bezier_acceleration(bezier:CubicBezier, n:int=100) -> list[Tensor]:
 92    '''
 93    Gets the acceleration of a  cubic Bezier curve based on positions given \n
 94    :param start: Start position
 95    :param end: End position
 96    :param offset_1: offset from start to first control point
 97    :param offset_2: offset from start to second control point
 98    :param n: number of samples
 99    :returns points:
100    '''
101
102    start,end,offset_1,offset_2 = bezier.get_data()
103     
104    P0 = start
105    P1 = start + offset_1
106    P2 = start + offset_2
107    P3 = end
108
109    points = []
110    for i in range(n):
111        t = i/n
112        p = 6*(1-t)*(P2-2*P1+P0) + 6*t*(P3-2*P2+P1)
113        points.append(p)
114    
115    return points
116
117
118def svg_to_beziers(pth:str, flip_y:bool= False, n:int=20, dx:float=0, dy:float=0, scale_x:float = 1/10, scale_y:float = 1/10) -> tuple[list[Tensor], Spline]:
119    '''
120    Converts a .SVG file containing bezier curves to a set of AcousTools bezier curves \n
121    :param pth: String path to .svg file
122    :param flip_y: If true flip the y axis
123    :param n: Number of samples along bezier to return
124    :param dx: change in x direction to apply
125    :param dy: change in y direction to apply
126    :param scale_x: scale in x direction to apply
127    :param scale_y: scale in y direction to apply
128    :returns (points, bezier): Points and the bezier curve as list of tuples. Bezier defined as (start, end, offset1, offset2) where offsets are from start
129    '''
130    if svg_warning:
131        raise ImportError('Requires svgpathtools module `pip install svgpathtools`')
132        
133
134    paths, _ = svg2paths(pth)
135
136
137    def ReIm_to_AcousTools_point(point, flip_y, dx, dy, scale_x, scale_y):
138        if flip_y:
139            y_mul = -1
140        else:
141            y_mul = 1
142
143        point_AT = create_points(1,x=(point.real*scale_x) + dx, y=(y_mul*point.imag*scale_y)-dy,z=0)
144        return point_AT
145
146    points = []
147    control_points = []
148    i = -1
149
150    for pth in paths:
151        for bez in pth:
152            if type(bez) == _CubicBezier:
153                i += 1
154
155                start_RI = bez.start
156                control_1_RI = bez.control1
157                control_2_RI = bez.control2
158                end_RI = bez.end
159
160                start = ReIm_to_AcousTools_point(start_RI, flip_y, dx, dy, scale_x , scale_y)
161                control1 = ReIm_to_AcousTools_point(control_1_RI, flip_y,dx, dy, scale_x , scale_y)
162                control2 = ReIm_to_AcousTools_point(control_2_RI, flip_y,dx, dy, scale_x , scale_y)
163                end = ReIm_to_AcousTools_point(end_RI, flip_y,dx, dy, scale_x , scale_y)
164                
165                b = CubicBezier(start, end, control1-start, control2-start)
166                control_points.append(b)
167
168                points += interpolate_bezier(b, n=n)
169
170            elif type(bez) == Line:
171                start_RI = bez.start
172                end_RI = bez.end
173                
174                start = ReIm_to_AcousTools_point(start_RI, flip_y)
175                end = ReIm_to_AcousTools_point(end_RI, flip_y)
176                points += interpolate_path([start, end],n=n)
177    
178
179    # xs = [p[:,0] for p in points]
180    # max_x = max(xs).clone()
181    # min_x = min(xs).clone()
182
183    # ys = [p[:,1] for p in points]
184    # max_y = max(ys).clone()
185    # min_y = min(ys).clone()
186   
187
188    return points, Spline(control_points)
189
190def bezier_to_C1(spline:Spline, check_C0:bool=True, n:int=20, get_points=True) -> tuple[list[Tensor]]:
191    '''
192    Converts a spline of beziers to be C1 continuous (https://en.wikipedia.org/wiki/Composite_B%C3%A9zier_curve#Smooth_joining)
193    :param spline: spline of curves to convert  
194    :param check_C0: If True will encure C0 continuity as well. Raises an error if violated
195    :param n: number of samples
196    :returns points,new_bezier: Points and new C1 spline curve
197    '''
198    # new_bezier = CubicBezier(None,None,None,None)
199    # new_bezier.start = bezier[0]
200    # new_bezier.append(bezier[0])
201
202    new_spline = Spline()
203    new_spline.add_curve(spline[0])
204
205    for i,(b1,b2) in enumerate(itertools.pairwise(spline)):
206        P0, P3, c11, c12 = b1.get_data()
207        start_2,P6, c21, c22 = b2.get_data()
208        P1 = P0 + c11
209        P2 = P0 + c12
210        P5 = P3 + c22
211 
212        # if check_C0: assert (P3 == start_2).all() #Assert we have C0 continuity
213
214        P4_offset = (P3 - P2)
215
216        new_spline.add_curve(CubicBezier(P3, P6, P4_offset, c22))
217
218    if (new_spline[0][0] == new_spline[-1][1]).all(): #C0 continuous at the last point -> Path is a loop
219        [P0, P3, c11, c12] = new_spline[-1].get_data()
220        [start_2,P6, c21, c22 ] = new_spline[0].get_data()
221
222        P1 = P0 + c11
223        P2 = P0 + c12
224        P5 = P3 + c22
225 
226        # if check_C0: assert (P3 == start_2).all() #Assert we have C0 continuity
227
228        P4_offset = (P3 - P2)
229
230        new_spline[0] = CubicBezier(P3, P6, P4_offset, c22)
231
232
233    if get_points:
234        points =[]
235        for bez in new_spline:
236            points += interpolate_bezier(bez, n)
237    
238    
239        return points,new_spline
240
241    return new_spline
242
243def close_bezier(spline:Spline, n:int=20)  -> tuple[list[Tensor]]:
244    '''
245    Links the last point in a bezier to the start of it with a new bezier \n
246    :param bezier: Bezier spline to close as list of (start, end, offset1, offset2) where offsets are from start 
247    :param n: number of points to sample
248    :returns points,bezier: points,bezier
249    '''
250
251    start = spline[0]
252    end = spline[-1]
253
254    new_b = [end[1], start[0],torch.zeros_like(start[0]),torch.zeros_like(start[0])]
255    spline.add_curve(CubicBezier(*new_b))
256
257    if n != 0:
258        points =[]
259        for bez in spline:
260            points += interpolate_bezier(bez, n)
261        return points,spline
262    return spline
263
264def bezier_to_distance(bezier:CubicBezier, max_distance:float=0.001, start_n=20):
265    '''
266    Samples bezier to have at most max_distance between points. \n
267    :param bezier:`acoustools.Paths.Curves.Bezier` object
268    :param max_distance: maximum straight line distance between points
269    :param start_n: number of points to start
270    :returns points:
271    '''
272    bezier_points = interpolate_bezier(bezier,n=start_n)
273
274    points = []
275    
276    for i,(p1,p2) in enumerate(itertools.pairwise(bezier_points)):
277        
278        n = int(torch.ceil(torch.max(distance(p1, p2) / max_distance)).item())
279        points += interpolate_points(p1,p2, n)
280    
281    return points
282
283def create_bezier_circle(N=10, origin=(0,0,0), radius=0.01, plane='xy'):
284    angle = 3.14*2 / N
285    origin = create_points(1,1,origin[0], origin[1],origin[2])
286    beziers = []
287    for i in range(N):
288        pos_1 = radius * math.sin(angle*i) 
289        pos_2 = radius * math.cos(angle*i)
290
291        if plane == 'xy':
292            start = create_points(1,1,pos_1,pos_2,0) + origin
293        elif plane == 'xz':
294            start = create_points(1,1,pos_1,0,pos_2) + origin
295        elif plane == 'yz':
296            start = create_points(1,1,0,pos_1,pos_2) + origin
297        else:
298            raise ValueError("Plane not valid. Must be xy, xz or yz")
299        
300        pos_3 = radius * math.sin(angle*(i+1)) 
301        pos_4 = radius * math.cos(angle*(i+1))
302
303        if plane == 'xy':
304            end = create_points(1,1,pos_3,pos_4,0) + origin
305        elif plane == 'xz':
306            end = create_points(1,1,pos_3,0,pos_4) + origin
307        elif plane == 'yz':
308            end = create_points(1,1,0,pos_3,pos_4) + origin
309        else:
310            raise ValueError("Plane not valid. Must be xy, xz or yz")
311
312        offset_1 = create_points(1,1,0,0,0)
313
314        bez = CubicBezier(start,end,offset_1, offset_1.clone())
315        beziers.append(bez)
316    spline = Spline(beziers)
317
318    return spline
319
320
321def connect_ends(spline:Spline):
322    '''
323    Sets the end of the last curve in spline to be the start of the first
324    '''
325    start = spline[0]
326    end = spline[-1]
327
328    end.end = start.start
def interpolate_bezier( bezier: acoustools.Paths.Curves.CubicBezier, n: int = 100) -> list[torch.Tensor]:
20def interpolate_bezier(bezier:CubicBezier, n:int=100) -> list[Tensor]:
21
22    '''
23    Create cubic Bezier curve based on positions given \n
24    :param start: Start position
25    :param end: End position
26    :param offset_1: offset from start to first control point
27    :param offset_2: offset from start to second control point
28    :param n: number of samples
29    :returns points:
30    '''
31
32    #Make even sample?= distance
33
34    start,end,offset_1,offset_2 = bezier.get_data()
35
36    
37    if type(offset_1) == list: offset_1 = torch.tensor(offset_1).reshape((1,3,1))
38    if type(offset_1) == int: offset_1 = torch.ones_like(start) * offset_1
39
40    if type(offset_2) == list: offset_2 = torch.tensor(offset_2).reshape((1,3,1))
41    if type(offset_2) == int: offset_2 = torch.ones_like(start) * offset_2
42
43
44    P1 = start
45    P2 = start + offset_1
46    P3 = start + offset_2
47    P4 = end
48
49    points = []
50
51    for i in range(n):
52        t = i/n
53        # THIS IS QUITE SLOW - REDUCE THIS TO ONE LINE - 
54        P5 = (1-t)*P1 + t*P2
55        P6 = (1-t)*P2 + t*P3
56        P7 = (1-t)*P3 + t*P4
57        P8 = (1-t)*P5 + t*P6
58        P9 = (1-t)*P6 + t*P7
59        point = (1-t)*P8 + t*P9
60
61        points.append(point)
62
63    return points

Create cubic Bezier curve based on positions given

Parameters
  • start: Start position
  • end: End position
  • offset_1: offset from start to first control point
  • offset_2: offset from start to second control point
  • n: number of samples :returns points:
def interpolate_bezier_velocity( bezier: acoustools.Paths.Curves.CubicBezier, n: int = 100) -> list[torch.Tensor]:
65def interpolate_bezier_velocity(bezier:CubicBezier, n:int=100) -> list[Tensor]:
66    '''
67    Gets the velocity of a  cubic Bezier curve based on positions given \n
68    :param start: Start position
69    :param end: End position
70    :param offset_1: offset from start to first control point
71    :param offset_2: offset from start to second control point
72    :param n: number of samples
73    :returns points:
74    '''
75
76    start,end,offset_1,offset_2 = bezier.get_data()
77    
78    P0 = start
79    P1 = start + offset_1
80    P2 = start + offset_2
81    P3 = end
82
83    points = []
84    for i in range(n):
85        t = i/n
86        p = 3*(1-t)**2 *(P1-P0) + 6*(1-t)*t*(P2-P1) + 3*t**2 * (P3-P2)
87        points.append(p)
88    
89    return points

Gets the velocity of a cubic Bezier curve based on positions given

Parameters
  • start: Start position
  • end: End position
  • offset_1: offset from start to first control point
  • offset_2: offset from start to second control point
  • n: number of samples :returns points:
def interpolate_bezier_acceleration( bezier: acoustools.Paths.Curves.CubicBezier, n: int = 100) -> list[torch.Tensor]:
 92def interpolate_bezier_acceleration(bezier:CubicBezier, n:int=100) -> list[Tensor]:
 93    '''
 94    Gets the acceleration of a  cubic Bezier curve based on positions given \n
 95    :param start: Start position
 96    :param end: End position
 97    :param offset_1: offset from start to first control point
 98    :param offset_2: offset from start to second control point
 99    :param n: number of samples
100    :returns points:
101    '''
102
103    start,end,offset_1,offset_2 = bezier.get_data()
104     
105    P0 = start
106    P1 = start + offset_1
107    P2 = start + offset_2
108    P3 = end
109
110    points = []
111    for i in range(n):
112        t = i/n
113        p = 6*(1-t)*(P2-2*P1+P0) + 6*t*(P3-2*P2+P1)
114        points.append(p)
115    
116    return points

Gets the acceleration of a cubic Bezier curve based on positions given

Parameters
  • start: Start position
  • end: End position
  • offset_1: offset from start to first control point
  • offset_2: offset from start to second control point
  • n: number of samples :returns points:
def svg_to_beziers( pth: str, flip_y: bool = False, n: int = 20, dx: float = 0, dy: float = 0, scale_x: float = 0.1, scale_y: float = 0.1) -> tuple[list[torch.Tensor], acoustools.Paths.Curves.Spline]:
119def svg_to_beziers(pth:str, flip_y:bool= False, n:int=20, dx:float=0, dy:float=0, scale_x:float = 1/10, scale_y:float = 1/10) -> tuple[list[Tensor], Spline]:
120    '''
121    Converts a .SVG file containing bezier curves to a set of AcousTools bezier curves \n
122    :param pth: String path to .svg file
123    :param flip_y: If true flip the y axis
124    :param n: Number of samples along bezier to return
125    :param dx: change in x direction to apply
126    :param dy: change in y direction to apply
127    :param scale_x: scale in x direction to apply
128    :param scale_y: scale in y direction to apply
129    :returns (points, bezier): Points and the bezier curve as list of tuples. Bezier defined as (start, end, offset1, offset2) where offsets are from start
130    '''
131    if svg_warning:
132        raise ImportError('Requires svgpathtools module `pip install svgpathtools`')
133        
134
135    paths, _ = svg2paths(pth)
136
137
138    def ReIm_to_AcousTools_point(point, flip_y, dx, dy, scale_x, scale_y):
139        if flip_y:
140            y_mul = -1
141        else:
142            y_mul = 1
143
144        point_AT = create_points(1,x=(point.real*scale_x) + dx, y=(y_mul*point.imag*scale_y)-dy,z=0)
145        return point_AT
146
147    points = []
148    control_points = []
149    i = -1
150
151    for pth in paths:
152        for bez in pth:
153            if type(bez) == _CubicBezier:
154                i += 1
155
156                start_RI = bez.start
157                control_1_RI = bez.control1
158                control_2_RI = bez.control2
159                end_RI = bez.end
160
161                start = ReIm_to_AcousTools_point(start_RI, flip_y, dx, dy, scale_x , scale_y)
162                control1 = ReIm_to_AcousTools_point(control_1_RI, flip_y,dx, dy, scale_x , scale_y)
163                control2 = ReIm_to_AcousTools_point(control_2_RI, flip_y,dx, dy, scale_x , scale_y)
164                end = ReIm_to_AcousTools_point(end_RI, flip_y,dx, dy, scale_x , scale_y)
165                
166                b = CubicBezier(start, end, control1-start, control2-start)
167                control_points.append(b)
168
169                points += interpolate_bezier(b, n=n)
170
171            elif type(bez) == Line:
172                start_RI = bez.start
173                end_RI = bez.end
174                
175                start = ReIm_to_AcousTools_point(start_RI, flip_y)
176                end = ReIm_to_AcousTools_point(end_RI, flip_y)
177                points += interpolate_path([start, end],n=n)
178    
179
180    # xs = [p[:,0] for p in points]
181    # max_x = max(xs).clone()
182    # min_x = min(xs).clone()
183
184    # ys = [p[:,1] for p in points]
185    # max_y = max(ys).clone()
186    # min_y = min(ys).clone()
187   
188
189    return points, Spline(control_points)

Converts a .SVG file containing bezier curves to a set of AcousTools bezier curves

Parameters
  • pth: String path to .svg file
  • flip_y: If true flip the y axis
  • n: Number of samples along bezier to return
  • dx: change in x direction to apply
  • dy: change in y direction to apply
  • scale_x: scale in x direction to apply
  • scale_y: scale in y direction to apply :returns (points, bezier): Points and the bezier curve as list of tuples. Bezier defined as (start, end, offset1, offset2) where offsets are from start
def bezier_to_C1( spline: acoustools.Paths.Curves.Spline, check_C0: bool = True, n: int = 20, get_points=True) -> tuple[list[torch.Tensor]]:
191def bezier_to_C1(spline:Spline, check_C0:bool=True, n:int=20, get_points=True) -> tuple[list[Tensor]]:
192    '''
193    Converts a spline of beziers to be C1 continuous (https://en.wikipedia.org/wiki/Composite_B%C3%A9zier_curve#Smooth_joining)
194    :param spline: spline of curves to convert  
195    :param check_C0: If True will encure C0 continuity as well. Raises an error if violated
196    :param n: number of samples
197    :returns points,new_bezier: Points and new C1 spline curve
198    '''
199    # new_bezier = CubicBezier(None,None,None,None)
200    # new_bezier.start = bezier[0]
201    # new_bezier.append(bezier[0])
202
203    new_spline = Spline()
204    new_spline.add_curve(spline[0])
205
206    for i,(b1,b2) in enumerate(itertools.pairwise(spline)):
207        P0, P3, c11, c12 = b1.get_data()
208        start_2,P6, c21, c22 = b2.get_data()
209        P1 = P0 + c11
210        P2 = P0 + c12
211        P5 = P3 + c22
212 
213        # if check_C0: assert (P3 == start_2).all() #Assert we have C0 continuity
214
215        P4_offset = (P3 - P2)
216
217        new_spline.add_curve(CubicBezier(P3, P6, P4_offset, c22))
218
219    if (new_spline[0][0] == new_spline[-1][1]).all(): #C0 continuous at the last point -> Path is a loop
220        [P0, P3, c11, c12] = new_spline[-1].get_data()
221        [start_2,P6, c21, c22 ] = new_spline[0].get_data()
222
223        P1 = P0 + c11
224        P2 = P0 + c12
225        P5 = P3 + c22
226 
227        # if check_C0: assert (P3 == start_2).all() #Assert we have C0 continuity
228
229        P4_offset = (P3 - P2)
230
231        new_spline[0] = CubicBezier(P3, P6, P4_offset, c22)
232
233
234    if get_points:
235        points =[]
236        for bez in new_spline:
237            points += interpolate_bezier(bez, n)
238    
239    
240        return points,new_spline
241
242    return new_spline

Converts a spline of beziers to be C1 continuous (https://en.wikipedia.org/wiki/Composite_B%C3%A9zier_curve#Smooth_joining)

Parameters
  • spline: spline of curves to convert
  • check_C0: If True will encure C0 continuity as well. Raises an error if violated
  • n: number of samples :returns points,new_bezier: Points and new C1 spline curve
def close_bezier( spline: acoustools.Paths.Curves.Spline, n: int = 20) -> tuple[list[torch.Tensor]]:
244def close_bezier(spline:Spline, n:int=20)  -> tuple[list[Tensor]]:
245    '''
246    Links the last point in a bezier to the start of it with a new bezier \n
247    :param bezier: Bezier spline to close as list of (start, end, offset1, offset2) where offsets are from start 
248    :param n: number of points to sample
249    :returns points,bezier: points,bezier
250    '''
251
252    start = spline[0]
253    end = spline[-1]
254
255    new_b = [end[1], start[0],torch.zeros_like(start[0]),torch.zeros_like(start[0])]
256    spline.add_curve(CubicBezier(*new_b))
257
258    if n != 0:
259        points =[]
260        for bez in spline:
261            points += interpolate_bezier(bez, n)
262        return points,spline
263    return spline

Links the last point in a bezier to the start of it with a new bezier

Parameters
  • bezier: Bezier spline to close as list of (start, end, offset1, offset2) where offsets are from start
  • n: number of points to sample :returns points,bezier: points,bezier
def bezier_to_distance( bezier: acoustools.Paths.Curves.CubicBezier, max_distance: float = 0.001, start_n=20):
265def bezier_to_distance(bezier:CubicBezier, max_distance:float=0.001, start_n=20):
266    '''
267    Samples bezier to have at most max_distance between points. \n
268    :param bezier:`acoustools.Paths.Curves.Bezier` object
269    :param max_distance: maximum straight line distance between points
270    :param start_n: number of points to start
271    :returns points:
272    '''
273    bezier_points = interpolate_bezier(bezier,n=start_n)
274
275    points = []
276    
277    for i,(p1,p2) in enumerate(itertools.pairwise(bezier_points)):
278        
279        n = int(torch.ceil(torch.max(distance(p1, p2) / max_distance)).item())
280        points += interpolate_points(p1,p2, n)
281    
282    return points

Samples bezier to have at most max_distance between points.

Parameters
  • bezier: acoustools.Paths.Curves.Bezier object
  • max_distance: maximum straight line distance between points
  • start_n: number of points to start :returns points:
def create_bezier_circle(N=10, origin=(0, 0, 0), radius=0.01, plane='xy'):
284def create_bezier_circle(N=10, origin=(0,0,0), radius=0.01, plane='xy'):
285    angle = 3.14*2 / N
286    origin = create_points(1,1,origin[0], origin[1],origin[2])
287    beziers = []
288    for i in range(N):
289        pos_1 = radius * math.sin(angle*i) 
290        pos_2 = radius * math.cos(angle*i)
291
292        if plane == 'xy':
293            start = create_points(1,1,pos_1,pos_2,0) + origin
294        elif plane == 'xz':
295            start = create_points(1,1,pos_1,0,pos_2) + origin
296        elif plane == 'yz':
297            start = create_points(1,1,0,pos_1,pos_2) + origin
298        else:
299            raise ValueError("Plane not valid. Must be xy, xz or yz")
300        
301        pos_3 = radius * math.sin(angle*(i+1)) 
302        pos_4 = radius * math.cos(angle*(i+1))
303
304        if plane == 'xy':
305            end = create_points(1,1,pos_3,pos_4,0) + origin
306        elif plane == 'xz':
307            end = create_points(1,1,pos_3,0,pos_4) + origin
308        elif plane == 'yz':
309            end = create_points(1,1,0,pos_3,pos_4) + origin
310        else:
311            raise ValueError("Plane not valid. Must be xy, xz or yz")
312
313        offset_1 = create_points(1,1,0,0,0)
314
315        bez = CubicBezier(start,end,offset_1, offset_1.clone())
316        beziers.append(bez)
317    spline = Spline(beziers)
318
319    return spline
def connect_ends(spline: acoustools.Paths.Curves.Spline):
322def connect_ends(spline:Spline):
323    '''
324    Sets the end of the last curve in spline to be the start of the first
325    '''
326    start = spline[0]
327    end = spline[-1]
328
329    end.end = start.start

Sets the end of the last curve in spline to be the start of the first