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