Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender-addons.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSpivak Vladimir (cwolf3d) <cwolf3d@gmail.com>2019-09-13 16:03:12 +0300
committerSpivak Vladimir (cwolf3d) <cwolf3d@gmail.com>2019-09-13 16:03:12 +0300
commit60e4580191805ace935fae06bf62ccd0037065b7 (patch)
tree3adb0ff1acbefee51993ab49a2ca313058890c13 /curve_tools/internal.py
parent40d73269bdb7280d1643c1eae4683c085cc9d71a (diff)
Curve Tools 2: move to release T65825
Diffstat (limited to 'curve_tools/internal.py')
-rw-r--r--curve_tools/internal.py958
1 files changed, 958 insertions, 0 deletions
diff --git a/curve_tools/internal.py b/curve_tools/internal.py
new file mode 100644
index 00000000..6741bb22
--- /dev/null
+++ b/curve_tools/internal.py
@@ -0,0 +1,958 @@
+# ***** GPL LICENSE BLOCK *****
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+# All rights reserved.
+#
+# ***** GPL LICENSE BLOCK *****
+
+import bpy, math, cmath
+from mathutils import Vector, Matrix
+from collections import namedtuple
+
+units = [
+ ('-', 'None', '1.0', 0),
+ ('px', 'Pixel', '1.0', 1),
+ ('m', 'Meter', '1.0', 2),
+ ('dm', 'Decimeter', '0.1', 3),
+ ('cm', 'Centimeter', '0.01', 4),
+ ('mm', 'Millimeter', '0.001', 5),
+ ('yd', 'Yard', '0.9144', 6),
+ ('ft', 'Foot', '0.3048', 7),
+ ('in', 'Inch', '0.0254', 8)
+]
+
+param_tollerance = 0.0001
+AABB = namedtuple('AxisAlignedBoundingBox', 'center dimensions')
+Plane = namedtuple('Plane', 'normal distance')
+Circle = namedtuple('Circle', 'orientation center radius')
+
+def circleOfTriangle(a, b, c):
+ # https://en.wikipedia.org/wiki/Circumscribed_circle#Cartesian_coordinates_from_cross-_and_dot-products
+ dirBA = a-b
+ dirCB = b-c
+ dirAC = c-a
+ normal = dirBA.cross(dirCB)
+ lengthBA = dirBA.length
+ lengthCB = dirCB.length
+ lengthAC = dirAC.length
+ lengthN = normal.length
+ if lengthN == 0:
+ return None
+ factor = -1/(2*lengthN*lengthN)
+ alpha = (dirBA@dirAC)*(lengthCB*lengthCB*factor)
+ beta = (dirBA@dirCB)*(lengthAC*lengthAC*factor)
+ gamma = (dirAC@dirCB)*(lengthBA*lengthBA*factor)
+ center = a*alpha+b*beta+c*gamma
+ radius = (lengthBA*lengthCB*lengthAC)/(2*lengthN)
+ tangent = (a-center).normalized()
+ orientation = Matrix.Identity(3)
+ orientation.col[2] = normal/lengthN
+ orientation.col[1] = (a-center).normalized()
+ orientation.col[0] = orientation.col[1].xyz.cross(orientation.col[2].xyz)
+ return Circle(orientation=orientation, center=center, radius=radius)
+
+def circleOfBezier(points, tollerance=0.000001, samples=16):
+ circle = circleOfTriangle(points[0], bezierPointAt(points, 0.5), points[3])
+ if circle == None:
+ return None
+ variance = 0
+ for t in range(0, samples):
+ variance += ((circle.center-bezierPointAt(points, (t+1)/(samples-1))).length/circle.radius-1) ** 2
+ variance /= samples
+ return None if variance > tollerance else circle
+
+def areaOfPolygon(vertices):
+ area = 0
+ for index, current in enumerate(vertices):
+ prev = vertices[index-1]
+ area += (current[0]+prev[0])*(current[1]-prev[1])
+ return area*0.5
+
+def linePointDistance(begin, dir, point):
+ return (point-begin).cross(dir.normalized()).length
+
+def linePlaneIntersection(origin, dir, plane):
+ det = dir@plane.normal
+ return float('nan') if det == 0 else (plane.distance-origin@plane.normal)/det
+
+def nearestPointOfLines(originA, dirA, originB, dirB, tollerance=0.0):
+ # https://en.wikipedia.org/wiki/Skew_lines#Nearest_Points
+ normal = dirA.cross(dirB)
+ normalA = dirA.cross(normal)
+ normalB = dirB.cross(normal)
+ divisorA = dirA@normalB
+ divisorB = dirB@normalA
+ if abs(divisorA) <= tollerance or abs(divisorB) <= tollerance:
+ return (float('nan'), float('nan'), None, None)
+ else:
+ paramA = (originB-originA)@normalB/divisorA
+ paramB = (originA-originB)@normalA/divisorB
+ return (paramA, paramB, originA+dirA*paramA, originB+dirB*paramB)
+
+def lineSegmentLineSegmentIntersection(beginA, endA, beginB, endB, tollerance=0.001):
+ dirA = endA-beginA
+ dirB = endB-beginB
+ intersection = nearestPointOfLines(beginA, dirA, beginB, dirB)
+ if math.isnan(intersection[0]) or (intersection[2]-intersection[3]).length > tollerance or \
+ intersection[0] < 0 or intersection[0] > 1 or intersection[1] < 0 or intersection[1] > 1:
+ return None
+ return intersection
+
+def aabbOfPoints(points):
+ min = Vector(points[0])
+ max = Vector(points[0])
+ for point in points:
+ for i in range(0, 3):
+ if min[i] > point[i]:
+ min[i] = point[i]
+ if max[i] < point[i]:
+ max[i] = point[i]
+ return AABB(center=(max+min)*0.5, dimensions=(max-min)*0.5)
+
+def aabbIntersectionTest(a, b, tollerance=0.0):
+ for i in range(0, 3):
+ if abs(a.center[i]-b.center[i]) > a.dimensions[i]+b.dimensions[i]+tollerance:
+ return False
+ return True
+
+def isPointInAABB(point, aabb, tollerance=0.0, ignore_axis=None):
+ for i in range(0, 3):
+ if i != ignore_axis and (point[i] < aabb.center[i]-aabb.dimensions[i]-tollerance or point[i] > aabb.center[i]+aabb.dimensions[i]+tollerance):
+ return False
+ return True
+
+def lineAABBIntersection(lineBegin, lineEnd, aabb):
+ intersections = []
+ for i in range(0, 3):
+ normal = [0, 0, 0]
+ normal = Vector(normal[0:i] + [1] + normal[i+1:])
+ for j in range(-1, 2, 2):
+ plane = Plane(normal=normal, distance=aabb.center[i]+j*aabb.dimensions[i])
+ param = linePlaneIntersection(lineBegin, lineEnd-lineBegin, plane)
+ if param < 0 or param > 1 or math.isnan(param):
+ continue
+ point = lineBegin+param*(lineEnd-lineBegin)
+ if isPointInAABB(point, aabb, 0.0, i):
+ intersections.append((param, point))
+ return intersections
+
+def bezierPointAt(points, t):
+ s = 1-t
+ return s*s*s*points[0] + 3*s*s*t*points[1] + 3*s*t*t*points[2] + t*t*t*points[3]
+
+def bezierTangentAt(points, t):
+ s = 1-t
+ return s*s*(points[1]-points[0])+2*s*t*(points[2]-points[1])+t*t*(points[3]-points[2])
+ # return s*s*points[0] + (s*s-2*s*t)*points[1] + (2*s*t-t*t)*points[2] + t*t*points[3]
+
+def bezierLength(points, beginT=0, endT=1, samples=1024):
+ # https://en.wikipedia.org/wiki/Arc_length#Finding_arc_lengths_by_integrating
+ vec = [points[1]-points[0], points[2]-points[1], points[3]-points[2]]
+ 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]]
+ factors = [
+ dot[0],
+ 4*(dot[1]-dot[0]),
+ 6*dot[0]+4*dot[3]+2*dot[2]-12*dot[1],
+ 12*dot[1]+4*(dot[4]-dot[0]-dot[2])-8*dot[3],
+ dot[0]+dot[5]+2*dot[2]+4*(dot[3]-dot[1]-dot[4])
+ ]
+ # https://en.wikipedia.org/wiki/Trapezoidal_rule
+ length = 0
+ prev_value = math.sqrt(factors[4]+factors[3]+factors[2]+factors[1]+factors[0])
+ for index in range(0, samples+1):
+ t = beginT+(endT-beginT)*index/samples
+ # value = math.sqrt(factors[4]*(t**4)+factors[3]*(t**3)+factors[2]*(t**2)+factors[1]*t+factors[0])
+ value = math.sqrt((((factors[4]*t+factors[3])*t+factors[2])*t+factors[1])*t+factors[0])
+ length += (prev_value+value)*0.5
+ prev_value = value
+ return length*3/samples
+
+# https://en.wikipedia.org/wiki/Root_of_unity
+# cubic_roots_of_unity = [cmath.rect(1, i/3*2*math.pi) for i in range(0, 3)]
+cubic_roots_of_unity = [complex(1, 0), complex(-1, math.sqrt(3))*0.5, complex(-1, -math.sqrt(3))*0.5]
+def bezierRoots(dists, tollerance=0.0001):
+ # https://en.wikipedia.org/wiki/Cubic_function
+ # y(t) = a*t^3 +b*t^2 +c*t^1 +d*t^0
+ a = 3*(dists[1]-dists[2])+dists[3]-dists[0]
+ b = 3*(dists[0]-2*dists[1]+dists[2])
+ c = 3*(dists[1]-dists[0])
+ d = dists[0]
+ if abs(a) > tollerance: # Cubic
+ E2 = a*c
+ E3 = a*a*d
+ A = (2*b*b-9*E2)*b+27*E3
+ B = b*b-3*E2
+ C = ((A+cmath.sqrt(A*A-4*B*B*B))*0.5) ** (1/3)
+ roots = []
+ for root in cubic_roots_of_unity:
+ root *= C
+ root = -1/(3*a)*(b+root+B/root)
+ if abs(root.imag) < tollerance and root.real > -param_tollerance and root.real < 1.0+param_tollerance:
+ roots.append(max(0.0, min(root.real, 1.0)))
+ # Remove doubles
+ roots.sort()
+ for index in range(len(roots)-1, 0, -1):
+ if abs(roots[index-1]-roots[index]) < param_tollerance:
+ roots.pop(index)
+ return roots
+ elif abs(b) > tollerance: # Quadratic
+ disc = c*c-4*b*d
+ if disc < 0:
+ return []
+ disc = math.sqrt(disc)
+ return [(-c-disc)/(2*b), (-c+disc)/(2*b)]
+ elif abs(c) > tollerance: # Linear
+ root = -d/c
+ return [root] if root >= 0.0 and root <= 1.0 else []
+ else: # Constant / Parallel
+ return [] if abs(d) > tollerance else float('inf')
+
+def xRaySplineIntersectionTest(spline, origin):
+ spline_points = spline.bezier_points if spline.type == 'BEZIER' else spline.points
+ cyclic_parallel_fix_flag = False
+ intersections = []
+
+ def areIntersectionsAdjacent(index):
+ if len(intersections) < 2:
+ return
+ prev = intersections[index-1]
+ current = intersections[index]
+ if prev[1] == current[0] and \
+ prev[2] > 1.0-param_tollerance and current[2] < param_tollerance and \
+ ((prev[3] < 0 and current[3] < 0) or (prev[3] > 0 and current[3] > 0)):
+ intersections.pop(index)
+
+ def appendIntersection(index, root, tangentY, intersectionX):
+ beginPoint = spline_points[index-1]
+ endPoint = spline_points[index]
+ if root == float('inf'): # Segment is parallel to ray
+ if index == 0 and spline.use_cyclic_u:
+ cyclic_parallel_fix_flag = True
+ if len(intersections) > 0 and intersections[-1][1] == beginPoint:
+ intersections[-1][1] = endPoint # Skip in adjacency test
+ elif intersectionX >= origin[0]:
+ intersections.append([beginPoint, endPoint, root, tangentY, intersectionX])
+ areIntersectionsAdjacent(len(intersections)-1)
+
+ if spline.type == 'BEZIER':
+ for index, endPoint in enumerate(spline.bezier_points):
+ if index == 0 and not spline.use_cyclic_u:
+ continue
+ beginPoint = spline_points[index-1]
+ points = (beginPoint.co, beginPoint.handle_right, endPoint.handle_left, endPoint.co)
+ roots = bezierRoots((points[0][1]-origin[1], points[1][1]-origin[1], points[2][1]-origin[1], points[3][1]-origin[1]))
+ if roots == float('inf'): # Intersection
+ appendIntersection(index, float('inf'), None, None)
+ else:
+ for root in roots:
+ appendIntersection(index, root, bezierTangentAt(points, root)[1], bezierPointAt(points, root)[0])
+ elif spline.type == 'POLY':
+ for index, endPoint in enumerate(spline.points):
+ if index == 0 and not spline.use_cyclic_u:
+ continue
+ beginPoint = spline_points[index-1]
+ points = (beginPoint.co, endPoint.co)
+ if (points[0][0] < origin[0] and points[1][0] < origin[0]) or \
+ (points[0][1] < origin[1] and points[1][1] < origin[1]) or \
+ (points[0][1] > origin[1] and points[1][1] > origin[1]):
+ continue
+ diff = points[1]-points[0]
+ height = origin[1]-points[0][1]
+ if diff[1] == 0: # Parallel
+ if height == 0: # Intersection
+ appendIntersection(index, float('inf'), None, None)
+ else: # Not parallel
+ root = height/diff[1]
+ appendIntersection(index, root, diff[1], points[0][0]+diff[0]*root)
+
+ if cyclic_parallel_fix_flag:
+ appendIntersection(0, float('inf'), None, None)
+ areIntersectionsAdjacent(0)
+ return intersections
+
+def isPointInSpline(point, spline):
+ return spline.use_cyclic_u and len(xRaySplineIntersectionTest(spline, point))%2 == 1
+
+def isSegmentLinear(points, tollerance=0.0001):
+ return 1.0-(points[1]-points[0]).normalized()@(points[3]-points[2]).normalized() < tollerance
+
+def bezierSegmentPoints(begin, end):
+ return [begin.co, begin.handle_right, end.handle_left, end.co]
+
+def deleteFromArray(item, array):
+ for index, current in enumerate(array):
+ if current is item:
+ array.pop(index)
+ break
+
+def copyAttributes(dst, src):
+ for attribute in dir(src):
+ try:
+ setattr(dst, attribute, getattr(src, attribute))
+ except:
+ pass
+
+def bezierSliceFromTo(points, minParam, maxParam):
+ fromP = bezierPointAt(points, minParam)
+ fromT = bezierTangentAt(points, minParam)
+ toP = bezierPointAt(points, maxParam)
+ toT = bezierTangentAt(points, maxParam)
+ paramDiff = maxParam-minParam
+ return [fromP, fromP+fromT*paramDiff, toP-toT*paramDiff, toP]
+
+def bezierIntersectionBroadPhase(solutions, pointsA, pointsB, aMin=0.0, aMax=1.0, bMin=0.0, bMax=1.0, depth=8, tollerance=0.001):
+ if aabbIntersectionTest(aabbOfPoints(bezierSliceFromTo(pointsA, aMin, aMax)), aabbOfPoints(bezierSliceFromTo(pointsB, bMin, bMax)), tollerance) == False:
+ return
+ if depth == 0:
+ solutions.append([aMin, aMax, bMin, bMax])
+ return
+ depth -= 1
+ aMid = (aMin+aMax)*0.5
+ bMid = (bMin+bMax)*0.5
+ bezierIntersectionBroadPhase(solutions, pointsA, pointsB, aMin, aMid, bMin, bMid, depth, tollerance)
+ bezierIntersectionBroadPhase(solutions, pointsA, pointsB, aMin, aMid, bMid, bMax, depth, tollerance)
+ bezierIntersectionBroadPhase(solutions, pointsA, pointsB, aMid, aMax, bMin, bMid, depth, tollerance)
+ bezierIntersectionBroadPhase(solutions, pointsA, pointsB, aMid, aMax, bMid, bMax, depth, tollerance)
+
+def bezierIntersectionNarrowPhase(broadPhase, pointsA, pointsB, tollerance=0.000001):
+ aMin = broadPhase[0]
+ aMax = broadPhase[1]
+ bMin = broadPhase[2]
+ bMax = broadPhase[3]
+ while (aMax-aMin > tollerance) or (bMax-bMin > tollerance):
+ aMid = (aMin+aMax)*0.5
+ bMid = (bMin+bMax)*0.5
+ a1 = bezierPointAt(pointsA, (aMin+aMid)*0.5)
+ a2 = bezierPointAt(pointsA, (aMid+aMax)*0.5)
+ b1 = bezierPointAt(pointsB, (bMin+bMid)*0.5)
+ b2 = bezierPointAt(pointsB, (bMid+bMax)*0.5)
+ a1b1Dist = (a1-b1).length
+ a2b1Dist = (a2-b1).length
+ a1b2Dist = (a1-b2).length
+ a2b2Dist = (a2-b2).length
+ minDist = min(a1b1Dist, a2b1Dist, a1b2Dist, a2b2Dist)
+ if a1b1Dist == minDist:
+ aMax = aMid
+ bMax = bMid
+ elif a2b1Dist == minDist:
+ aMin = aMid
+ bMax = bMid
+ elif a1b2Dist == minDist:
+ aMax = aMid
+ bMin = bMid
+ else:
+ aMin = aMid
+ bMin = bMid
+ return [aMin, bMin, minDist]
+
+def segmentIntersection(segmentA, segmentB, tollerance=0.001):
+ pointsA = bezierSegmentPoints(segmentA['beginPoint'], segmentA['endPoint'])
+ pointsB = bezierSegmentPoints(segmentB['beginPoint'], segmentB['endPoint'])
+ result = []
+ def addCut(paramA, paramB):
+ cutA = {'param': paramA, 'segment': segmentA}
+ cutB = {'param': paramB, 'segment': segmentB}
+ cutA['otherCut'] = cutB
+ cutB['otherCut'] = cutA
+ segmentA['cuts'].append(cutA)
+ segmentB['cuts'].append(cutB)
+ result.append([cutA, cutB])
+ if isSegmentLinear(pointsA) and isSegmentLinear(pointsB):
+ intersection = lineSegmentLineSegmentIntersection(pointsA[0], pointsA[3], pointsB[0], pointsB[3])
+ if intersection != None:
+ addCut(intersection[0], intersection[1])
+ return result
+ solutions = []
+ bezierIntersectionBroadPhase(solutions, pointsA, pointsB)
+ for index in range(0, len(solutions)):
+ solutions[index] = bezierIntersectionNarrowPhase(solutions[index], pointsA, pointsB)
+ for index in range(0, len(solutions)):
+ for otherIndex in range(0, len(solutions)):
+ if solutions[index][2] == float('inf'):
+ break
+ if index == otherIndex or solutions[otherIndex][2] == float('inf'):
+ continue
+ diffA = solutions[index][0]-solutions[otherIndex][0]
+ diffB = solutions[index][1]-solutions[otherIndex][1]
+ if diffA*diffA+diffB*diffB < 0.01:
+ if solutions[index][2] < solutions[otherIndex][2]:
+ solutions[otherIndex][2] = float('inf')
+ else:
+ solutions[index][2] = float('inf')
+ def areIntersectionsAdjacent(segmentA, segmentB, paramA, paramB):
+ return segmentA['endIndex'] == segmentB['beginIndex'] and paramA > 1-param_tollerance and paramB < param_tollerance
+ for solution in solutions:
+ if (solution[2] > tollerance) or \
+ (segmentA['spline'] == segmentB['spline'] and \
+ (areIntersectionsAdjacent(segmentA, segmentB, solution[0], solution[1]) or \
+ areIntersectionsAdjacent(segmentB, segmentA, solution[1], solution[0]))):
+ continue
+ addCut(solution[0], solution[1])
+ return result
+
+def bezierMultiIntersection(segments):
+ for index in range(0, len(segments)):
+ for otherIndex in range(index+1, len(segments)):
+ segmentIntersection(segments[index], segments[otherIndex])
+ prepareSegmentIntersections(segments)
+ subdivideBezierSegments(segments)
+
+def bezierSubivideAt(points, params):
+ if len(params) == 0:
+ return []
+ newPoints = []
+ newPoints.append(points[0]+(points[1]-points[0])*params[0])
+ for index, param in enumerate(params):
+ paramLeft = param
+ if index > 0:
+ paramLeft -= params[index-1]
+ paramRight = -param
+ if index == len(params)-1:
+ paramRight += 1.0
+ else:
+ paramRight += params[index+1]
+ point = bezierPointAt(points, param)
+ tangent = bezierTangentAt(points, param)
+ newPoints.append(point-tangent*paramLeft)
+ newPoints.append(point)
+ newPoints.append(point+tangent*paramRight)
+ newPoints.append(points[3]-(points[3]-points[2])*(1.0-params[-1]))
+ return newPoints
+
+def subdivideBezierSegment(segment):
+ # Blender only allows uniform subdivision. Use this method to subdivide at arbitrary params.
+ # NOTE: segment['cuts'] must be sorted by param
+ if len(segment['cuts']) == 0:
+ return
+
+ segment['beginPoint'] = segment['spline'].bezier_points[segment['beginIndex']]
+ segment['endPoint'] = segment['spline'].bezier_points[segment['endIndex']]
+ params = [cut['param'] for cut in segment['cuts']]
+ newPoints = bezierSubivideAt(bezierSegmentPoints(segment['beginPoint'], segment['endPoint']), params)
+ bpy.ops.curve.select_all(action='DESELECT')
+ segment['beginPoint'] = segment['spline'].bezier_points[segment['beginIndex']]
+ segment['beginPoint'].select_right_handle = True
+ segment['beginPoint'].handle_left_type = 'FREE'
+ segment['beginPoint'].handle_right_type = 'FREE'
+ segment['endPoint'] = segment['spline'].bezier_points[segment['endIndex']]
+ segment['endPoint'].select_left_handle = True
+ segment['endPoint'].handle_left_type = 'FREE'
+ segment['endPoint'].handle_right_type = 'FREE'
+
+ bpy.ops.curve.subdivide(number_cuts=len(params))
+ if segment['endIndex'] > 0:
+ segment['endIndex'] += len(params)
+ segment['beginPoint'] = segment['spline'].bezier_points[segment['beginIndex']]
+ segment['endPoint'] = segment['spline'].bezier_points[segment['endIndex']]
+ segment['beginPoint'].select_right_handle = False
+ segment['beginPoint'].handle_right = newPoints[0]
+ segment['endPoint'].select_left_handle = False
+ segment['endPoint'].handle_left = newPoints[-1]
+
+ for index, cut in enumerate(segment['cuts']):
+ cut['index'] = segment['beginIndex']+1+index
+ newPoint = segment['spline'].bezier_points[cut['index']]
+ newPoint.handle_left_type = 'FREE'
+ newPoint.handle_right_type = 'FREE'
+ newPoint.select_left_handle = False
+ newPoint.select_control_point = False
+ newPoint.select_right_handle = False
+ newPoint.handle_left = newPoints[index*3+1]
+ newPoint.co = newPoints[index*3+2]
+ newPoint.handle_right = newPoints[index*3+3]
+
+def prepareSegmentIntersections(segments):
+ def areCutsAdjacent(cutA, cutB):
+ return cutA['segment']['beginIndex'] == cutB['segment']['endIndex'] and \
+ cutA['param'] < param_tollerance and cutB['param'] > 1.0-param_tollerance
+ for segment in segments:
+ segment['cuts'].sort(key=(lambda cut: cut['param']))
+ for index in range(len(segment['cuts'])-1, 0, -1):
+ prev = segment['cuts'][index-1]
+ current = segment['cuts'][index]
+ if abs(prev['param']-current['param']) < param_tollerance and \
+ prev['otherCut']['segment']['spline'] == current['otherCut']['segment']['spline'] and \
+ (areCutsAdjacent(prev['otherCut'], current['otherCut']) or \
+ areCutsAdjacent(current['otherCut'], prev['otherCut'])):
+ deleteFromArray(prev['otherCut'], prev['otherCut']['segment']['cuts'])
+ deleteFromArray(current['otherCut'], current['otherCut']['segment']['cuts'])
+ segment['cuts'].pop(index-1 if current['otherCut']['param'] < param_tollerance else index)
+ current = segment['cuts'][index-1]['otherCut']
+ current['segment']['extraCut'] = current
+
+def subdivideBezierSegmentsOfSameSpline(segments):
+ # NOTE: segment['cuts'] must be sorted by param
+ indexOffset = 0
+ for segment in segments:
+ segment['beginIndex'] += indexOffset
+ if segment['endIndex'] > 0:
+ segment['endIndex'] += indexOffset
+ subdivideBezierSegment(segment)
+ indexOffset += len(segment['cuts'])
+ for segment in segments:
+ segment['beginPoint'] = segment['spline'].bezier_points[segment['beginIndex']]
+ segment['endPoint'] = segment['spline'].bezier_points[segment['endIndex']]
+
+def subdivideBezierSegments(segments):
+ # NOTE: segment['cuts'] must be sorted by param
+ groups = {}
+ for segment in segments:
+ spline = segment['spline']
+ if (spline in groups) == False:
+ groups[spline] = []
+ group = groups[spline]
+ group.append(segment)
+ for spline in groups:
+ subdivideBezierSegmentsOfSameSpline(groups[spline])
+
+def curveObject():
+ obj = bpy.context.object
+ return obj if obj != None and obj.type == 'CURVE' and obj.mode == 'EDIT' else None
+
+def bezierSegments(splines, selection_only):
+ segments = []
+ for spline in splines:
+ if spline.type != 'BEZIER':
+ continue
+ for index, current in enumerate(spline.bezier_points):
+ next = spline.bezier_points[(index+1) % len(spline.bezier_points)]
+ if next == spline.bezier_points[0] and not spline.use_cyclic_u:
+ continue
+ if not selection_only or (current.select_right_handle and next.select_left_handle):
+ segments.append({
+ 'spline': spline,
+ 'beginIndex': index,
+ 'endIndex': index+1 if index < len(spline.bezier_points)-1 else 0,
+ 'beginPoint': current,
+ 'endPoint': next,
+ 'cuts': []
+ })
+ return segments
+
+def getSelectedSplines(include_bezier, include_polygon, allow_partial_selection=False):
+ result = []
+ for spline in bpy.context.object.data.splines:
+ selected = not allow_partial_selection
+ if spline.type == 'BEZIER':
+ if not include_bezier:
+ continue
+ for index, point in enumerate(spline.bezier_points):
+ if point.select_left_handle == allow_partial_selection or \
+ point.select_control_point == allow_partial_selection or \
+ point.select_right_handle == allow_partial_selection:
+ selected = allow_partial_selection
+ break
+ elif spline.type == 'POLY':
+ if not include_polygon:
+ continue
+ for index, point in enumerate(spline.points):
+ if point.select == allow_partial_selection:
+ selected = allow_partial_selection
+ break
+ else:
+ continue
+ if selected:
+ result.append(spline)
+ return result
+
+def addObject(type, name):
+ bpy.ops.object.select_all(action='DESELECT')
+ if type == 'CURVE':
+ data = bpy.data.curves.new(name=name, type='CURVE')
+ data.dimensions = '3D'
+ elif type == 'MESH':
+ data = bpy.data.meshes.new(name=name, type='MESH')
+ obj = bpy.data.objects.new(name, data)
+ obj.location = bpy.context.scene.cursor.location
+ bpy.context.scene.collection.objects.link(obj)
+ obj.select_set(True)
+ bpy.context.view_layer.objects.active = obj
+ return obj
+
+def addPolygonSpline(obj, cyclic, vertices, weights=None, select=False):
+ spline = obj.data.splines.new(type='POLY')
+ spline.use_cyclic_u = cyclic
+ spline.points.add(len(vertices)-1)
+ for index, point in enumerate(spline.points):
+ point.co.xyz = vertices[index]
+ point.select = select
+ if weights:
+ point.weight_softbody = weights[index]
+ return spline
+
+def addBezierSpline(obj, cyclic, vertices, weights=None, select=False):
+ spline = obj.data.splines.new(type='BEZIER')
+ spline.use_cyclic_u = cyclic
+ spline.bezier_points.add(len(vertices)-1)
+ for index, point in enumerate(spline.bezier_points):
+ point.handle_left = vertices[index][0]
+ point.co = vertices[index][1]
+ point.handle_right = vertices[index][2]
+ if weights:
+ point.weight_softbody = weights[index]
+ point.select_left_handle = select
+ point.select_control_point = select
+ point.select_right_handle = select
+ if isSegmentLinear([vertices[index-1][1], vertices[index-1][2], vertices[index][0], vertices[index][1]]):
+ spline.bezier_points[index-1].handle_right_type = 'VECTOR'
+ point.handle_left_type = 'VECTOR'
+ return spline
+
+def polygonArcAt(center, radius, begin_angle, angle, step_angle, include_ends):
+ vertices = []
+ circle_samples = math.ceil(abs(angle)/step_angle)
+ for t in (range(0, circle_samples+1) if include_ends else range(1, circle_samples)):
+ t = begin_angle+angle*t/circle_samples
+ normal = Vector((math.cos(t), math.sin(t), 0))
+ vertices.append(center+normal*radius)
+ return vertices
+
+def bezierArcAt(tangent, normal, center, radius, angle, tollerance=0.99999):
+ transform = Matrix.Identity(4)
+ transform.col[0].xyz = tangent.cross(normal)*radius
+ transform.col[1].xyz = tangent*radius
+ transform.col[2].xyz = normal*radius
+ transform.col[3].xyz = center
+ segments = []
+ segment_count = math.ceil(abs(angle)/(math.pi*0.5)*tollerance)
+ angle /= segment_count
+ x0 = math.cos(angle*0.5)
+ y0 = math.sin(angle*0.5)
+ x1 = (4.0-x0)/3.0
+ y1 = (1.0-x0)*(3.0-x0)/(3.0*y0)
+ points = [
+ Vector((x0, -y0, 0)),
+ Vector((x1, -y1, 0)),
+ Vector((x1, y1, 0)),
+ Vector((x0, y0, 0))
+ ]
+ for i in range(0, segment_count):
+ rotation = Matrix.Rotation((i+0.5)*angle, 4, 'Z')
+ segments.append(list(map(lambda v: transform@(rotation@v), points)))
+ return segments
+
+def iterateSpline(spline, callback):
+ spline_points = spline.bezier_points if spline.type == 'BEZIER' else spline.points
+ for index, spline_point in enumerate(spline_points):
+ prev = spline_points[index-1]
+ current = spline_points[index]
+ next = spline_points[(index+1)%len(spline_points)]
+ if spline.type == 'BEZIER':
+ selected = current.select_control_point
+ prev_segment_points = bezierSegmentPoints(prev, current)
+ next_segment_points = bezierSegmentPoints(current, next)
+ prev_tangent = (prev_segment_points[3]-prev_segment_points[2]).normalized()
+ current_tangent = (next_segment_points[1]-next_segment_points[0]).normalized()
+ next_tangent = (next_segment_points[3]-next_segment_points[2]).normalized()
+ else:
+ selected = current.select
+ prev_segment_points = [prev.co.xyz, None, None, current.co.xyz]
+ next_segment_points = [current.co.xyz, None, None, next.co.xyz]
+ prev_tangent = (prev_segment_points[3]-prev_segment_points[0]).normalized()
+ current_tangent = next_tangent = (next_segment_points[3]-next_segment_points[0]).normalized()
+ normal = prev_tangent.cross(current_tangent).normalized()
+ angle = prev_tangent@current_tangent
+ angle = 0 if abs(angle-1.0) < 0.0001 else math.acos(angle)
+ is_first = (index == 0) and not spline.use_cyclic_u
+ is_last = (index == len(spline_points)-1) and not spline.use_cyclic_u
+ callback(prev_segment_points, next_segment_points, selected, prev_tangent, current_tangent, next_tangent, normal, angle, is_first, is_last)
+ return spline_points
+
+def offsetPolygonOfSpline(spline, offset, step_angle, round_line_join, bezier_samples=128, tollerance=0.000001):
+ def offsetVertex(position, tangent):
+ normal = Vector((-tangent[1], tangent[0], 0))
+ return position+normal*offset
+ vertices = []
+ def handlePoint(prev_segment_points, next_segment_points, selected, prev_tangent, current_tangent, next_tangent, normal, angle, is_first, is_last):
+ sign = math.copysign(1, normal[2])
+ angle *= sign
+ if is_last:
+ return
+ is_protruding = (abs(angle) > tollerance and abs(offset) > tollerance)
+ if is_protruding and not is_first and sign != math.copysign(1, offset): # Convex Corner
+ if round_line_join:
+ begin_angle = math.atan2(prev_tangent[1], prev_tangent[0])+math.pi*0.5
+ vertices.extend(polygonArcAt(next_segment_points[0], offset, begin_angle, angle, step_angle, False))
+ else:
+ distance = offset*math.tan(angle*0.5)
+ vertices.append(offsetVertex(next_segment_points[0], current_tangent)+current_tangent*distance)
+ if is_protruding or is_first:
+ vertices.append(offsetVertex(next_segment_points[0], current_tangent))
+ if spline.type == 'POLY' or isSegmentLinear(next_segment_points):
+ vertices.append(offsetVertex(next_segment_points[3], next_tangent))
+ else: # Trace Bezier Segment
+ prev_tangent = bezierTangentAt(next_segment_points, 0).normalized()
+ for t in range(1, bezier_samples+1):
+ t /= bezier_samples
+ tangent = bezierTangentAt(next_segment_points, t).normalized()
+ if t == 1 or math.acos(min(max(-1, prev_tangent@tangent), 1)) >= step_angle:
+ vertices.append(offsetVertex(bezierPointAt(next_segment_points, t), tangent))
+ prev_tangent = tangent
+ spline_points = iterateSpline(spline, handlePoint)
+
+ # Solve Self Intersections
+ original_area = areaOfPolygon([point.co for point in spline_points])
+ sign = -1 if offset < 0 else 1
+ i = (0 if spline.use_cyclic_u else 1)
+ while i < len(vertices):
+ j = i+2
+ while j < len(vertices) - (0 if i > 0 else 1):
+ intersection = lineSegmentLineSegmentIntersection(vertices[i-1], vertices[i], vertices[j-1], vertices[j])
+ if intersection == None:
+ j += 1
+ continue
+ intersection = (intersection[2]+intersection[3])*0.5
+ areaInner = sign*areaOfPolygon([intersection, vertices[i], vertices[j-1]])
+ areaOuter = sign*areaOfPolygon([intersection, vertices[j], vertices[i-1]])
+ if areaInner > areaOuter:
+ vertices = vertices[i:j]+[intersection]
+ i = (0 if spline.use_cyclic_u else 1)
+ else:
+ vertices = vertices[:i]+[intersection]+vertices[j:]
+ j = i+2
+ i += 1
+ new_area = areaOfPolygon(vertices)
+ return [vertices] if original_area*new_area >= 0 else []
+
+def filletSpline(spline, radius, chamfer_mode, limit_half_way, tollerance=0.0001):
+ vertices = []
+ distance_limit_factor = 0.5 if limit_half_way else 1.0
+ def handlePoint(prev_segment_points, next_segment_points, selected, prev_tangent, current_tangent, next_tangent, normal, angle, is_first, is_last):
+ 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)
+ if not selected or is_first or is_last or angle == 0 or distance == 0 or \
+ (spline.type == 'BEZIER' and not (isSegmentLinear(prev_segment_points) and isSegmentLinear(next_segment_points))):
+ prev_handle = next_segment_points[0] if is_first else prev_segment_points[2] if spline.type == 'BEZIER' else prev_segment_points[0]
+ next_handle = next_segment_points[0] if is_last else next_segment_points[1] if spline.type == 'BEZIER' else next_segment_points[3]
+ vertices.append([prev_handle, next_segment_points[0], next_handle])
+ return
+ tan_factor = math.tan(angle*0.5)
+ offset = min(radius, distance/tan_factor)
+ distance = offset*tan_factor
+ circle_center = next_segment_points[0]+normal.cross(prev_tangent)*offset-prev_tangent*distance
+ segments = bezierArcAt(prev_tangent, normal, circle_center, offset, angle)
+ if chamfer_mode:
+ vertices.append([prev_segment_points[0], segments[0][0], segments[-1][3]])
+ vertices.append([segments[0][0], segments[-1][3], next_segment_points[3]])
+ else:
+ for i in range(0, len(segments)+1):
+ vertices.append([
+ segments[i-1][2] if i > 0 else prev_segment_points[0],
+ segments[i][0] if i < len(segments) else segments[i-1][3],
+ segments[i][1] if i < len(segments) else next_segment_points[3]
+ ])
+ iterateSpline(spline, handlePoint)
+ i = 0 if spline.use_cyclic_u else 1
+ while(i < len(vertices)):
+ if (vertices[i-1][1]-vertices[i][1]).length < tollerance:
+ vertices[i-1][2] = vertices[i][2]
+ del vertices[i]
+ else:
+ i = i+1
+ return addBezierSpline(bpy.context.object, spline.use_cyclic_u, vertices)
+
+def discretizeCurve(spline, step_angle, samples):
+ vertices = []
+ def handlePoint(prev_segment_points, next_segment_points, selected, prev_tangent, current_tangent, next_tangent, normal, angle, is_first, is_last):
+ if is_last:
+ return
+ if isSegmentLinear(next_segment_points):
+ vertices.append(next_segment_points[3])
+ else:
+ prev_tangent = bezierTangentAt(next_segment_points, 0).normalized()
+ for t in range(1, samples+1):
+ t /= samples
+ tangent = bezierTangentAt(next_segment_points, t).normalized()
+ if t == 1 or math.acos(min(max(-1, prev_tangent@tangent), 1)) >= step_angle:
+ vertices.append(bezierPointAt(next_segment_points, t))
+ prev_tangent = tangent
+ iterateSpline(spline, handlePoint)
+ return vertices
+
+def bezierBooleanGeometry(splineA, splineB, operation):
+ if not splineA.use_cyclic_u or not splineB.use_cyclic_u:
+ return False
+ segmentsA = bezierSegments([splineA], False)
+ segmentsB = bezierSegments([splineB], False)
+
+ deletionFlagA = isPointInSpline(splineA.bezier_points[0].co, splineB)
+ deletionFlagB = isPointInSpline(splineB.bezier_points[0].co, splineA)
+ if operation == 'DIFFERENCE':
+ deletionFlagB = not deletionFlagB
+ elif operation == 'INTERSECTION':
+ deletionFlagA = not deletionFlagA
+ deletionFlagB = not deletionFlagB
+ elif operation != 'UNION':
+ return False
+
+ intersections = []
+ for segmentA in segmentsA:
+ for segmentB in segmentsB:
+ intersections.extend(segmentIntersection(segmentA, segmentB))
+ if len(intersections) == 0:
+ if deletionFlagA:
+ bpy.context.object.data.splines.remove(splineA)
+ if deletionFlagB:
+ bpy.context.object.data.splines.remove(splineB)
+ return True
+
+ prepareSegmentIntersections(segmentsA)
+ prepareSegmentIntersections(segmentsB)
+ subdivideBezierSegmentsOfSameSpline(segmentsA)
+ subdivideBezierSegmentsOfSameSpline(segmentsB)
+
+ def collectCuts(cuts, segments, deletionFlag):
+ for segmentIndex, segment in enumerate(segments):
+ if 'extraCut' in segment:
+ deletionFlag = not deletionFlag
+ segment['extraCut']['index'] = segment['beginIndex']
+ segment['extraCut']['deletionFlag'] = deletionFlag
+ cuts.append(segment['extraCut'])
+ else:
+ cuts.append(None)
+ cuts.extend(segments[segmentIndex]['cuts'])
+ segment['deletionFlag'] = deletionFlag
+ for cutIndex, cut in enumerate(segment['cuts']):
+ deletionFlag = not deletionFlag
+ cut['deletionFlag'] = deletionFlag
+ cutsA = []
+ cutsB = []
+ collectCuts(cutsA, segmentsA, deletionFlagA)
+ collectCuts(cutsB, segmentsB, deletionFlagB)
+
+ beginIndex = 0
+ for segment in segmentsA:
+ if segment['deletionFlag'] == False:
+ beginIndex = segment['beginIndex']
+ break
+ for cut in segment['cuts']:
+ if cut['deletionFlag'] == False:
+ beginIndex = cut['index']
+ break
+
+ cuts = cutsA
+ spline = splineA
+ index = beginIndex
+ backward = False
+ vertices = []
+ while True:
+ current = spline.bezier_points[index]
+ vertices.append([current.handle_left, current.co, current.handle_right])
+ if backward:
+ current.handle_left, current.handle_right = current.handle_right.copy(), current.handle_left.copy()
+ index += len(spline.bezier_points)-1 if backward else 1
+ index %= len(spline.bezier_points)
+ if spline == splineA and index == beginIndex:
+ break
+
+ cut = cuts[index]
+ if cut != None:
+ current = spline.bezier_points[index]
+ current_handle = current.handle_right if backward else current.handle_left
+ spline = splineA if spline == splineB else splineB
+ cuts = cutsA if spline == splineA else cutsB
+ index = cut['otherCut']['index']
+ backward = cut['otherCut']['deletionFlag']
+ next = spline.bezier_points[index]
+ if backward:
+ next.handle_right = current_handle
+ else:
+ next.handle_left = current_handle
+ if spline == splineA and index == beginIndex:
+ break
+
+ spline = addBezierSpline(bpy.context.object, True, vertices)
+ bpy.context.object.data.splines.remove(splineA)
+ bpy.context.object.data.splines.remove(splineB)
+ bpy.context.object.data.splines.active = spline
+ return True
+
+def truncateToFitBox(transform, spline, aabb):
+ spline_points = spline.points
+ aux = {
+ 'traces': [],
+ 'vertices': [],
+ 'weights': []
+ }
+ def terminateTrace(aux):
+ if len(aux['vertices']) > 0:
+ aux['traces'].append((aux['vertices'], aux['weights']))
+ aux['vertices'] = []
+ aux['weights'] = []
+ for index, point in enumerate(spline_points):
+ begin = transform@point.co.xyz
+ end = spline_points[(index+1)%len(spline_points)]
+ inside = isPointInAABB(begin, aabb)
+ if inside:
+ aux['vertices'].append(begin)
+ aux['weights'].append(point.weight_softbody)
+ if index == len(spline_points)-1 and not spline.use_cyclic_u:
+ break
+ intersections = lineAABBIntersection(begin, transform@end.co.xyz, aabb)
+ if len(intersections) == 2:
+ terminateTrace(aux)
+ aux['traces'].append((
+ [intersections[0][1], intersections[1][1]],
+ [end.weight_softbody, end.weight_softbody]
+ ))
+ elif len(intersections) == 1:
+ aux['vertices'].append(intersections[0][1])
+ aux['weights'].append(end.weight_softbody)
+ if inside:
+ terminateTrace(aux)
+ elif inside and index == len(spline_points)-1 and spline.use_cyclic_u:
+ terminateTrace(aux)
+ aux['traces'][0] = (aux['traces'][-1][0]+aux['traces'][0][0], aux['traces'][-1][1]+aux['traces'][0][1])
+ aux['traces'].pop()
+ terminateTrace(aux)
+ return aux['traces']
+
+def arrayModifier(splines, offset, count, connect, serpentine):
+ if connect:
+ for spline in splines:
+ if spline.use_cyclic_u:
+ spline.use_cyclic_u = False
+ points = spline.points if spline.type == 'POLY' else spline.bezier_points
+ points.add(1)
+ copyAttributes(points[-1], points[0])
+ bpy.ops.curve.select_all(action='DESELECT')
+ for spline in splines:
+ if spline.type == 'BEZIER':
+ for point in spline.bezier_points:
+ point.select_left_handle = point.select_control_point = point.select_right_handle = True
+ elif spline.type == 'POLY':
+ for point in spline.points:
+ point.select = True
+ splines_at_layer = [splines]
+ for i in range(1, count):
+ bpy.ops.curve.duplicate()
+ bpy.ops.transform.translate(value=offset)
+ splines_at_layer.append(getSelectedSplines(True, True))
+ if serpentine:
+ bpy.ops.curve.switch_direction()
+ if connect:
+ for i in range(1, count):
+ prev_layer = splines_at_layer[i-1]
+ next_layer = splines_at_layer[i]
+ for j in range(0, len(next_layer)):
+ bpy.ops.curve.select_all(action='DESELECT')
+ if prev_layer[j].type == 'POLY':
+ prev_layer[j].points[-1].select = True
+ else:
+ prev_layer[j].bezier_points[-1].select_control_point = True
+ if next_layer[j].type == 'POLY':
+ next_layer[j].points[0].select = True
+ else:
+ next_layer[j].bezier_points[0].select_control_point = True
+ bpy.ops.curve.make_segment()
+ bpy.ops.curve.select_all(action='DESELECT')