Cleanup: remove unnecessary use of addon_utils to access the FBX version
[blender-addons.git] / curve_tools / internal.py
blob4f1c5e042b9acbb7fc8a9596944e3ccf6e9e264e
1 # SPDX-FileCopyrightText: 2019-2022 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 import bpy, math, cmath
6 from mathutils import Vector, Matrix
7 from collections import namedtuple
9 units = [
10 ('-', 'None', '1.0', 0),
11 ('px', 'Pixel', '1.0', 1),
12 ('m', 'Meter', '1.0', 2),
13 ('dm', 'Decimeter', '0.1', 3),
14 ('cm', 'Centimeter', '0.01', 4),
15 ('mm', 'Millimeter', '0.001', 5),
16 ('yd', 'Yard', '0.9144', 6),
17 ('ft', 'Foot', '0.3048', 7),
18 ('in', 'Inch', '0.0254', 8)
21 param_tolerance = 0.0001
22 AABB = namedtuple('AxisAlignedBoundingBox', 'center dimensions')
23 Plane = namedtuple('Plane', 'normal distance')
24 Circle = namedtuple('Circle', 'orientation center radius')
26 def circleOfTriangle(a, b, c):
27 # https://en.wikipedia.org/wiki/Circumscribed_circle#Cartesian_coordinates_from_cross-_and_dot-products
28 dirBA = a-b
29 dirCB = b-c
30 dirAC = c-a
31 normal = dirBA.cross(dirCB)
32 lengthBA = dirBA.length
33 lengthCB = dirCB.length
34 lengthAC = dirAC.length
35 lengthN = normal.length
36 if lengthN == 0:
37 return None
38 factor = -1/(2*lengthN*lengthN)
39 alpha = (dirBA@dirAC)*(lengthCB*lengthCB*factor)
40 beta = (dirBA@dirCB)*(lengthAC*lengthAC*factor)
41 gamma = (dirAC@dirCB)*(lengthBA*lengthBA*factor)
42 center = a*alpha+b*beta+c*gamma
43 radius = (lengthBA*lengthCB*lengthAC)/(2*lengthN)
44 tangent = (a-center).normalized()
45 orientation = Matrix.Identity(3)
46 orientation.col[2] = normal/lengthN
47 orientation.col[1] = (a-center).normalized()
48 orientation.col[0] = orientation.col[1].xyz.cross(orientation.col[2].xyz)
49 return Circle(orientation=orientation, center=center, radius=radius)
51 def circleOfBezier(points, tolerance=0.000001, samples=16):
52 circle = circleOfTriangle(points[0], bezierPointAt(points, 0.5), points[3])
53 if circle == None:
54 return None
55 variance = 0
56 for t in range(0, samples):
57 variance += ((circle.center-bezierPointAt(points, (t+1)/(samples-1))).length/circle.radius-1) ** 2
58 variance /= samples
59 return None if variance > tolerance else circle
61 def areaOfPolygon(vertices):
62 area = 0
63 for index, current in enumerate(vertices):
64 prev = vertices[index-1]
65 area += (current[0]+prev[0])*(current[1]-prev[1])
66 return area*0.5
68 def linePointDistance(begin, dir, point):
69 return (point-begin).cross(dir.normalized()).length
71 def linePlaneIntersection(origin, dir, plane):
72 det = dir@plane.normal
73 return float('nan') if det == 0 else (plane.distance-origin@plane.normal)/det
75 def nearestPointOfLines(originA, dirA, originB, dirB, tolerance=0.0):
76 # https://en.wikipedia.org/wiki/Skew_lines#Nearest_Points
77 normal = dirA.cross(dirB)
78 normalA = dirA.cross(normal)
79 normalB = dirB.cross(normal)
80 divisorA = dirA@normalB
81 divisorB = dirB@normalA
82 if abs(divisorA) <= tolerance or abs(divisorB) <= tolerance:
83 return (float('nan'), float('nan'), None, None)
84 else:
85 paramA = (originB-originA)@normalB/divisorA
86 paramB = (originA-originB)@normalA/divisorB
87 return (paramA, paramB, originA+dirA*paramA, originB+dirB*paramB)
89 def lineSegmentLineSegmentIntersection(beginA, endA, beginB, endB, tolerance=0.001):
90 dirA = endA-beginA
91 dirB = endB-beginB
92 paramA, paramB, pointA, pointB = nearestPointOfLines(beginA, dirA, beginB, dirB)
93 if math.isnan(paramA) or (pointA-pointB).length > tolerance or \
94 paramA < 0 or paramA > 1 or paramB < 0 or paramB > 1:
95 return None
96 return (paramA, paramB, pointA, pointB)
98 def aabbOfPoints(points):
99 min = Vector(points[0])
100 max = Vector(points[0])
101 for point in points:
102 for i in range(0, 3):
103 if min[i] > point[i]:
104 min[i] = point[i]
105 if max[i] < point[i]:
106 max[i] = point[i]
107 return AABB(center=(max+min)*0.5, dimensions=(max-min)*0.5)
109 def aabbIntersectionTest(a, b, tolerance=0.0):
110 for i in range(0, 3):
111 if abs(a.center[i]-b.center[i]) > a.dimensions[i]+b.dimensions[i]+tolerance:
112 return False
113 return True
115 def isPointInAABB(point, aabb, tolerance=0.0, ignore_axis=None):
116 for i in range(0, 3):
117 if i != ignore_axis and (point[i] < aabb.center[i]-aabb.dimensions[i]-tolerance or point[i] > aabb.center[i]+aabb.dimensions[i]+tolerance):
118 return False
119 return True
121 def lineAABBIntersection(lineBegin, lineEnd, aabb):
122 intersections = []
123 for i in range(0, 3):
124 normal = [0, 0, 0]
125 normal = Vector(normal[0:i] + [1] + normal[i+1:])
126 for j in range(-1, 2, 2):
127 plane = Plane(normal=normal, distance=aabb.center[i]+j*aabb.dimensions[i])
128 param = linePlaneIntersection(lineBegin, lineEnd-lineBegin, plane)
129 if param < 0 or param > 1 or math.isnan(param):
130 continue
131 point = lineBegin+param*(lineEnd-lineBegin)
132 if isPointInAABB(point, aabb, 0.0, i):
133 intersections.append((param, point))
134 return intersections
136 def bezierPointAt(points, t):
137 s = 1-t
138 return s*s*s*points[0] + 3*s*s*t*points[1] + 3*s*t*t*points[2] + t*t*t*points[3]
140 def bezierTangentAt(points, t):
141 s = 1-t
142 return s*s*(points[1]-points[0])+2*s*t*(points[2]-points[1])+t*t*(points[3]-points[2])
143 # return s*s*points[0] + (s*s-2*s*t)*points[1] + (2*s*t-t*t)*points[2] + t*t*points[3]
145 def bezierLength(points, beginT=0, endT=1, samples=1024):
146 # https://en.wikipedia.org/wiki/Arc_length#Finding_arc_lengths_by_integrating
147 vec = [points[1]-points[0], points[2]-points[1], points[3]-points[2]]
148 dot = [vec[0]@vec[0], vec[0]@vec[1], vec[0]@vec[2], vec[1]@vec[1], vec[1]@vec[2], vec[2]@vec[2]]
149 factors = [
150 dot[0],
151 4*(dot[1]-dot[0]),
152 6*dot[0]+4*dot[3]+2*dot[2]-12*dot[1],
153 12*dot[1]+4*(dot[4]-dot[0]-dot[2])-8*dot[3],
154 dot[0]+dot[5]+2*dot[2]+4*(dot[3]-dot[1]-dot[4])
156 # https://en.wikipedia.org/wiki/Trapezoidal_rule
157 length = 0
158 prev_value = math.sqrt(factors[4]+factors[3]+factors[2]+factors[1]+factors[0])
159 for index in range(0, samples+1):
160 t = beginT+(endT-beginT)*index/samples
161 # value = math.sqrt(factors[4]*(t**4)+factors[3]*(t**3)+factors[2]*(t**2)+factors[1]*t+factors[0])
162 value = math.sqrt((((factors[4]*t+factors[3])*t+factors[2])*t+factors[1])*t+factors[0])
163 length += (prev_value+value)*0.5
164 prev_value = value
165 return length*3/samples
167 # https://en.wikipedia.org/wiki/Root_of_unity
168 # cubic_roots_of_unity = [cmath.rect(1, i/3*2*math.pi) for i in range(0, 3)]
169 cubic_roots_of_unity = [complex(1, 0), complex(-1, math.sqrt(3))*0.5, complex(-1, -math.sqrt(3))*0.5]
170 def bezierRoots(dists, tolerance=0.0001):
171 # https://en.wikipedia.org/wiki/Cubic_function
172 # y(t) = a*t^3 +b*t^2 +c*t^1 +d*t^0
173 a = 3*(dists[1]-dists[2])+dists[3]-dists[0]
174 b = 3*(dists[0]-2*dists[1]+dists[2])
175 c = 3*(dists[1]-dists[0])
176 d = dists[0]
177 if abs(a) > tolerance: # Cubic
178 E2 = a*c
179 E3 = a*a*d
180 A = (2*b*b-9*E2)*b+27*E3
181 B = b*b-3*E2
182 C = ((A+cmath.sqrt(A*A-4*B*B*B))*0.5) ** (1/3)
183 roots = []
184 for root in cubic_roots_of_unity:
185 root *= C
186 root = -1/(3*a)*(b+root+B/root)
187 if abs(root.imag) < tolerance and root.real > -param_tolerance and root.real < 1.0+param_tolerance:
188 roots.append(max(0.0, min(root.real, 1.0)))
189 # Remove doubles
190 roots.sort()
191 for index in range(len(roots)-1, 0, -1):
192 if abs(roots[index-1]-roots[index]) < param_tolerance:
193 roots.pop(index)
194 return roots
195 elif abs(b) > tolerance: # Quadratic
196 disc = c*c-4*b*d
197 if disc < 0:
198 return []
199 disc = math.sqrt(disc)
200 return [(-c-disc)/(2*b), (-c+disc)/(2*b)]
201 elif abs(c) > tolerance: # Linear
202 root = -d/c
203 return [root] if root >= 0.0 and root <= 1.0 else []
204 else: # Constant / Parallel
205 return [] if abs(d) > tolerance else float('inf')
207 def xRaySplineIntersectionTest(spline, origin):
208 spline_points = spline.bezier_points if spline.type == 'BEZIER' else spline.points
209 cyclic_parallel_fix_flag = False
210 intersections = []
212 def areIntersectionsAdjacent(index):
213 if len(intersections) < 2:
214 return
215 prev = intersections[index-1]
216 current = intersections[index]
217 if prev[1] == current[0] and \
218 prev[2] > 1.0-param_tolerance and current[2] < param_tolerance and \
219 ((prev[3] < 0 and current[3] < 0) or (prev[3] > 0 and current[3] > 0)):
220 intersections.pop(index)
222 def appendIntersection(index, root, tangentY, intersectionX):
223 beginPoint = spline_points[index-1]
224 endPoint = spline_points[index]
225 if root == float('inf'): # Segment is parallel to ray
226 if index == 0 and spline.use_cyclic_u:
227 cyclic_parallel_fix_flag = True
228 if len(intersections) > 0 and intersections[-1][1] == beginPoint:
229 intersections[-1][1] = endPoint # Skip in adjacency test
230 elif intersectionX >= origin[0]:
231 intersections.append([beginPoint, endPoint, root, tangentY, intersectionX])
232 areIntersectionsAdjacent(len(intersections)-1)
234 if spline.type == 'BEZIER':
235 for index, endPoint in enumerate(spline.bezier_points):
236 if index == 0 and not spline.use_cyclic_u:
237 continue
238 beginPoint = spline_points[index-1]
239 points = (beginPoint.co, beginPoint.handle_right, endPoint.handle_left, endPoint.co)
240 roots = bezierRoots((points[0][1]-origin[1], points[1][1]-origin[1], points[2][1]-origin[1], points[3][1]-origin[1]))
241 if roots == float('inf'): # Intersection
242 appendIntersection(index, float('inf'), None, None)
243 else:
244 for root in roots:
245 appendIntersection(index, root, bezierTangentAt(points, root)[1], bezierPointAt(points, root)[0])
246 elif spline.type == 'POLY':
247 for index, endPoint in enumerate(spline.points):
248 if index == 0 and not spline.use_cyclic_u:
249 continue
250 beginPoint = spline_points[index-1]
251 points = (beginPoint.co, endPoint.co)
252 if (points[0][0] < origin[0] and points[1][0] < origin[0]) or \
253 (points[0][1] < origin[1] and points[1][1] < origin[1]) or \
254 (points[0][1] > origin[1] and points[1][1] > origin[1]):
255 continue
256 diff = points[1]-points[0]
257 height = origin[1]-points[0][1]
258 if diff[1] == 0: # Parallel
259 if height == 0: # Intersection
260 appendIntersection(index, float('inf'), None, None)
261 else: # Not parallel
262 root = height/diff[1]
263 appendIntersection(index, root, diff[1], points[0][0]+diff[0]*root)
265 if cyclic_parallel_fix_flag:
266 appendIntersection(0, float('inf'), None, None)
267 areIntersectionsAdjacent(0)
268 return intersections
270 def isPointInSpline(point, spline):
271 return spline.use_cyclic_u and len(xRaySplineIntersectionTest(spline, point))%2 == 1
273 def isSegmentLinear(points, tolerance=0.0001):
274 return 1.0-(points[1]-points[0]).normalized()@(points[3]-points[2]).normalized() < tolerance
276 def bezierSegmentPoints(begin, end):
277 return [begin.co, begin.handle_right, end.handle_left, end.co]
279 def grab_cursor(context, event):
280 if event.mouse_region_x < 0:
281 context.window.cursor_warp(context.region.x+context.region.width, event.mouse_y)
282 elif event.mouse_region_x > context.region.width:
283 context.window.cursor_warp(context.region.x, event.mouse_y)
284 elif event.mouse_region_y < 0:
285 context.window.cursor_warp(event.mouse_x, context.region.y+context.region.height)
286 elif event.mouse_region_y > context.region.height:
287 context.window.cursor_warp(event.mouse_x, context.region.y)
289 def deleteFromArray(item, array):
290 for index, current in enumerate(array):
291 if current is item:
292 array.pop(index)
293 break
295 def copyAttributes(dst, src):
296 for attribute in dir(src):
297 try:
298 setattr(dst, attribute, getattr(src, attribute))
299 except:
300 pass
302 def bezierSliceFromTo(points, minParam, maxParam):
303 fromP = bezierPointAt(points, minParam)
304 fromT = bezierTangentAt(points, minParam)
305 toP = bezierPointAt(points, maxParam)
306 toT = bezierTangentAt(points, maxParam)
307 paramDiff = maxParam-minParam
308 return [fromP, fromP+fromT*paramDiff, toP-toT*paramDiff, toP]
310 def bezierIntersectionBroadPhase(solutions, pointsA, pointsB, aMin=0.0, aMax=1.0, bMin=0.0, bMax=1.0, depth=8, tolerance=0.001):
311 if aabbIntersectionTest(aabbOfPoints(bezierSliceFromTo(pointsA, aMin, aMax)), aabbOfPoints(bezierSliceFromTo(pointsB, bMin, bMax)), tolerance) == False:
312 return
313 if depth == 0:
314 solutions.append([aMin, aMax, bMin, bMax])
315 return
316 depth -= 1
317 aMid = (aMin+aMax)*0.5
318 bMid = (bMin+bMax)*0.5
319 bezierIntersectionBroadPhase(solutions, pointsA, pointsB, aMin, aMid, bMin, bMid, depth, tolerance)
320 bezierIntersectionBroadPhase(solutions, pointsA, pointsB, aMin, aMid, bMid, bMax, depth, tolerance)
321 bezierIntersectionBroadPhase(solutions, pointsA, pointsB, aMid, aMax, bMin, bMid, depth, tolerance)
322 bezierIntersectionBroadPhase(solutions, pointsA, pointsB, aMid, aMax, bMid, bMax, depth, tolerance)
324 def bezierIntersectionNarrowPhase(broadPhase, pointsA, pointsB, tolerance=0.000001):
325 aMin = broadPhase[0]
326 aMax = broadPhase[1]
327 bMin = broadPhase[2]
328 bMax = broadPhase[3]
329 while (aMax-aMin > tolerance) or (bMax-bMin > tolerance):
330 aMid = (aMin+aMax)*0.5
331 bMid = (bMin+bMax)*0.5
332 a1 = bezierPointAt(pointsA, (aMin+aMid)*0.5)
333 a2 = bezierPointAt(pointsA, (aMid+aMax)*0.5)
334 b1 = bezierPointAt(pointsB, (bMin+bMid)*0.5)
335 b2 = bezierPointAt(pointsB, (bMid+bMax)*0.5)
336 a1b1Dist = (a1-b1).length
337 a2b1Dist = (a2-b1).length
338 a1b2Dist = (a1-b2).length
339 a2b2Dist = (a2-b2).length
340 minDist = min(a1b1Dist, a2b1Dist, a1b2Dist, a2b2Dist)
341 if a1b1Dist == minDist:
342 aMax = aMid
343 bMax = bMid
344 elif a2b1Dist == minDist:
345 aMin = aMid
346 bMax = bMid
347 elif a1b2Dist == minDist:
348 aMax = aMid
349 bMin = bMid
350 else:
351 aMin = aMid
352 bMin = bMid
353 return [aMin, bMin, minDist]
355 def segmentIntersection(segmentA, segmentB, tolerance=0.001):
356 pointsA = bezierSegmentPoints(segmentA['beginPoint'], segmentA['endPoint'])
357 pointsB = bezierSegmentPoints(segmentB['beginPoint'], segmentB['endPoint'])
358 result = []
359 def addCut(paramA, paramB):
360 cutA = {'param': paramA, 'segment': segmentA}
361 cutB = {'param': paramB, 'segment': segmentB}
362 cutA['otherCut'] = cutB
363 cutB['otherCut'] = cutA
364 segmentA['cuts'].append(cutA)
365 segmentB['cuts'].append(cutB)
366 result.append([cutA, cutB])
367 if isSegmentLinear(pointsA) and isSegmentLinear(pointsB):
368 intersection = lineSegmentLineSegmentIntersection(pointsA[0], pointsA[3], pointsB[0], pointsB[3])
369 if intersection != None:
370 addCut(intersection[0], intersection[1])
371 return result
372 solutions = []
373 bezierIntersectionBroadPhase(solutions, pointsA, pointsB)
374 for index in range(0, len(solutions)):
375 solutions[index] = bezierIntersectionNarrowPhase(solutions[index], pointsA, pointsB)
376 for index in range(0, len(solutions)):
377 for otherIndex in range(0, len(solutions)):
378 if solutions[index][2] == float('inf'):
379 break
380 if index == otherIndex or solutions[otherIndex][2] == float('inf'):
381 continue
382 diffA = solutions[index][0]-solutions[otherIndex][0]
383 diffB = solutions[index][1]-solutions[otherIndex][1]
384 if diffA*diffA+diffB*diffB < 0.01:
385 if solutions[index][2] < solutions[otherIndex][2]:
386 solutions[otherIndex][2] = float('inf')
387 else:
388 solutions[index][2] = float('inf')
389 def areIntersectionsAdjacent(segmentA, segmentB, paramA, paramB):
390 return segmentA['endIndex'] == segmentB['beginIndex'] and paramA > 1-param_tolerance and paramB < param_tolerance
391 for solution in solutions:
392 if (solution[2] > tolerance) or \
393 (segmentA['spline'] == segmentB['spline'] and \
394 (areIntersectionsAdjacent(segmentA, segmentB, solution[0], solution[1]) or \
395 areIntersectionsAdjacent(segmentB, segmentA, solution[1], solution[0]))):
396 continue
397 addCut(solution[0], solution[1])
398 return result
400 def bezierMultiIntersection(segments):
401 for index in range(0, len(segments)):
402 for otherIndex in range(index+1, len(segments)):
403 segmentIntersection(segments[index], segments[otherIndex])
404 prepareSegmentIntersections(segments)
405 subdivideBezierSegments(segments)
407 def bezierProjectHandles(segments):
408 insertions = []
409 index_offset = 0
410 for segment in segments:
411 if len(insertions) > 0 and insertions[-1][0] != segment['spline']:
412 index_offset = 0
413 points = bezierSegmentPoints(segment['beginPoint'], segment['endPoint'])
414 paramA, paramB, pointA, pointB = nearestPointOfLines(points[0], points[1]-points[0], points[3], points[2]-points[3])
415 if pointA and pointB:
416 segment['cuts'].append({'param': 0.5})
417 insertions.append((segment['spline'], segment['beginIndex']+1+index_offset, (pointA+pointB)*0.5))
418 index_offset += 1
419 subdivideBezierSegments(segments)
420 for insertion in insertions:
421 bezier_point = insertion[0].bezier_points[insertion[1]]
422 bezier_point.co = insertion[2]
423 bezier_point.handle_left_type = 'VECTOR'
424 bezier_point.handle_right_type = 'VECTOR'
426 def bezierSubivideAt(points, params):
427 if len(params) == 0:
428 return []
429 newPoints = []
430 newPoints.append(points[0]+(points[1]-points[0])*params[0])
431 for index, param in enumerate(params):
432 paramLeft = param
433 if index > 0:
434 paramLeft -= params[index-1]
435 paramRight = -param
436 if index == len(params)-1:
437 paramRight += 1.0
438 else:
439 paramRight += params[index+1]
440 point = bezierPointAt(points, param)
441 tangent = bezierTangentAt(points, param)
442 newPoints.append(point-tangent*paramLeft)
443 newPoints.append(point)
444 newPoints.append(point+tangent*paramRight)
445 newPoints.append(points[3]-(points[3]-points[2])*(1.0-params[-1]))
446 return newPoints
448 def subdivideBezierSegment(segment):
449 # Blender only allows uniform subdivision. Use this method to subdivide at arbitrary params.
450 # NOTE: segment['cuts'] must be sorted by param
451 if len(segment['cuts']) == 0:
452 return
454 segment['beginPoint'] = segment['spline'].bezier_points[segment['beginIndex']]
455 segment['endPoint'] = segment['spline'].bezier_points[segment['endIndex']]
456 params = [cut['param'] for cut in segment['cuts']]
457 newPoints = bezierSubivideAt(bezierSegmentPoints(segment['beginPoint'], segment['endPoint']), params)
458 bpy.ops.curve.select_all(action='DESELECT')
459 segment['beginPoint'] = segment['spline'].bezier_points[segment['beginIndex']]
460 segment['beginPoint'].select_right_handle = True
461 segment['beginPoint'].handle_left_type = 'FREE'
462 segment['beginPoint'].handle_right_type = 'FREE'
463 segment['endPoint'] = segment['spline'].bezier_points[segment['endIndex']]
464 segment['endPoint'].select_left_handle = True
465 segment['endPoint'].handle_left_type = 'FREE'
466 segment['endPoint'].handle_right_type = 'FREE'
468 bpy.ops.curve.subdivide(number_cuts=len(params))
469 if segment['endIndex'] > 0:
470 segment['endIndex'] += len(params)
471 segment['beginPoint'] = segment['spline'].bezier_points[segment['beginIndex']]
472 segment['endPoint'] = segment['spline'].bezier_points[segment['endIndex']]
473 segment['beginPoint'].select_right_handle = False
474 segment['beginPoint'].handle_right = newPoints[0]
475 segment['endPoint'].select_left_handle = False
476 segment['endPoint'].handle_left = newPoints[-1]
478 for index, cut in enumerate(segment['cuts']):
479 cut['index'] = segment['beginIndex']+1+index
480 newPoint = segment['spline'].bezier_points[cut['index']]
481 newPoint.handle_left_type = 'FREE'
482 newPoint.handle_right_type = 'FREE'
483 newPoint.select_left_handle = False
484 newPoint.select_control_point = False
485 newPoint.select_right_handle = False
486 newPoint.handle_left = newPoints[index*3+1]
487 newPoint.co = newPoints[index*3+2]
488 newPoint.handle_right = newPoints[index*3+3]
490 def prepareSegmentIntersections(segments):
491 def areCutsAdjacent(cutA, cutB):
492 return cutA['segment']['beginIndex'] == cutB['segment']['endIndex'] and \
493 cutA['param'] < param_tolerance and cutB['param'] > 1.0-param_tolerance
494 for segment in segments:
495 segment['cuts'].sort(key=(lambda cut: cut['param']))
496 for index in range(len(segment['cuts'])-1, 0, -1):
497 prev = segment['cuts'][index-1]
498 current = segment['cuts'][index]
499 if abs(prev['param']-current['param']) < param_tolerance and \
500 prev['otherCut']['segment']['spline'] == current['otherCut']['segment']['spline'] and \
501 (areCutsAdjacent(prev['otherCut'], current['otherCut']) or \
502 areCutsAdjacent(current['otherCut'], prev['otherCut'])):
503 deleteFromArray(prev['otherCut'], prev['otherCut']['segment']['cuts'])
504 deleteFromArray(current['otherCut'], current['otherCut']['segment']['cuts'])
505 segment['cuts'].pop(index-1 if current['otherCut']['param'] < param_tolerance else index)
506 current = segment['cuts'][index-1]['otherCut']
507 current['segment']['extraCut'] = current
509 def subdivideBezierSegmentsOfSameSpline(segments):
510 # NOTE: segment['cuts'] must be sorted by param
511 indexOffset = 0
512 for segment in segments:
513 segment['beginIndex'] += indexOffset
514 if segment['endIndex'] > 0:
515 segment['endIndex'] += indexOffset
516 subdivideBezierSegment(segment)
517 indexOffset += len(segment['cuts'])
518 for segment in segments:
519 segment['beginPoint'] = segment['spline'].bezier_points[segment['beginIndex']]
520 segment['endPoint'] = segment['spline'].bezier_points[segment['endIndex']]
522 def subdivideBezierSegments(segments):
523 # NOTE: segment['cuts'] must be sorted by param
524 groups = {}
525 for segment in segments:
526 spline = segment['spline']
527 if (spline in groups) == False:
528 groups[spline] = []
529 group = groups[spline]
530 group.append(segment)
531 for spline in groups:
532 subdivideBezierSegmentsOfSameSpline(groups[spline])
534 def curveObject():
535 obj = bpy.context.object
536 return obj if obj != None and obj.type == 'CURVE' and obj.mode == 'EDIT' else None
538 def bezierSegments(splines, selection_only):
539 segments = []
540 for spline in splines:
541 if spline.type != 'BEZIER':
542 continue
543 for index, current in enumerate(spline.bezier_points):
544 next = spline.bezier_points[(index+1) % len(spline.bezier_points)]
545 if next == spline.bezier_points[0] and not spline.use_cyclic_u:
546 continue
547 if not selection_only or (current.select_right_handle and next.select_left_handle):
548 segments.append({
549 'spline': spline,
550 'beginIndex': index,
551 'endIndex': index+1 if index < len(spline.bezier_points)-1 else 0,
552 'beginPoint': current,
553 'endPoint': next,
554 'cuts': []
556 return segments
558 def getSelectedSplines(include_bezier, include_polygon, allow_partial_selection=False):
559 result = []
560 for spline in bpy.context.object.data.splines:
561 selected = not allow_partial_selection
562 if spline.type == 'BEZIER':
563 if not include_bezier:
564 continue
565 for index, point in enumerate(spline.bezier_points):
566 if point.select_left_handle == allow_partial_selection or \
567 point.select_control_point == allow_partial_selection or \
568 point.select_right_handle == allow_partial_selection:
569 selected = allow_partial_selection
570 break
571 elif spline.type == 'POLY':
572 if not include_polygon:
573 continue
574 for index, point in enumerate(spline.points):
575 if point.select == allow_partial_selection:
576 selected = allow_partial_selection
577 break
578 else:
579 continue
580 if selected:
581 result.append(spline)
582 return result
584 def addObject(type, name):
585 if type == 'CURVE':
586 data = bpy.data.curves.new(name=name, type='CURVE')
587 data.dimensions = '3D'
588 elif type == 'MESH':
589 data = bpy.data.meshes.new(name=name, type='MESH')
590 obj = bpy.data.objects.new(name, data)
591 obj.location = bpy.context.scene.cursor.location
592 bpy.context.scene.collection.objects.link(obj)
593 obj.select_set(True)
594 bpy.context.view_layer.objects.active = obj
595 return obj
597 def addPolygonSpline(obj, cyclic, vertices, weights=None, select=False):
598 spline = obj.data.splines.new(type='POLY')
599 spline.use_cyclic_u = cyclic
600 spline.points.add(len(vertices)-1)
601 for index, point in enumerate(spline.points):
602 point.co.xyz = vertices[index]
603 point.select = select
604 if weights:
605 point.weight_softbody = weights[index]
606 return spline
608 def addBezierSpline(obj, cyclic, vertices, weights=None, select=False):
609 spline = obj.data.splines.new(type='BEZIER')
610 spline.use_cyclic_u = cyclic
611 spline.bezier_points.add(len(vertices)-1)
612 for index, point in enumerate(spline.bezier_points):
613 point.handle_left = vertices[index][0]
614 point.co = vertices[index][1]
615 point.handle_right = vertices[index][2]
616 if weights:
617 point.weight_softbody = weights[index]
618 point.select_left_handle = select
619 point.select_control_point = select
620 point.select_right_handle = select
621 if isSegmentLinear([vertices[index-1][1], vertices[index-1][2], vertices[index][0], vertices[index][1]]):
622 spline.bezier_points[index-1].handle_right_type = 'VECTOR'
623 point.handle_left_type = 'VECTOR'
624 return spline
626 def mergeEnds(splines, points, is_last_point):
627 bpy.ops.curve.select_all(action='DESELECT')
628 points[0].handle_left_type = points[0].handle_right_type = 'FREE'
629 new_co = (points[0].co+points[1].co)*0.5
630 handle = (points[1].handle_left if is_last_point[1] else points[1].handle_right)+new_co-points[1].co
631 points[0].select_left_handle = points[0].select_right_handle = True
632 if is_last_point[0]:
633 points[0].handle_left += new_co-points[0].co
634 points[0].handle_right = handle
635 else:
636 points[0].handle_right += new_co-points[0].co
637 points[0].handle_left = handle
638 points[0].co = new_co
639 points[0].select_control_point = points[1].select_control_point = True
640 bpy.ops.curve.make_segment()
641 spline = splines[0] if splines[0] in bpy.context.object.data.splines.values() else splines[1]
642 point = next(point for point in spline.bezier_points if point.select_left_handle)
643 point.select_left_handle = point.select_right_handle = point.select_control_point = False
644 bpy.ops.curve.delete()
645 return spline
647 def polygonArcAt(center, radius, begin_angle, angle, step_angle, include_ends):
648 vertices = []
649 circle_samples = math.ceil(abs(angle)/step_angle)
650 for t in (range(0, circle_samples+1) if include_ends else range(1, circle_samples)):
651 t = begin_angle+angle*t/circle_samples
652 normal = Vector((math.cos(t), math.sin(t), 0))
653 vertices.append(center+normal*radius)
654 return vertices
656 def bezierArcAt(tangent, normal, center, radius, angle, tolerance=0.99999):
657 transform = Matrix.Identity(4)
658 transform.col[0].xyz = tangent.cross(normal)*radius
659 transform.col[1].xyz = tangent*radius
660 transform.col[2].xyz = normal*radius
661 transform.col[3].xyz = center
662 segments = []
663 segment_count = math.ceil(abs(angle)/(math.pi*0.5)*tolerance)
664 angle /= segment_count
665 x0 = math.cos(angle*0.5)
666 y0 = math.sin(angle*0.5)
667 x1 = (4.0-x0)/3.0
668 y1 = (1.0-x0)*(3.0-x0)/(3.0*y0)
669 points = [
670 Vector((x0, -y0, 0)),
671 Vector((x1, -y1, 0)),
672 Vector((x1, y1, 0)),
673 Vector((x0, y0, 0))
675 for i in range(0, segment_count):
676 rotation = Matrix.Rotation((i+0.5)*angle, 4, 'Z')
677 segments.append(list(map(lambda v: transform@(rotation@v), points)))
678 return segments
680 def iterateSpline(spline, callback):
681 spline_points = spline.bezier_points if spline.type == 'BEZIER' else spline.points
682 for index, spline_point in enumerate(spline_points):
683 prev = spline_points[index-1]
684 current = spline_points[index]
685 next = spline_points[(index+1)%len(spline_points)]
686 if spline.type == 'BEZIER':
687 selected = current.select_control_point
688 prev_segment_points = bezierSegmentPoints(prev, current)
689 next_segment_points = bezierSegmentPoints(current, next)
690 prev_tangent = (prev_segment_points[3]-prev_segment_points[2]).normalized()
691 current_tangent = (next_segment_points[1]-next_segment_points[0]).normalized()
692 next_tangent = (next_segment_points[3]-next_segment_points[2]).normalized()
693 else:
694 selected = current.select
695 prev_segment_points = [prev.co.xyz, None, None, current.co.xyz]
696 next_segment_points = [current.co.xyz, None, None, next.co.xyz]
697 prev_tangent = (prev_segment_points[3]-prev_segment_points[0]).normalized()
698 current_tangent = next_tangent = (next_segment_points[3]-next_segment_points[0]).normalized()
699 normal = prev_tangent.cross(current_tangent).normalized()
700 angle = prev_tangent@current_tangent
701 angle = 0 if abs(angle-1.0) < 0.0001 else math.acos(angle)
702 is_first = (index == 0) and not spline.use_cyclic_u
703 is_last = (index == len(spline_points)-1) and not spline.use_cyclic_u
704 callback(prev_segment_points, next_segment_points, selected, prev_tangent, current_tangent, next_tangent, normal, angle, is_first, is_last)
705 return spline_points
707 def offsetPolygonOfSpline(spline, offset, step_angle, round_line_join, bezier_samples=128, tolerance=0.000001):
708 def offsetVertex(position, tangent):
709 normal = Vector((-tangent[1], tangent[0], 0))
710 return position+normal*offset
711 vertices = []
712 def handlePoint(prev_segment_points, next_segment_points, selected, prev_tangent, current_tangent, next_tangent, normal, angle, is_first, is_last):
713 sign = math.copysign(1, normal[2])
714 angle *= sign
715 if is_last:
716 return
717 is_protruding = (abs(angle) > tolerance and abs(offset) > tolerance)
718 if is_protruding and not is_first and sign != math.copysign(1, offset): # Convex Corner
719 if round_line_join:
720 begin_angle = math.atan2(prev_tangent[1], prev_tangent[0])+math.pi*0.5
721 vertices.extend(polygonArcAt(next_segment_points[0], offset, begin_angle, angle, step_angle, False))
722 else:
723 distance = offset*math.tan(angle*0.5)
724 vertices.append(offsetVertex(next_segment_points[0], current_tangent)+current_tangent*distance)
725 if is_protruding or is_first:
726 vertices.append(offsetVertex(next_segment_points[0], current_tangent))
727 if spline.type == 'POLY' or isSegmentLinear(next_segment_points):
728 vertices.append(offsetVertex(next_segment_points[3], next_tangent))
729 else: # Trace Bezier Segment
730 prev_tangent = bezierTangentAt(next_segment_points, 0).normalized()
731 for t in range(1, bezier_samples+1):
732 t /= bezier_samples
733 tangent = bezierTangentAt(next_segment_points, t).normalized()
734 if t == 1 or math.acos(min(max(-1, prev_tangent@tangent), 1)) >= step_angle:
735 vertices.append(offsetVertex(bezierPointAt(next_segment_points, t), tangent))
736 prev_tangent = tangent
737 spline_points = iterateSpline(spline, handlePoint)
739 # Solve Self Intersections
740 original_area = areaOfPolygon([point.co for point in spline_points])
741 sign = -1 if offset < 0 else 1
742 i = (0 if spline.use_cyclic_u else 1)
743 while i < len(vertices):
744 j = i+2
745 while j < len(vertices) - (0 if i > 0 else 1):
746 intersection = lineSegmentLineSegmentIntersection(vertices[i-1], vertices[i], vertices[j-1], vertices[j])
747 if intersection == None:
748 j += 1
749 continue
750 intersection = (intersection[2]+intersection[3])*0.5
751 areaInner = sign*areaOfPolygon([intersection, vertices[i], vertices[j-1]])
752 areaOuter = sign*areaOfPolygon([intersection, vertices[j], vertices[i-1]])
753 if areaInner > areaOuter:
754 vertices = vertices[i:j]+[intersection]
755 i = (0 if spline.use_cyclic_u else 1)
756 else:
757 vertices = vertices[:i]+[intersection]+vertices[j:]
758 j = i+2
759 i += 1
760 new_area = areaOfPolygon(vertices)
761 return [vertices] if original_area*new_area >= 0 else []
763 def filletSpline(spline, radius, chamfer_mode, limit_half_way, tolerance=0.0001):
764 vertices = []
765 distance_limit_factor = 0.5 if limit_half_way else 1.0
766 def handlePoint(prev_segment_points, next_segment_points, selected, prev_tangent, current_tangent, next_tangent, normal, angle, is_first, is_last):
767 distance = min((prev_segment_points[0]-prev_segment_points[3]).length*distance_limit_factor, (next_segment_points[0]-next_segment_points[3]).length*distance_limit_factor)
768 if not selected or is_first or is_last or angle == 0 or distance == 0 or \
769 (spline.type == 'BEZIER' and not (isSegmentLinear(prev_segment_points) and isSegmentLinear(next_segment_points))):
770 prev_handle = next_segment_points[0] if is_first else prev_segment_points[2] if spline.type == 'BEZIER' else prev_segment_points[0]
771 next_handle = next_segment_points[0] if is_last else next_segment_points[1] if spline.type == 'BEZIER' else next_segment_points[3]
772 vertices.append([prev_handle, next_segment_points[0], next_handle])
773 return
774 tan_factor = math.tan(angle*0.5)
775 offset = min(radius, distance/tan_factor)
776 distance = offset*tan_factor
777 circle_center = next_segment_points[0]+normal.cross(prev_tangent)*offset-prev_tangent*distance
778 segments = bezierArcAt(prev_tangent, normal, circle_center, offset, angle)
779 if chamfer_mode:
780 vertices.append([prev_segment_points[0], segments[0][0], segments[-1][3]])
781 vertices.append([segments[0][0], segments[-1][3], next_segment_points[3]])
782 else:
783 for i in range(0, len(segments)+1):
784 vertices.append([
785 segments[i-1][2] if i > 0 else prev_segment_points[0],
786 segments[i][0] if i < len(segments) else segments[i-1][3],
787 segments[i][1] if i < len(segments) else next_segment_points[3]
789 iterateSpline(spline, handlePoint)
790 i = 0 if spline.use_cyclic_u else 1
791 while(i < len(vertices)):
792 if (vertices[i-1][1]-vertices[i][1]).length < tolerance:
793 vertices[i-1][2] = vertices[i][2]
794 del vertices[i]
795 else:
796 i = i+1
797 return addBezierSpline(bpy.context.object, spline.use_cyclic_u, vertices)
799 def dogBone(spline, radius):
800 vertices = []
801 def handlePoint(prev_segment_points, next_segment_points, selected, prev_tangent, current_tangent, next_tangent, normal, angle, is_first, is_last):
802 if not selected or is_first or is_last or angle == 0 or normal[2] > 0.0 or \
803 (spline.type == 'BEZIER' and not (isSegmentLinear(prev_segment_points) and isSegmentLinear(next_segment_points))):
804 prev_handle = next_segment_points[0] if is_first else prev_segment_points[2] if spline.type == 'BEZIER' else prev_segment_points[0]
805 next_handle = next_segment_points[0] if is_last else next_segment_points[1] if spline.type == 'BEZIER' else next_segment_points[3]
806 vertices.append([prev_handle, next_segment_points[0], next_handle])
807 return
808 tan_factor = math.tan(angle*0.5)
809 corner = next_segment_points[0]+normal.cross(prev_tangent)*radius-prev_tangent*radius*tan_factor
810 direction = next_segment_points[0]-corner
811 distance = direction.length
812 corner = next_segment_points[0]+direction/distance*(distance-radius)
813 vertices.append([prev_segment_points[0], next_segment_points[0], corner])
814 vertices.append([next_segment_points[0], corner, next_segment_points[0]])
815 vertices.append([corner, next_segment_points[0], next_segment_points[3]])
816 iterateSpline(spline, handlePoint)
817 return vertices
819 def discretizeCurve(spline, step_angle, samples):
820 vertices = []
821 def handlePoint(prev_segment_points, next_segment_points, selected, prev_tangent, current_tangent, next_tangent, normal, angle, is_first, is_last):
822 if is_last:
823 return
824 if isSegmentLinear(next_segment_points):
825 vertices.append(next_segment_points[3])
826 else:
827 prev_tangent = bezierTangentAt(next_segment_points, 0).normalized()
828 for t in range(1, samples+1):
829 t /= samples
830 tangent = bezierTangentAt(next_segment_points, t).normalized()
831 if t == 1 or math.acos(min(max(-1, prev_tangent@tangent), 1)) >= step_angle:
832 vertices.append(bezierPointAt(next_segment_points, t))
833 prev_tangent = tangent
834 iterateSpline(spline, handlePoint)
835 return vertices
837 def bezierBooleanGeometry(splineA, splineB, operation):
838 if not splineA.use_cyclic_u or not splineB.use_cyclic_u:
839 return False
840 segmentsA = bezierSegments([splineA], False)
841 segmentsB = bezierSegments([splineB], False)
843 deletionFlagA = isPointInSpline(splineA.bezier_points[0].co, splineB)
844 deletionFlagB = isPointInSpline(splineB.bezier_points[0].co, splineA)
845 if operation == 'DIFFERENCE':
846 deletionFlagB = not deletionFlagB
847 elif operation == 'INTERSECTION':
848 deletionFlagA = not deletionFlagA
849 deletionFlagB = not deletionFlagB
850 elif operation != 'UNION':
851 return False
853 intersections = []
854 for segmentA in segmentsA:
855 for segmentB in segmentsB:
856 intersections.extend(segmentIntersection(segmentA, segmentB))
857 if len(intersections) == 0:
858 if deletionFlagA:
859 bpy.context.object.data.splines.remove(splineA)
860 if deletionFlagB:
861 bpy.context.object.data.splines.remove(splineB)
862 return True
864 prepareSegmentIntersections(segmentsA)
865 prepareSegmentIntersections(segmentsB)
866 subdivideBezierSegmentsOfSameSpline(segmentsA)
867 subdivideBezierSegmentsOfSameSpline(segmentsB)
869 def collectCuts(cuts, segments, deletionFlag):
870 for segmentIndex, segment in enumerate(segments):
871 if 'extraCut' in segment:
872 deletionFlag = not deletionFlag
873 segment['extraCut']['index'] = segment['beginIndex']
874 segment['extraCut']['deletionFlag'] = deletionFlag
875 cuts.append(segment['extraCut'])
876 else:
877 cuts.append(None)
878 cuts.extend(segments[segmentIndex]['cuts'])
879 segment['deletionFlag'] = deletionFlag
880 for cutIndex, cut in enumerate(segment['cuts']):
881 deletionFlag = not deletionFlag
882 cut['deletionFlag'] = deletionFlag
883 cutsA = []
884 cutsB = []
885 collectCuts(cutsA, segmentsA, deletionFlagA)
886 collectCuts(cutsB, segmentsB, deletionFlagB)
888 beginIndex = 0
889 for segment in segmentsA:
890 if segment['deletionFlag'] == False:
891 beginIndex = segment['beginIndex']
892 break
893 for cut in segment['cuts']:
894 if cut['deletionFlag'] == False:
895 beginIndex = cut['index']
896 break
898 cuts = cutsA
899 spline = splineA
900 index = beginIndex
901 backward = False
902 vertices = []
903 while True:
904 current = spline.bezier_points[index]
905 vertices.append([current.handle_left, current.co, current.handle_right])
906 if backward:
907 current.handle_left, current.handle_right = current.handle_right.copy(), current.handle_left.copy()
908 index += len(spline.bezier_points)-1 if backward else 1
909 index %= len(spline.bezier_points)
910 if spline == splineA and index == beginIndex:
911 break
913 cut = cuts[index]
914 if cut != None:
915 current = spline.bezier_points[index]
916 current_handle = current.handle_right if backward else current.handle_left
917 spline = splineA if spline == splineB else splineB
918 cuts = cutsA if spline == splineA else cutsB
919 index = cut['otherCut']['index']
920 backward = cut['otherCut']['deletionFlag']
921 next = spline.bezier_points[index]
922 if backward:
923 next.handle_right = current_handle
924 else:
925 next.handle_left = current_handle
926 if spline == splineA and index == beginIndex:
927 break
929 spline = addBezierSpline(bpy.context.object, True, vertices)
930 bpy.context.object.data.splines.remove(splineA)
931 bpy.context.object.data.splines.remove(splineB)
932 bpy.context.object.data.splines.active = spline
933 return True
935 def truncateToFitBox(transform, spline, aabb):
936 spline_points = spline.points
937 aux = {
938 'traces': [],
939 'vertices': [],
940 'weights': []
942 def terminateTrace(aux):
943 if len(aux['vertices']) > 0:
944 aux['traces'].append((aux['vertices'], aux['weights']))
945 aux['vertices'] = []
946 aux['weights'] = []
947 for index, point in enumerate(spline_points):
948 begin = transform@point.co.xyz
949 end = spline_points[(index+1)%len(spline_points)]
950 inside = isPointInAABB(begin, aabb)
951 if inside:
952 aux['vertices'].append(begin)
953 aux['weights'].append(point.weight_softbody)
954 if index == len(spline_points)-1 and not spline.use_cyclic_u:
955 break
956 intersections = lineAABBIntersection(begin, transform@end.co.xyz, aabb)
957 if len(intersections) == 2:
958 terminateTrace(aux)
959 aux['traces'].append((
960 [intersections[0][1], intersections[1][1]],
961 [end.weight_softbody, end.weight_softbody]
963 elif len(intersections) == 1:
964 aux['vertices'].append(intersections[0][1])
965 aux['weights'].append(end.weight_softbody)
966 if inside:
967 terminateTrace(aux)
968 elif inside and index == len(spline_points)-1 and spline.use_cyclic_u:
969 terminateTrace(aux)
970 aux['traces'][0] = (aux['traces'][-1][0]+aux['traces'][0][0], aux['traces'][-1][1]+aux['traces'][0][1])
971 aux['traces'].pop()
972 terminateTrace(aux)
973 return aux['traces']
975 def arrayModifier(splines, offset, count, connect, serpentine):
976 if connect:
977 for spline in splines:
978 if spline.use_cyclic_u:
979 spline.use_cyclic_u = False
980 points = spline.points if spline.type == 'POLY' else spline.bezier_points
981 points.add(1)
982 copyAttributes(points[-1], points[0])
983 bpy.ops.curve.select_all(action='DESELECT')
984 for spline in splines:
985 if spline.type == 'BEZIER':
986 for point in spline.bezier_points:
987 point.select_left_handle = point.select_control_point = point.select_right_handle = True
988 elif spline.type == 'POLY':
989 for point in spline.points:
990 point.select = True
991 splines_at_layer = [splines]
992 for i in range(1, count):
993 bpy.ops.curve.duplicate()
994 bpy.ops.transform.translate(value=offset)
995 splines_at_layer.append(getSelectedSplines(True, True))
996 if serpentine:
997 bpy.ops.curve.switch_direction()
998 if connect:
999 for i in range(1, count):
1000 prev_layer = splines_at_layer[i-1]
1001 next_layer = splines_at_layer[i]
1002 for j in range(0, len(next_layer)):
1003 bpy.ops.curve.select_all(action='DESELECT')
1004 if prev_layer[j].type == 'POLY':
1005 prev_layer[j].points[-1].select = True
1006 else:
1007 prev_layer[j].bezier_points[-1].select_control_point = True
1008 if next_layer[j].type == 'POLY':
1009 next_layer[j].points[0].select = True
1010 else:
1011 next_layer[j].bezier_points[0].select_control_point = True
1012 bpy.ops.curve.make_segment()
1013 bpy.ops.curve.select_all(action='DESELECT')