Newer
Older
#
#
# This Blender add-on assigns one or more Bezier Curves as shape keys to another
# Bezier Curve
#
# Supported Blender Version: 2.80 Beta
#
# Copyright (C) 2019 Shrinivas Kulkarni
#
Shrinivas Kulkarni
committed
# License: GPL-3.0 (https://github.com/Shriinivas/assignshapekey/blob/master/LICENSE)
#
import bpy, bmesh, bgl, gpu
from gpu_extras.batch import batch_for_shader
Shrinivas Kulkarni
committed
from bpy.props import BoolProperty, EnumProperty, StringProperty
from collections import OrderedDict
from mathutils import Vector
from math import sqrt, floor
from functools import cmp_to_key
Shrinivas Kulkarni
committed
from bpy.types import Panel, Operator, AddonPreferences
bl_info = {
"name": "Assign Shape Keys",
"author": "Shrinivas Kulkarni",
"version": (1, 0, 0),
Shrinivas Kulkarni
committed
"location": "View 3D > Sidebar > Edit Tab",
"description": "Assigns one or more Bezier curves as shape keys to another Bezier curve",
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
"wiki_url": "https://github.com/Shriinivas/assignshapekey/blob/master/README.md",
"blender": (2, 80, 0),
}
matchList = [('vCnt', 'Vertex Count', 'Match by vertex count'),
('bbArea', 'Area', 'Match by surface area of the bounding box'), \
('bbHeight', 'Height', 'Match by bounding box height'), \
('bbWidth', 'Width', 'Match by bounding box width'),
('bbDepth', 'Depth', 'Match by bounding box depth'),
('minX', 'Min X', 'Match by bounding bon Min X'),
('maxX', 'Max X', 'Match by bounding bon Max X'),
('minY', 'Min Y', 'Match by bounding bon Min Y'),
('maxY', 'Max Y', 'Match by bounding bon Max Y'),
('minZ', 'Min Z', 'Match by bounding bon Min Z'),
('maxZ', 'Max Z', 'Match by bounding bon Max Z')]
DEF_ERR_MARGIN = 0.0001
def isBezier(obj):
return obj.type == 'CURVE' and len(obj.data.splines) > 0 \
and obj.data.splines[0].type == 'BEZIER'
#Avoid errors due to floating point conversions/comparisons
#TODO: return -1, 0, 1
def floatCmpWithMargin(float1, float2, margin = DEF_ERR_MARGIN):
return abs(float1 - float2) < margin
def vectCmpWithMargin(v1, v2, margin = DEF_ERR_MARGIN):
return all(floatCmpWithMargin(v1[i], v2[i], margin) for i in range(0, len(v1)))
class Segment():
#pts[0] - start, pts[1] - ctrl1, pts[2] - ctrl2, , pts[3] - end
def pointAtT(pts, t):
return pts[0] + t * (3 * (pts[1] - pts[0]) +
t* (3 * (pts[0] + pts[2]) - 6 * pts[1] +
t * (-pts[0] + 3 * (pts[1] - pts[2]) + pts[3])))
def getSegLenRecurs(pts, start, end, t1 = 0, t2 = 1, error = DEF_ERR_MARGIN):
t1_5 = (t1 + t2)/2
mid = Segment.pointAtT(pts, t1_5)
l = (end - start).length
l2 = (mid - start).length + (end - mid).length
if (l2 - l > error):
return (Segment.getSegLenRecurs(pts, start, mid, t1, t1_5, error) +
Segment.getSegLenRecurs(pts, mid, end, t1_5, t2, error))
return l2
def __init__(self, start, ctrl1, ctrl2, end):
self.start = start
self.ctrl1 = ctrl1
self.ctrl2 = ctrl2
self.end = end
pts = [start, ctrl1, ctrl2, end]
self.length = Segment.getSegLenRecurs(pts, start, end)
#see https://stackoverflow.com/questions/878862/drawing-part-of-a-b%c3%a9zier-curve-by-reusing-a-basic-b%c3%a9zier-curve-function/879213#879213
def partialSeg(self, t0, t1):
pts = [self.start, self.ctrl1, self.ctrl2, self.end]
if(t0 > t1):
tt = t1
t1 = t0
t0 = tt
#Let's make at least the line segments of predictable length :)
if(pts[0] == pts[1] and pts[2] == pts[3]):
pt0 = Vector([(1 - t0) * pts[0][i] + t0 * pts[2][i] for i in range(0, 3)])
pt1 = Vector([(1 - t1) * pts[0][i] + t1 * pts[2][i] for i in range(0, 3)])
return Segment(pt0, pt0, pt1, pt1)
u0 = 1.0 - t0
u1 = 1.0 - t1
qa = [pts[0][i]*u0*u0 + pts[1][i]*2*t0*u0 + pts[2][i]*t0*t0 for i in range(0, 3)]
qb = [pts[0][i]*u1*u1 + pts[1][i]*2*t1*u1 + pts[2][i]*t1*t1 for i in range(0, 3)]
qc = [pts[1][i]*u0*u0 + pts[2][i]*2*t0*u0 + pts[3][i]*t0*t0 for i in range(0, 3)]
qd = [pts[1][i]*u1*u1 + pts[2][i]*2*t1*u1 + pts[3][i]*t1*t1 for i in range(0, 3)]
pta = Vector([qa[i]*u0 + qc[i]*t0 for i in range(0, 3)])
ptb = Vector([qa[i]*u1 + qc[i]*t1 for i in range(0, 3)])
ptc = Vector([qb[i]*u0 + qd[i]*t0 for i in range(0, 3)])
ptd = Vector([qb[i]*u1 + qd[i]*t1 for i in range(0, 3)])
return Segment(pta, ptb, ptc, ptd)
#see https://stackoverflow.com/questions/24809978/calculating-the-bounding-box-of-cubic-bezier-curve
#(3 D - 9 C + 9 B - 3 A) t^2 + (6 A - 12 B + 6 C) t + 3 (B - A)
#pts[0] - start, pts[1] - ctrl1, pts[2] - ctrl2, , pts[3] - end
#TODO: Return Vectors to make world space calculations consistent
def bbox(self, mw = None):
def evalBez(AA, BB, CC, DD, t):
return AA * (1 - t) * (1 - t) * (1 - t) + \
3 * BB * t * (1 - t) * (1 - t) + \
3 * CC * t * t * (1 - t) + \
DD * t * t * t
A = self.start
B = self.ctrl1
C = self.ctrl2
D = self.end
if(mw != None):
A = mw @ A
B = mw @ B
C = mw @ C
D = mw @ D
MINXYZ = [min([A[i], D[i]]) for i in range(0, 3)]
MAXXYZ = [max([A[i], D[i]]) for i in range(0, 3)]
leftBotBack_rgtTopFront = [MINXYZ, MAXXYZ]
a = [3 * D[i] - 9 * C[i] + 9 * B[i] - 3 * A[i] for i in range(0, 3)]
b = [6 * A[i] - 12 * B[i] + 6 * C[i] for i in range(0, 3)]
c = [3 * (B[i] - A[i]) for i in range(0, 3)]
solnsxyz = []
for i in range(0, 3):
solns = []
if(a[i] == 0):
if(b[i] == 0):
solns.append(0)#Independent of t so lets take the starting pt
else:
solns.append(c[i] / b[i])
else:
rootFact = b[i] * b[i] - 4 * a[i] * c[i]
if(rootFact >=0 ):
#Two solutions with + and - sqrt
solns.append((-b[i] + sqrt(rootFact)) / (2 * a[i]))
solns.append((-b[i] - sqrt(rootFact)) / (2 * a[i]))
solnsxyz.append(solns)
for i, soln in enumerate(solnsxyz):
for j, t in enumerate(soln):
if(t < 1 and t > 0):
co = evalBez(A[i], B[i], C[i], D[i], t)
if(co < leftBotBack_rgtTopFront[0][i]):
leftBotBack_rgtTopFront[0][i] = co
if(co > leftBotBack_rgtTopFront[1][i]):
leftBotBack_rgtTopFront[1][i] = co
return leftBotBack_rgtTopFront
class Part():
def __init__(self, parent, segs, isClosed):
self.parent = parent
self.segs = segs
#use_cyclic_u
self.isClosed = isClosed
#Indicates if this should be closed based on its counterparts in other paths
self.toClose = isClosed
self.length = sum(seg.length for seg in self.segs)
self.bbox = None
self.bboxWorldSpace = None
def getSeg(self, idx):
return self.segs[idx]
def getSegs(self):
return self.segs
def getSegsCopy(self, start, end):
if(start == None):
start = 0
if(end == None):
end = len(self.segs)
return self.segs[start:end]
Loading
Loading full blame...