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
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
29 normal
= dirBA
.cross(dirCB
)
30 lengthBA
= dirBA
.length
31 lengthCB
= dirCB
.length
32 lengthAC
= dirAC
.length
33 lengthN
= normal
.length
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])
54 for t
in range(0, samples
):
55 variance
+= ((circle
.center
-bezierPointAt(points
, (t
+1)/(samples
-1))).length
/circle
.radius
-1) ** 2
57 return None if variance
> tolerance
else circle
59 def areaOfPolygon(vertices
):
61 for index
, current
in enumerate(vertices
):
62 prev
= vertices
[index
-1]
63 area
+= (current
[0]+prev
[0])*(current
[1]-prev
[1])
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)
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):
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:
94 return (paramA
, paramB
, pointA
, pointB
)
96 def aabbOfPoints(points
):
97 min = Vector(points
[0])
98 max = Vector(points
[0])
100 for i
in range(0, 3):
101 if min[i
] > point
[i
]:
103 if 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
:
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
):
119 def lineAABBIntersection(lineBegin
, lineEnd
, aabb
):
121 for i
in range(0, 3):
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
):
129 point
= lineBegin
+param
*(lineEnd
-lineBegin
)
130 if isPointInAABB(point
, aabb
, 0.0, i
):
131 intersections
.append((param
, point
))
134 def bezierPointAt(points
, 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
):
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]]
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
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
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])
175 if abs(a
) > tolerance
: # Cubic
178 A
= (2*b
*b
-9*E2
)*b
+27*E3
180 C
= ((A
+cmath
.sqrt(A
*A
-4*B
*B
*B
))*0.5) ** (1/3)
182 for root
in cubic_roots_of_unity
:
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)))
189 for index
in range(len(roots
)-1, 0, -1):
190 if abs(roots
[index
-1]-roots
[index
]) < param_tolerance
:
193 elif abs(b
) > tolerance
: # Quadratic
197 disc
= math
.sqrt(disc
)
198 return [(-c
-disc
)/(2*b
), (-c
+disc
)/(2*b
)]
199 elif abs(c
) > tolerance
: # Linear
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
210 def areIntersectionsAdjacent(index
):
211 if len(intersections
) < 2:
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
:
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)
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
:
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]):
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)
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)
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
):
293 def copyAttributes(dst
, src
):
294 for attribute
in dir(src
):
296 setattr(dst
, attribute
, getattr(src
, attribute
))
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:
312 solutions
.append([aMin
, aMax
, bMin
, bMax
])
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):
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
:
342 elif a2b1Dist
== minDist
:
345 elif a1b2Dist
== minDist
:
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'])
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])
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'):
378 if index
== otherIndex
or solutions
[otherIndex
][2] == float('inf'):
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')
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]))):
395 addCut(solution
[0], solution
[1])
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
):
408 for segment
in segments
:
409 if len(insertions
) > 0 and insertions
[-1][0] != segment
['spline']:
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))
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
):
428 newPoints
.append(points
[0]+(points
[1]-points
[0])*params
[0])
429 for index
, param
in enumerate(params
):
432 paramLeft
-= params
[index
-1]
434 if index
== len(params
)-1:
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]))
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:
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
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
523 for segment
in segments
:
524 spline
= segment
['spline']
525 if (spline
in groups
) == False:
527 group
= groups
[spline
]
528 group
.append(segment
)
529 for spline
in groups
:
530 subdivideBezierSegmentsOfSameSpline(groups
[spline
])
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
):
538 for spline
in splines
:
539 if spline
.type != 'BEZIER':
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
:
545 if not selection_only
or (current
.select_right_handle
and next
.select_left_handle
):
549 'endIndex': index
+1 if index
< len(spline
.bezier_points
)-1 else 0,
550 'beginPoint': current
,
556 def getSelectedSplines(include_bezier
, include_polygon
, allow_partial_selection
=False):
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
:
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
569 elif spline
.type == 'POLY':
570 if not include_polygon
:
572 for index
, point
in enumerate(spline
.points
):
573 if point
.select
== allow_partial_selection
:
574 selected
= allow_partial_selection
579 result
.append(spline
)
582 def addObject(type, name
):
584 data
= bpy
.data
.curves
.new(name
=name
, type='CURVE')
585 data
.dimensions
= '3D'
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
)
592 bpy
.context
.view_layer
.objects
.active
= 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
603 point
.weight_softbody
= weights
[index
]
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]
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'
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
631 points
[0].handle_left
+= new_co
-points
[0].co
632 points
[0].handle_right
= handle
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()
645 def polygonArcAt(center
, radius
, begin_angle
, angle
, step_angle
, include_ends
):
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
)
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
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)
666 y1
= (1.0-x0
)*(3.0-x0
)/(3.0*y0
)
668 Vector((x0
, -y0
, 0)),
669 Vector((x1
, -y1
, 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
)))
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()
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
)
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
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])
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
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))
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):
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
):
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:
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)
755 vertices
= vertices
[:i
]+[intersection
]+vertices
[j
:]
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):
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
])
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
)
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]])
781 for i
in range(0, len(segments
)+1):
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]
795 return addBezierSpline(bpy
.context
.object, spline
.use_cyclic_u
, vertices
)
797 def dogBone(spline
, radius
):
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
])
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
)
817 def discretizeCurve(spline
, step_angle
, samples
):
819 def handlePoint(prev_segment_points
, next_segment_points
, selected
, prev_tangent
, current_tangent
, next_tangent
, normal
, angle
, is_first
, is_last
):
822 if isSegmentLinear(next_segment_points
):
823 vertices
.append(next_segment_points
[3])
825 prev_tangent
= bezierTangentAt(next_segment_points
, 0).normalized()
826 for t
in range(1, samples
+1):
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
)
835 def bezierBooleanGeometry(splineA
, splineB
, operation
):
836 if not splineA
.use_cyclic_u
or not splineB
.use_cyclic_u
:
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':
852 for segmentA
in segmentsA
:
853 for segmentB
in segmentsB
:
854 intersections
.extend(segmentIntersection(segmentA
, segmentB
))
855 if len(intersections
) == 0:
857 bpy
.context
.object.data
.splines
.remove(splineA
)
859 bpy
.context
.object.data
.splines
.remove(splineB
)
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'])
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
883 collectCuts(cutsA
, segmentsA
, deletionFlagA
)
884 collectCuts(cutsB
, segmentsB
, deletionFlagB
)
887 for segment
in segmentsA
:
888 if segment
['deletionFlag'] == False:
889 beginIndex
= segment
['beginIndex']
891 for cut
in segment
['cuts']:
892 if cut
['deletionFlag'] == False:
893 beginIndex
= cut
['index']
902 current
= spline
.bezier_points
[index
]
903 vertices
.append([current
.handle_left
, current
.co
, current
.handle_right
])
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
:
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
]
921 next
.handle_right
= current_handle
923 next
.handle_left
= current_handle
924 if spline
== splineA
and index
== beginIndex
:
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
933 def truncateToFitBox(transform
, spline
, aabb
):
934 spline_points
= spline
.points
940 def terminateTrace(aux
):
941 if len(aux
['vertices']) > 0:
942 aux
['traces'].append((aux
['vertices'], 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
)
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
:
954 intersections
= lineAABBIntersection(begin
, transform
@end.co
.xyz
, aabb
)
955 if len(intersections
) == 2:
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
)
966 elif inside
and index
== len(spline_points
)-1 and spline
.use_cyclic_u
:
968 aux
['traces'][0] = (aux
['traces'][-1][0]+aux
['traces'][0][0], aux
['traces'][-1][1]+aux
['traces'][0][1])
973 def arrayModifier(splines
, offset
, count
, connect
, serpentine
):
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
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
:
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))
995 bpy
.ops
.curve
.switch_direction()
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
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
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')