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
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
31 normal
= dirBA
.cross(dirCB
)
32 lengthBA
= dirBA
.length
33 lengthCB
= dirCB
.length
34 lengthAC
= dirAC
.length
35 lengthN
= normal
.length
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])
56 for t
in range(0, samples
):
57 variance
+= ((circle
.center
-bezierPointAt(points
, (t
+1)/(samples
-1))).length
/circle
.radius
-1) ** 2
59 return None if variance
> tolerance
else circle
61 def areaOfPolygon(vertices
):
63 for index
, current
in enumerate(vertices
):
64 prev
= vertices
[index
-1]
65 area
+= (current
[0]+prev
[0])*(current
[1]-prev
[1])
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)
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):
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:
96 return (paramA
, paramB
, pointA
, pointB
)
98 def aabbOfPoints(points
):
99 min = Vector(points
[0])
100 max = Vector(points
[0])
102 for i
in range(0, 3):
103 if min[i
] > point
[i
]:
105 if 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
:
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
):
121 def lineAABBIntersection(lineBegin
, lineEnd
, aabb
):
123 for i
in range(0, 3):
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
):
131 point
= lineBegin
+param
*(lineEnd
-lineBegin
)
132 if isPointInAABB(point
, aabb
, 0.0, i
):
133 intersections
.append((param
, point
))
136 def bezierPointAt(points
, 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
):
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]]
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
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
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])
177 if abs(a
) > tolerance
: # Cubic
180 A
= (2*b
*b
-9*E2
)*b
+27*E3
182 C
= ((A
+cmath
.sqrt(A
*A
-4*B
*B
*B
))*0.5) ** (1/3)
184 for root
in cubic_roots_of_unity
:
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)))
191 for index
in range(len(roots
)-1, 0, -1):
192 if abs(roots
[index
-1]-roots
[index
]) < param_tolerance
:
195 elif abs(b
) > tolerance
: # Quadratic
199 disc
= math
.sqrt(disc
)
200 return [(-c
-disc
)/(2*b
), (-c
+disc
)/(2*b
)]
201 elif abs(c
) > tolerance
: # Linear
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
212 def areIntersectionsAdjacent(index
):
213 if len(intersections
) < 2:
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
:
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)
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
:
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]):
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)
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)
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
):
295 def copyAttributes(dst
, src
):
296 for attribute
in dir(src
):
298 setattr(dst
, attribute
, getattr(src
, attribute
))
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:
314 solutions
.append([aMin
, aMax
, bMin
, bMax
])
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):
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
:
344 elif a2b1Dist
== minDist
:
347 elif a1b2Dist
== minDist
:
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'])
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])
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'):
380 if index
== otherIndex
or solutions
[otherIndex
][2] == float('inf'):
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')
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]))):
397 addCut(solution
[0], solution
[1])
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
):
410 for segment
in segments
:
411 if len(insertions
) > 0 and insertions
[-1][0] != segment
['spline']:
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))
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
):
430 newPoints
.append(points
[0]+(points
[1]-points
[0])*params
[0])
431 for index
, param
in enumerate(params
):
434 paramLeft
-= params
[index
-1]
436 if index
== len(params
)-1:
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]))
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:
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
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
525 for segment
in segments
:
526 spline
= segment
['spline']
527 if (spline
in groups
) == False:
529 group
= groups
[spline
]
530 group
.append(segment
)
531 for spline
in groups
:
532 subdivideBezierSegmentsOfSameSpline(groups
[spline
])
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
):
540 for spline
in splines
:
541 if spline
.type != 'BEZIER':
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
:
547 if not selection_only
or (current
.select_right_handle
and next
.select_left_handle
):
551 'endIndex': index
+1 if index
< len(spline
.bezier_points
)-1 else 0,
552 'beginPoint': current
,
558 def getSelectedSplines(include_bezier
, include_polygon
, allow_partial_selection
=False):
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
:
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
571 elif spline
.type == 'POLY':
572 if not include_polygon
:
574 for index
, point
in enumerate(spline
.points
):
575 if point
.select
== allow_partial_selection
:
576 selected
= allow_partial_selection
581 result
.append(spline
)
584 def addObject(type, name
):
586 data
= bpy
.data
.curves
.new(name
=name
, type='CURVE')
587 data
.dimensions
= '3D'
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
)
594 bpy
.context
.view_layer
.objects
.active
= 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
605 point
.weight_softbody
= weights
[index
]
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]
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'
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
633 points
[0].handle_left
+= new_co
-points
[0].co
634 points
[0].handle_right
= handle
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()
647 def polygonArcAt(center
, radius
, begin_angle
, angle
, step_angle
, include_ends
):
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
)
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
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)
668 y1
= (1.0-x0
)*(3.0-x0
)/(3.0*y0
)
670 Vector((x0
, -y0
, 0)),
671 Vector((x1
, -y1
, 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
)))
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()
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
)
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
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])
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
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))
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):
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
):
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:
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)
757 vertices
= vertices
[:i
]+[intersection
]+vertices
[j
:]
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):
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
])
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
)
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]])
783 for i
in range(0, len(segments
)+1):
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]
797 return addBezierSpline(bpy
.context
.object, spline
.use_cyclic_u
, vertices
)
799 def dogBone(spline
, radius
):
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
])
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
)
819 def discretizeCurve(spline
, step_angle
, samples
):
821 def handlePoint(prev_segment_points
, next_segment_points
, selected
, prev_tangent
, current_tangent
, next_tangent
, normal
, angle
, is_first
, is_last
):
824 if isSegmentLinear(next_segment_points
):
825 vertices
.append(next_segment_points
[3])
827 prev_tangent
= bezierTangentAt(next_segment_points
, 0).normalized()
828 for t
in range(1, samples
+1):
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
)
837 def bezierBooleanGeometry(splineA
, splineB
, operation
):
838 if not splineA
.use_cyclic_u
or not splineB
.use_cyclic_u
:
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':
854 for segmentA
in segmentsA
:
855 for segmentB
in segmentsB
:
856 intersections
.extend(segmentIntersection(segmentA
, segmentB
))
857 if len(intersections
) == 0:
859 bpy
.context
.object.data
.splines
.remove(splineA
)
861 bpy
.context
.object.data
.splines
.remove(splineB
)
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'])
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
885 collectCuts(cutsA
, segmentsA
, deletionFlagA
)
886 collectCuts(cutsB
, segmentsB
, deletionFlagB
)
889 for segment
in segmentsA
:
890 if segment
['deletionFlag'] == False:
891 beginIndex
= segment
['beginIndex']
893 for cut
in segment
['cuts']:
894 if cut
['deletionFlag'] == False:
895 beginIndex
= cut
['index']
904 current
= spline
.bezier_points
[index
]
905 vertices
.append([current
.handle_left
, current
.co
, current
.handle_right
])
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
:
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
]
923 next
.handle_right
= current_handle
925 next
.handle_left
= current_handle
926 if spline
== splineA
and index
== beginIndex
:
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
935 def truncateToFitBox(transform
, spline
, aabb
):
936 spline_points
= spline
.points
942 def terminateTrace(aux
):
943 if len(aux
['vertices']) > 0:
944 aux
['traces'].append((aux
['vertices'], 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
)
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
:
956 intersections
= lineAABBIntersection(begin
, transform
@end.co
.xyz
, aabb
)
957 if len(intersections
) == 2:
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
)
968 elif inside
and index
== len(spline_points
)-1 and spline
.use_cyclic_u
:
970 aux
['traces'][0] = (aux
['traces'][-1][0]+aux
['traces'][0][0], aux
['traces'][-1][1]+aux
['traces'][0][1])
975 def arrayModifier(splines
, offset
, count
, connect
, serpentine
):
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
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
:
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))
997 bpy
.ops
.curve
.switch_direction()
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
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
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')