Rigify: store advanced options in armature instead of window manager.
[blender-addons.git] / curve_assign_shapekey.py
blob46a2b0d12a6251482fede74a6de319f32351507f
3 # This Blender add-on assigns one or more Bezier Curves as shape keys to another
4 # Bezier Curve
6 # Supported Blender Version: 2.80 Beta
8 # Copyright (C) 2019 Shrinivas Kulkarni
10 # License: GPL-3.0 (https://github.com/Shriinivas/assignshapekey/blob/master/LICENSE)
13 import bpy, bmesh, bgl, gpu
14 from gpu_extras.batch import batch_for_shader
15 from bpy.props import BoolProperty, EnumProperty, StringProperty
16 from collections import OrderedDict
17 from mathutils import Vector
18 from math import sqrt, floor
19 from functools import cmp_to_key
20 from bpy.types import Panel, Operator, AddonPreferences
23 bl_info = {
24 "name": "Assign Shape Keys",
25 "author": "Shrinivas Kulkarni",
26 "version": (1, 0, 1),
27 "location": "View 3D > Sidebar > Edit Tab",
28 "description": "Assigns one or more Bezier curves as shape keys to another Bezier curve",
29 "category": "Add Curve",
30 "wiki_url": "https://github.com/Shriinivas/assignshapekey/blob/master/README.md",
31 "blender": (2, 80, 0),
34 matchList = [('vCnt', 'Vertex Count', 'Match by vertex count'),
35 ('bbArea', 'Area', 'Match by surface area of the bounding box'), \
36 ('bbHeight', 'Height', 'Match by bounding box height'), \
37 ('bbWidth', 'Width', 'Match by bounding box width'),
38 ('bbDepth', 'Depth', 'Match by bounding box depth'),
39 ('minX', 'Min X', 'Match by bounding bon Min X'),
40 ('maxX', 'Max X', 'Match by bounding bon Max X'),
41 ('minY', 'Min Y', 'Match by bounding bon Min Y'),
42 ('maxY', 'Max Y', 'Match by bounding bon Max Y'),
43 ('minZ', 'Min Z', 'Match by bounding bon Min Z'),
44 ('maxZ', 'Max Z', 'Match by bounding bon Max Z')]
46 DEF_ERR_MARGIN = 0.0001
48 def isBezier(obj):
49 return obj.type == 'CURVE' and len(obj.data.splines) > 0 \
50 and obj.data.splines[0].type == 'BEZIER'
52 #Avoid errors due to floating point conversions/comparisons
53 #TODO: return -1, 0, 1
54 def floatCmpWithMargin(float1, float2, margin = DEF_ERR_MARGIN):
55 return abs(float1 - float2) < margin
57 def vectCmpWithMargin(v1, v2, margin = DEF_ERR_MARGIN):
58 return all(floatCmpWithMargin(v1[i], v2[i], margin) for i in range(0, len(v1)))
60 class Segment():
62 #pts[0] - start, pts[1] - ctrl1, pts[2] - ctrl2, , pts[3] - end
63 def pointAtT(pts, t):
64 return pts[0] + t * (3 * (pts[1] - pts[0]) +
65 t* (3 * (pts[0] + pts[2]) - 6 * pts[1] +
66 t * (-pts[0] + 3 * (pts[1] - pts[2]) + pts[3])))
68 def getSegLenRecurs(pts, start, end, t1 = 0, t2 = 1, error = DEF_ERR_MARGIN):
69 t1_5 = (t1 + t2)/2
70 mid = Segment.pointAtT(pts, t1_5)
71 l = (end - start).length
72 l2 = (mid - start).length + (end - mid).length
73 if (l2 - l > error):
74 return (Segment.getSegLenRecurs(pts, start, mid, t1, t1_5, error) +
75 Segment.getSegLenRecurs(pts, mid, end, t1_5, t2, error))
76 return l2
78 def __init__(self, start, ctrl1, ctrl2, end):
79 self.start = start
80 self.ctrl1 = ctrl1
81 self.ctrl2 = ctrl2
82 self.end = end
83 pts = [start, ctrl1, ctrl2, end]
84 self.length = Segment.getSegLenRecurs(pts, start, end)
86 #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
87 def partialSeg(self, t0, t1):
88 pts = [self.start, self.ctrl1, self.ctrl2, self.end]
90 if(t0 > t1):
91 tt = t1
92 t1 = t0
93 t0 = tt
95 #Let's make at least the line segments of predictable length :)
96 if(pts[0] == pts[1] and pts[2] == pts[3]):
97 pt0 = Vector([(1 - t0) * pts[0][i] + t0 * pts[2][i] for i in range(0, 3)])
98 pt1 = Vector([(1 - t1) * pts[0][i] + t1 * pts[2][i] for i in range(0, 3)])
99 return Segment(pt0, pt0, pt1, pt1)
101 u0 = 1.0 - t0
102 u1 = 1.0 - t1
104 qa = [pts[0][i]*u0*u0 + pts[1][i]*2*t0*u0 + pts[2][i]*t0*t0 for i in range(0, 3)]
105 qb = [pts[0][i]*u1*u1 + pts[1][i]*2*t1*u1 + pts[2][i]*t1*t1 for i in range(0, 3)]
106 qc = [pts[1][i]*u0*u0 + pts[2][i]*2*t0*u0 + pts[3][i]*t0*t0 for i in range(0, 3)]
107 qd = [pts[1][i]*u1*u1 + pts[2][i]*2*t1*u1 + pts[3][i]*t1*t1 for i in range(0, 3)]
109 pta = Vector([qa[i]*u0 + qc[i]*t0 for i in range(0, 3)])
110 ptb = Vector([qa[i]*u1 + qc[i]*t1 for i in range(0, 3)])
111 ptc = Vector([qb[i]*u0 + qd[i]*t0 for i in range(0, 3)])
112 ptd = Vector([qb[i]*u1 + qd[i]*t1 for i in range(0, 3)])
114 return Segment(pta, ptb, ptc, ptd)
116 #see https://stackoverflow.com/questions/24809978/calculating-the-bounding-box-of-cubic-bezier-curve
117 #(3 D - 9 C + 9 B - 3 A) t^2 + (6 A - 12 B + 6 C) t + 3 (B - A)
118 #pts[0] - start, pts[1] - ctrl1, pts[2] - ctrl2, , pts[3] - end
119 #TODO: Return Vectors to make world space calculations consistent
120 def bbox(self, mw = None):
121 def evalBez(AA, BB, CC, DD, t):
122 return AA * (1 - t) * (1 - t) * (1 - t) + \
123 3 * BB * t * (1 - t) * (1 - t) + \
124 3 * CC * t * t * (1 - t) + \
125 DD * t * t * t
127 A = self.start
128 B = self.ctrl1
129 C = self.ctrl2
130 D = self.end
132 if(mw != None):
133 A = mw @ A
134 B = mw @ B
135 C = mw @ C
136 D = mw @ D
138 MINXYZ = [min([A[i], D[i]]) for i in range(0, 3)]
139 MAXXYZ = [max([A[i], D[i]]) for i in range(0, 3)]
140 leftBotBack_rgtTopFront = [MINXYZ, MAXXYZ]
142 a = [3 * D[i] - 9 * C[i] + 9 * B[i] - 3 * A[i] for i in range(0, 3)]
143 b = [6 * A[i] - 12 * B[i] + 6 * C[i] for i in range(0, 3)]
144 c = [3 * (B[i] - A[i]) for i in range(0, 3)]
146 solnsxyz = []
147 for i in range(0, 3):
148 solns = []
149 if(a[i] == 0):
150 if(b[i] == 0):
151 solns.append(0)#Independent of t so lets take the starting pt
152 else:
153 solns.append(c[i] / b[i])
154 else:
155 rootFact = b[i] * b[i] - 4 * a[i] * c[i]
156 if(rootFact >=0 ):
157 #Two solutions with + and - sqrt
158 solns.append((-b[i] + sqrt(rootFact)) / (2 * a[i]))
159 solns.append((-b[i] - sqrt(rootFact)) / (2 * a[i]))
160 solnsxyz.append(solns)
162 for i, soln in enumerate(solnsxyz):
163 for j, t in enumerate(soln):
164 if(t < 1 and t > 0):
165 co = evalBez(A[i], B[i], C[i], D[i], t)
166 if(co < leftBotBack_rgtTopFront[0][i]):
167 leftBotBack_rgtTopFront[0][i] = co
168 if(co > leftBotBack_rgtTopFront[1][i]):
169 leftBotBack_rgtTopFront[1][i] = co
171 return leftBotBack_rgtTopFront
174 class Part():
175 def __init__(self, parent, segs, isClosed):
176 self.parent = parent
177 self.segs = segs
179 #use_cyclic_u
180 self.isClosed = isClosed
182 #Indicates if this should be closed based on its counterparts in other paths
183 self.toClose = isClosed
185 self.length = sum(seg.length for seg in self.segs)
186 self.bbox = None
187 self.bboxWorldSpace = None
189 def getSeg(self, idx):
190 return self.segs[idx]
192 def getSegs(self):
193 return self.segs
195 def getSegsCopy(self, start, end):
196 if(start == None):
197 start = 0
198 if(end == None):
199 end = len(self.segs)
200 return self.segs[start:end]
202 def getBBox(self, worldSpace):
203 #Avoid frequent calculations, as this will be called in compare method
204 if(not worldSpace and self.bbox != None):
205 return self.bbox
207 if(worldSpace and self.bboxWorldSpace != None):
208 return self.bboxWorldSpace
210 leftBotBack_rgtTopFront = [[None]*3,[None]*3]
212 for seg in self.segs:
214 if(worldSpace):
215 bb = seg.bbox(self.parent.curve.matrix_world)
216 else:
217 bb = seg.bbox()
219 for i in range(0, 3):
220 if (leftBotBack_rgtTopFront[0][i] == None or \
221 bb[0][i] < leftBotBack_rgtTopFront[0][i]):
222 leftBotBack_rgtTopFront[0][i] = bb[0][i]
224 for i in range(0, 3):
225 if (leftBotBack_rgtTopFront[1][i] == None or \
226 bb[1][i] > leftBotBack_rgtTopFront[1][i]):
227 leftBotBack_rgtTopFront[1][i] = bb[1][i]
229 if(worldSpace):
230 self.bboxWorldSpace = leftBotBack_rgtTopFront
231 else:
232 self.bbox = leftBotBack_rgtTopFront
234 return leftBotBack_rgtTopFront
236 #private
237 def getBBDiff(self, axisIdx, worldSpace):
238 obj = self.parent.curve
239 bbox = self.getBBox(worldSpace)
240 diff = abs(bbox[1][axisIdx] - bbox[0][axisIdx])
241 return diff
243 def getBBWidth(self, worldSpace):
244 return self.getBBDiff(0, worldSpace)
246 def getBBHeight(self, worldSpace):
247 return self.getBBDiff(1, worldSpace)
249 def getBBDepth(self, worldSpace):
250 return self.getBBDiff(2, worldSpace)
252 def bboxSurfaceArea(self, worldSpace):
253 leftBotBack_rgtTopFront = self.getBBox(worldSpace)
254 w = abs( leftBotBack_rgtTopFront[1][0] - leftBotBack_rgtTopFront[0][0] )
255 l = abs( leftBotBack_rgtTopFront[1][1] - leftBotBack_rgtTopFront[0][1] )
256 d = abs( leftBotBack_rgtTopFront[1][2] - leftBotBack_rgtTopFront[0][2] )
258 return 2 * (w * l + w * d + l * d)
260 def getSegCnt(self):
261 return len(self.segs)
263 def getBezierPtsInfo(self):
264 prevSeg = None
265 bezierPtsInfo = []
267 for j, seg in enumerate(self.getSegs()):
269 pt = seg.start
270 handleRight = seg.ctrl1
272 if(j == 0):
273 if(self.toClose):
274 handleLeft = self.getSeg(-1).ctrl2
275 else:
276 handleLeft = pt
277 else:
278 handleLeft = prevSeg.ctrl2
280 bezierPtsInfo.append([pt, handleLeft, handleRight])
281 prevSeg = seg
283 if(self.toClose == True):
284 bezierPtsInfo[-1][2] = seg.ctrl1
285 else:
286 bezierPtsInfo.append([prevSeg.end, prevSeg.ctrl2, prevSeg.end])
288 return bezierPtsInfo
290 def __repr__(self):
291 return str(self.length)
294 class Path:
295 def __init__(self, curve, objData = None, name = None):
297 if(objData == None):
298 objData = curve.data
300 if(name == None):
301 name = curve.name
303 self.name = name
304 self.curve = curve
306 self.parts = [Part(self, getSplineSegs(s), s.use_cyclic_u) for s in objData.splines]
308 def getPartCnt(self):
309 return len(self.parts)
311 def getPartView(self):
312 p = Part(self, [seg for part in self.parts for seg in part.getSegs()], None)
313 return p
315 def getPartBoundaryIdxs(self):
316 cumulCntList = set()
317 cumulCnt = 0
319 for p in self.parts:
320 cumulCnt += p.getSegCnt()
321 cumulCntList.add(cumulCnt)
323 return cumulCntList
325 def updatePartsList(self, segCntsPerPart, byPart):
326 monolithicSegList = [seg for part in self.parts for seg in part.getSegs()]
327 oldParts = self.parts[:]
328 currPart = oldParts[0]
329 partIdx = 0
330 self.parts.clear()
332 for i in range(0, len(segCntsPerPart)):
333 if( i == 0):
334 currIdx = 0
335 else:
336 currIdx = segCntsPerPart[i-1]
338 nextIdx = segCntsPerPart[i]
339 isClosed = False
341 if(vectCmpWithMargin(monolithicSegList[currIdx].start, \
342 currPart.getSegs()[0].start) and \
343 vectCmpWithMargin(monolithicSegList[nextIdx-1].end, \
344 currPart.getSegs()[-1].end)):
345 isClosed = currPart.isClosed
347 self.parts.append(Part(self, \
348 monolithicSegList[currIdx:nextIdx], isClosed))
350 if(monolithicSegList[nextIdx-1] == currPart.getSegs()[-1]):
351 partIdx += 1
352 if(partIdx < len(oldParts)):
353 currPart = oldParts[partIdx]
355 def getBezierPtsBySpline(self):
356 data = []
358 for i, part in enumerate(self.parts):
359 data.append(part.getBezierPtsInfo())
361 return data
363 def getNewCurveData(self):
365 newCurveData = self.curve.data.copy()
366 newCurveData.splines.clear()
368 splinesData = self.getBezierPtsBySpline()
370 for i, newPoints in enumerate(splinesData):
372 spline = newCurveData.splines.new('BEZIER')
373 spline.bezier_points.add(len(newPoints)-1)
374 spline.use_cyclic_u = self.parts[i].toClose
376 for j in range(0, len(spline.bezier_points)):
377 newPoint = newPoints[j]
378 spline.bezier_points[j].co = newPoint[0]
379 spline.bezier_points[j].handle_left = newPoint[1]
380 spline.bezier_points[j].handle_right = newPoint[2]
381 spline.bezier_points[j].handle_right_type = 'FREE'
383 return newCurveData
385 def updateCurve(self):
386 curveData = self.curve.data
387 #Remove existing shape keys first
388 if(curveData.shape_keys != None):
389 keyblocks = reversed(curveData.shape_keys.key_blocks)
390 for sk in keyblocks:
391 self.curve.shape_key_remove(sk)
392 self.curve.data = self.getNewCurveData()
393 bpy.data.curves.remove(curveData)
395 def main(removeOriginal, space, matchParts, matchCriteria, alignBy, alignValues):
396 targetObj = bpy.context.active_object
397 if(targetObj == None or not isBezier(targetObj)):
398 return
400 target = Path(targetObj)
402 shapekeys = [Path(c) for c in bpy.context.selected_objects if isBezier(c) \
403 and c != bpy.context.active_object]
405 if(len(shapekeys) == 0):
406 return
408 shapekeys = getExistingShapeKeyPaths(target) + shapekeys
409 userSel = [target] + shapekeys
411 for path in userSel:
412 alignPath(path, matchParts, matchCriteria, alignBy, alignValues)
414 addMissingSegs(userSel, byPart = (matchParts != "-None-"))
416 bIdxs = set()
417 for path in userSel:
418 bIdxs = bIdxs.union(path.getPartBoundaryIdxs())
420 for path in userSel:
421 path.updatePartsList(sorted(list(bIdxs)), byPart = False)
423 #All will have the same part count by now
424 allToClose = [all(path.parts[j].isClosed for path in userSel)
425 for j in range(0, len(userSel[0].parts))]
427 #All paths will have the same no of splines with the same no of bezier points
428 for path in userSel:
429 for j, part in enumerate(path.parts):
430 part.toClose = allToClose[j]
432 target.updateCurve()
434 target.curve.shape_key_add(name = 'Basis')
436 addShapeKeys(target.curve, shapekeys, space)
438 if(removeOriginal):
439 for path in userSel:
440 if(path.curve != target.curve):
441 safeRemoveCurveObj(path.curve)
443 return {}
445 def getSplineSegs(spline):
446 p = spline.bezier_points
447 segs = [Segment(p[i-1].co, p[i-1].handle_right, p[i].handle_left, p[i].co) \
448 for i in range(1, len(p))]
449 if(spline.use_cyclic_u):
450 segs.append(Segment(p[-1].co, p[-1].handle_right, p[0].handle_left, p[0].co))
451 return segs
453 def subdivideSeg(origSeg, noSegs):
454 if(noSegs < 2):
455 return [origSeg]
457 segs = []
458 oldT = 0
459 segLen = origSeg.length / noSegs
461 for i in range(0, noSegs-1):
462 t = float(i+1) / noSegs
463 seg = origSeg.partialSeg(oldT, t)
464 segs.append(seg)
465 oldT = t
467 seg = origSeg.partialSeg(oldT, 1)
468 segs.append(seg)
470 return segs
473 def getSubdivCntPerSeg(part, toAddCnt):
475 class SegWrapper:
476 def __init__(self, idx, seg):
477 self.idx = idx
478 self.seg = seg
479 self.length = seg.length
481 class PartWrapper:
482 def __init__(self, part):
483 self.segList = []
484 self.segCnt = len(part.getSegs())
485 for idx, seg in enumerate(part.getSegs()):
486 self.segList.append(SegWrapper(idx, seg))
488 partWrapper = PartWrapper(part)
489 partLen = part.length
490 avgLen = partLen / (partWrapper.segCnt + toAddCnt)
492 segsToDivide = [sr for sr in partWrapper.segList if sr.seg.length >= avgLen]
493 segToDivideCnt = len(segsToDivide)
494 avgLen = sum(sr.seg.length for sr in segsToDivide) / (segToDivideCnt + toAddCnt)
496 segsToDivide = sorted(segsToDivide, key=lambda x: x.length, reverse = True)
498 cnts = [0] * partWrapper.segCnt
499 addedCnt = 0
502 for i in range(0, segToDivideCnt):
503 segLen = segsToDivide[i].seg.length
505 divideCnt = int(round(segLen/avgLen)) - 1
506 if(divideCnt == 0):
507 break
509 if((addedCnt + divideCnt) >= toAddCnt):
510 cnts[segsToDivide[i].idx] = toAddCnt - addedCnt
511 addedCnt = toAddCnt
512 break
514 cnts[segsToDivide[i].idx] = divideCnt
516 addedCnt += divideCnt
518 #TODO: Verify if needed
519 while(toAddCnt > addedCnt):
520 for i in range(0, segToDivideCnt):
521 cnts[segsToDivide[i].idx] += 1
522 addedCnt += 1
523 if(toAddCnt == addedCnt):
524 break
526 return cnts
528 #Just distribute equally; this is likely a rare condition. So why complicate?
529 def distributeCnt(maxSegCntsByPart, startIdx, extraCnt):
530 added = 0
531 elemCnt = len(maxSegCntsByPart) - startIdx
532 cntPerElem = floor(extraCnt / elemCnt)
533 remainder = extraCnt % elemCnt
535 for i in range(startIdx, len(maxSegCntsByPart)):
536 maxSegCntsByPart[i] += cntPerElem
537 if(i < remainder + startIdx):
538 maxSegCntsByPart[i] += 1
540 #Make all the paths to have the maximum number of segments in the set
541 #TODO: Refactor
542 def addMissingSegs(selPaths, byPart):
543 maxSegCntsByPart = []
544 maxSegCnt = 0
546 resSegCnt = []
547 sortedPaths = sorted(selPaths, key = lambda c: -len(c.parts))
549 for i, path in enumerate(sortedPaths):
550 if(byPart == False):
551 segCnt = path.getPartView().getSegCnt()
552 if(segCnt > maxSegCnt):
553 maxSegCnt = segCnt
554 else:
555 resSegCnt.append([])
556 for j, part in enumerate(path.parts):
557 partSegCnt = part.getSegCnt()
558 resSegCnt[i].append(partSegCnt)
560 #First path
561 if(j == len(maxSegCntsByPart)):
562 maxSegCntsByPart.append(partSegCnt)
564 #last part of this path, but other paths in set have more parts
565 elif((j == len(path.parts) - 1) and
566 len(maxSegCntsByPart) > len(path.parts)):
568 remainingSegs = sum(maxSegCntsByPart[j:])
569 if(partSegCnt <= remainingSegs):
570 resSegCnt[i][j] = remainingSegs
571 else:
572 #This part has more segs than the sum of the remaining part segs
573 #So distribute the extra count
574 distributeCnt(maxSegCntsByPart, j, (partSegCnt - remainingSegs))
576 #Also, adjust the seg count of the last part of the previous
577 #segments that had fewer than max number of parts
578 for k in range(0, i):
579 if(len(sortedPaths[k].parts) < len(maxSegCntsByPart)):
580 totalSegs = sum(maxSegCntsByPart)
581 existingSegs = sum(maxSegCntsByPart[:len(sortedPaths[k].parts)-1])
582 resSegCnt[k][-1] = totalSegs - existingSegs
584 elif(partSegCnt > maxSegCntsByPart[j]):
585 maxSegCntsByPart[j] = partSegCnt
586 for i, path in enumerate(sortedPaths):
588 if(byPart == False):
589 partView = path.getPartView()
590 segCnt = partView.getSegCnt()
591 diff = maxSegCnt - segCnt
593 if(diff > 0):
594 cnts = getSubdivCntPerSeg(partView, diff)
595 cumulSegIdx = 0
596 for j in range(0, len(path.parts)):
597 part = path.parts[j]
598 newSegs = []
599 for k, seg in enumerate(part.getSegs()):
600 numSubdivs = cnts[cumulSegIdx] + 1
601 newSegs += subdivideSeg(seg, numSubdivs)
602 cumulSegIdx += 1
604 path.parts[j] = Part(path, newSegs, part.isClosed)
605 else:
606 for j in range(0, len(path.parts)):
607 part = path.parts[j]
608 newSegs = []
610 partSegCnt = part.getSegCnt()
612 #TODO: Adding everything in the last part?
613 if(j == (len(path.parts)-1) and
614 len(maxSegCntsByPart) > len(path.parts)):
615 diff = resSegCnt[i][j] - partSegCnt
616 else:
617 diff = maxSegCntsByPart[j] - partSegCnt
619 if(diff > 0):
620 cnts = getSubdivCntPerSeg(part, diff)
622 for k, seg in enumerate(part.getSegs()):
623 seg = part.getSeg(k)
624 subdivCnt = cnts[k] + 1 #1 for the existing one
625 newSegs += subdivideSeg(seg, subdivCnt)
627 #isClosed won't be used, but let's update anyway
628 path.parts[j] = Part(path, newSegs, part.isClosed)
630 #TODO: Simplify (Not very readable)
631 def alignPath(path, matchParts, matchCriteria, alignBy, alignValues):
633 parts = path.parts[:]
635 if(matchParts == 'custom'):
636 fnMap = {'vCnt' : lambda part: -1 * part.getSegCnt(), \
637 'bbArea': lambda part: -1 * part.bboxSurfaceArea(worldSpace = True), \
638 'bbHeight' : lambda part: -1 * part.getBBHeight(worldSpace = True), \
639 'bbWidth' : lambda part: -1 * part.getBBWidth(worldSpace = True), \
640 'bbDepth' : lambda part: -1 * part.getBBDepth(worldSpace = True)
642 matchPartCmpFns = []
643 for criterion in matchCriteria:
644 fn = fnMap.get(criterion)
645 if(fn == None):
646 minmax = criterion[:3] == 'max' #0 if min; 1 if max
647 axisIdx = ord(criterion[3:]) - ord('X')
649 fn = eval('lambda part: part.getBBox(worldSpace = True)[' + \
650 str(minmax) + '][' + str(axisIdx) + ']')
652 matchPartCmpFns.append(fn)
654 def comparer(left, right):
655 for fn in matchPartCmpFns:
656 a = fn(left)
657 b = fn(right)
659 if(floatCmpWithMargin(a, b)):
660 continue
661 else:
662 return (a > b) - ( a < b) #No cmp in python3
664 return 0
666 parts = sorted(parts, key = cmp_to_key(comparer))
668 alignCmpFn = None
669 if(alignBy == 'vertCo'):
670 def evalCmp(criteria, pt1, pt2):
671 if(len(criteria) == 0):
672 return True
674 minmax = criteria[0][0]
675 axisIdx = criteria[0][1]
676 val1 = pt1[axisIdx]
677 val2 = pt2[axisIdx]
679 if(floatCmpWithMargin(val1, val2)):
680 criteria = criteria[:]
681 criteria.pop(0)
682 return evalCmp(criteria, pt1, pt2)
684 return val1 < val2 if minmax == 'min' else val1 > val2
686 alignCri = [[a[:3], ord(a[3:]) - ord('X')] for a in alignValues]
687 alignCmpFn = lambda pt1, pt2, curve: (evalCmp(alignCri, \
688 curve.matrix_world @ pt1, curve.matrix_world @ pt2))
690 startPt = None
691 startIdx = None
693 for i in range(0, len(parts)):
694 #Only truly closed parts
695 if(alignCmpFn != None and parts[i].isClosed):
696 for j in range(0, parts[i].getSegCnt()):
697 seg = parts[i].getSeg(j)
698 if(j == 0 or alignCmpFn(seg.start, startPt, path.curve)):
699 startPt = seg.start
700 startIdx = j
702 path.parts[i]= Part(path, parts[i].getSegsCopy(startIdx, None) + \
703 parts[i].getSegsCopy(None, startIdx), parts[i].isClosed)
704 else:
705 path.parts[i] = parts[i]
707 #TODO: Other shape key attributes like interpolation...?
708 def getExistingShapeKeyPaths(path):
709 obj = path.curve
710 paths = []
712 if(obj.data.shape_keys != None):
713 keyblocks = obj.data.shape_keys.key_blocks[1:]#Skip basis
714 for key in keyblocks:
715 datacopy = obj.data.copy()
716 i = 0
717 for spline in datacopy.splines:
718 for pt in spline.bezier_points:
719 pt.co = key.data[i].co
720 pt.handle_left = key.data[i].handle_left
721 pt.handle_right = key.data[i].handle_right
722 i += 1
723 paths.append(Path(obj, datacopy, key.name))
724 return paths
726 def addShapeKeys(curve, paths, space):
727 for path in paths:
728 key = curve.shape_key_add(name = path.name)
729 pts = [pt for pset in path.getBezierPtsBySpline() for pt in pset]
730 for i, pt in enumerate(pts):
731 if(space == 'worldspace'):
732 pt = [curve.matrix_world.inverted() @ (path.curve.matrix_world @ p) for p in pt]
733 key.data[i].co = pt[0]
734 key.data[i].handle_left = pt[1]
735 key.data[i].handle_right = pt[2]
737 #TODO: Remove try
738 def safeRemoveCurveObj(obj):
739 try:
740 collections = obj.users_collection
742 for c in collections:
743 c.objects.unlink(obj)
745 if(obj.name in bpy.context.scene.collection.objects):
746 bpy.context.scene.collection.objects.unlink(obj)
748 if(obj.data.users == 1):
749 if(obj.type == 'CURVE'):
750 bpy.data.curves.remove(obj.data) #This also removes object?
751 elif(obj.type == 'MESH'):
752 bpy.data.meshes.remove(obj.data)
754 bpy.data.objects.remove(obj)
755 except:
756 pass
759 def markVertHandler(self, context):
760 if(self.markVertex):
761 bpy.ops.wm.mark_vertex()
764 #################### UI and Registration ####################
766 class AssignShapeKeysOp(Operator):
767 bl_idname = "object.assign_shape_keys"
768 bl_label = "Assign Shape Keys"
769 bl_options = {'REGISTER', 'UNDO'}
771 def execute(self, context):
772 params = context.window_manager.AssignShapeKeyParams
773 removeOriginal = params.removeOriginal
774 space = params.space
776 matchParts = params.matchParts
777 matchCri1 = params.matchCri1
778 matchCri2 = params.matchCri2
779 matchCri3 = params.matchCri3
781 alignBy = params.alignList
782 alignVal1 = params.alignVal1
783 alignVal2 = params.alignVal2
784 alignVal3 = params.alignVal3
786 createdObjsMap = main(removeOriginal, space, \
787 matchParts, [matchCri1, matchCri2, matchCri3], \
788 alignBy, [alignVal1, alignVal2, alignVal3])
790 return {'FINISHED'}
793 class MarkerController:
794 drawHandlerRef = None
795 defPointSize = 6
796 ptColor = (0, .8, .8, 1)
798 def createSMMap(self, context):
799 objs = context.selected_objects
800 smMap = {}
801 for curve in objs:
802 if(not isBezier(curve)):
803 continue
805 smMap[curve.name] = {}
806 mw = curve.matrix_world
807 for splineIdx, spline in enumerate(curve.data.splines):
808 if(not spline.use_cyclic_u):
809 continue
811 #initialize to the curr start vert co and idx
812 smMap[curve.name][splineIdx] = \
813 [mw @ curve.data.splines[splineIdx].bezier_points[0].co, 0]
815 for pt in spline.bezier_points:
816 pt.select_control_point = False
818 if(len(smMap[curve.name]) == 0):
819 del smMap[curve.name]
821 return smMap
823 def createBatch(self, context):
824 positions = [s[0] for cn in self.smMap.values() for s in cn.values()]
825 colors = [MarkerController.ptColor for i in range(0, len(positions))]
827 self.batch = batch_for_shader(self.shader, \
828 "POINTS", {"pos": positions, "color": colors})
830 if context.area:
831 context.area.tag_redraw()
833 def drawHandler(self):
834 bgl.glPointSize(MarkerController.defPointSize)
835 self.batch.draw(self.shader)
837 def removeMarkers(self, context):
838 if(MarkerController.drawHandlerRef != None):
839 bpy.types.SpaceView3D.draw_handler_remove(MarkerController.drawHandlerRef, \
840 "WINDOW")
842 if(context.area and hasattr(context.space_data, 'region_3d')):
843 context.area.tag_redraw()
845 MarkerController.drawHandlerRef = None
847 self.deselectAll()
849 def __init__(self, context):
850 self.smMap = self.createSMMap(context)
851 self.shader = gpu.shader.from_builtin('3D_FLAT_COLOR')
852 self.shader.bind()
854 MarkerController.drawHandlerRef = \
855 bpy.types.SpaceView3D.draw_handler_add(self.drawHandler, \
856 (), "WINDOW", "POST_VIEW")
858 self.createBatch(context)
860 def saveStartVerts(self):
861 for curveName in self.smMap.keys():
862 curve = bpy.data.objects[curveName]
863 splines = curve.data.splines
864 spMap = self.smMap[curveName]
866 for splineIdx in spMap.keys():
867 markerInfo = spMap[splineIdx]
868 if(markerInfo[1] != 0):
869 pts = splines[splineIdx].bezier_points
870 loc, idx = markerInfo[0], markerInfo[1]
871 cnt = len(pts)
873 ptCopy = [[p.co.copy(), p.handle_right.copy(), \
874 p.handle_left.copy(), p.handle_right_type, \
875 p.handle_left_type] for p in pts]
877 for i, pt in enumerate(pts):
878 srcIdx = (idx + i) % cnt
879 p = ptCopy[srcIdx]
881 #Must set the types first
882 pt.handle_right_type = p[3]
883 pt.handle_left_type = p[4]
884 pt.co = p[0]
885 pt.handle_right = p[1]
886 pt.handle_left = p[2]
888 def updateSMMap(self):
889 for curveName in self.smMap.keys():
890 curve = bpy.data.objects[curveName]
891 spMap = self.smMap[curveName]
892 mw = curve.matrix_world
894 for splineIdx in spMap.keys():
895 markerInfo = spMap[splineIdx]
896 loc, idx = markerInfo[0], markerInfo[1]
897 pts = curve.data.splines[splineIdx].bezier_points
899 selIdxs = [x for x in range(0, len(pts)) \
900 if pts[x].select_control_point == True]
902 selIdx = selIdxs[0] if(len(selIdxs) > 0 ) else idx
903 co = mw @ pts[selIdx].co
904 self.smMap[curveName][splineIdx] = [co, selIdx]
906 def deselectAll(self):
907 for curveName in self.smMap.keys():
908 curve = bpy.data.objects[curveName]
909 for spline in curve.data.splines:
910 for pt in spline.bezier_points:
911 pt.select_control_point = False
913 def getSpaces3D(context):
914 areas3d = [area for area in context.window.screen.areas \
915 if area.type == 'VIEW_3D']
917 return [s for a in areas3d for s in a.spaces if s.type == 'VIEW_3D']
919 def hideHandles(context):
920 states = []
921 spaces = MarkerController.getSpaces3D(context)
922 for s in spaces:
923 states.append(s.overlay.show_curve_handles)
924 s.overlay.show_curve_handles = False
925 return states
927 def resetShowHandleState(context, handleStates):
928 spaces = MarkerController.getSpaces3D(context)
929 for i, s in enumerate(spaces):
930 s.overlay.show_curve_handles = handleStates[i]
933 class ModalMarkSegStartOp(Operator):
934 bl_description = "Mark Vertex"
935 bl_idname = "wm.mark_vertex"
936 bl_label = "Mark Start Vertex"
938 def cleanup(self, context):
939 wm = context.window_manager
940 wm.event_timer_remove(self._timer)
941 self.markerState.removeMarkers(context)
942 MarkerController.resetShowHandleState(context, self.handleStates)
943 context.window_manager.AssignShapeKeyParams.markVertex = False
945 def modal (self, context, event):
946 params = context.window_manager.AssignShapeKeyParams
948 if(context.mode == 'OBJECT' or event.type == "ESC" or\
949 not context.window_manager.AssignShapeKeyParams.markVertex):
950 self.cleanup(context)
951 return {'CANCELLED'}
953 elif(event.type == "RET"):
954 self.markerState.saveStartVerts()
955 self.cleanup(context)
956 return {'FINISHED'}
958 if(event.type == 'TIMER'):
959 self.markerState.updateSMMap()
960 self.markerState.createBatch(context)
962 elif(event.type in {'LEFT_CTRL', 'RIGHT_CTRL'}):
963 self.ctrl = (event.value == 'PRESS')
965 elif(event.type in {'LEFT_SHIFT', 'RIGHT_SHIFT'}):
966 self.shift = (event.value == 'PRESS')
968 if(event.type not in {"MIDDLEMOUSE", "TAB", "LEFTMOUSE", \
969 "RIGHTMOUSE", 'WHEELDOWNMOUSE', 'WHEELUPMOUSE'} and \
970 not event.type.startswith("NUMPAD_")):
971 return {'RUNNING_MODAL'}
973 return {"PASS_THROUGH"}
975 def execute(self, context):
976 #TODO: Why such small step?
977 self._timer = context.window_manager.event_timer_add(time_step = 0.0001, \
978 window = context.window)
979 self.ctrl = False
980 self.shift = False
982 context.window_manager.modal_handler_add(self)
983 self.markerState = MarkerController(context)
985 #Hide so that users don't accidentally select handles instead of points
986 self.handleStates = MarkerController.hideHandles(context)
988 return {"RUNNING_MODAL"}
991 class AssignShapeKeyParams(bpy.types.PropertyGroup):
993 removeOriginal : BoolProperty(name = "Remove Shape Key Objects", \
994 description = "Remove shape key objects after assigning to target", \
995 default = True)
997 space : EnumProperty(name = "Space", \
998 items = [('worldspace', 'World Space', 'worldspace'),
999 ('localspace', 'Local Space', 'localspace')], \
1000 description = 'Space that shape keys are evluated in')
1002 alignList : EnumProperty(name="Vertex Alignment", items = \
1003 [("-None-", 'Manual Alignment', "Align curve segments based on starting vertex"), \
1004 ('vertCo', 'Vertex Coordinates', 'Align curve segments based on vertex coordinates')], \
1005 description = 'Start aligning the vertices of target and shape keys from',
1006 default = '-None-')
1008 alignVal1 : EnumProperty(name="Value 1",
1009 items = matchList, default = 'minX', description='First align criterion')
1011 alignVal2 : EnumProperty(name="Value 2",
1012 items = matchList, default = 'maxY', description='Second align criterion')
1014 alignVal3 : EnumProperty(name="Value 3",
1015 items = matchList, default = 'minZ', description='Third align criterion')
1017 matchParts : EnumProperty(name="Match Parts", items = \
1018 [("-None-", 'None', "Don't match parts"), \
1019 ('default', 'Default', 'Use part (spline) order as in curve'), \
1020 ('custom', 'Custom', 'Use one of the custom criteria for part matching')], \
1021 description='Match disconnected parts', default = 'default')
1023 matchCri1 : EnumProperty(name="Value 1",
1024 items = matchList, default = 'minX', description='First match criterion')
1026 matchCri2 : EnumProperty(name="Value 2",
1027 items = matchList, default = 'maxY', description='Second match criterion')
1029 matchCri3 : EnumProperty(name="Value 3",
1030 items = matchList, default = 'minZ', description='Third match criterion')
1032 markVertex : BoolProperty(name="Mark Starting Vertices", \
1033 description='Mark first vertices in all splines of selected curves', \
1034 default = False, update = markVertHandler)
1037 class AssignShapeKeysPanel(Panel):
1039 bl_label = "Curve Shape Keys"
1040 bl_idname = "CURVE_PT_assign_shape_keys"
1041 bl_space_type = 'VIEW_3D'
1042 bl_region_type = 'UI'
1043 bl_category = "Edit"
1044 bl_options = {'DEFAULT_CLOSED'}
1046 @classmethod
1047 def poll(cls, context):
1048 return context.mode in {'OBJECT', 'EDIT_CURVE'}
1050 def draw(self, context):
1052 layout = self.layout
1053 layout.label(text='Morph Curves:')
1054 col = layout.column()
1055 params = context.window_manager.AssignShapeKeyParams
1057 if(context.mode == 'OBJECT'):
1058 row = col.row()
1059 row.prop(params, "removeOriginal")
1061 row = col.row()
1062 row.prop(params, "space")
1064 row = col.row()
1065 row.prop(params, "alignList")
1067 if(params.alignList == 'vertCo'):
1068 row = col.row()
1069 row.prop(params, "alignVal1")
1070 row.prop(params, "alignVal2")
1071 row.prop(params, "alignVal3")
1073 row = col.row()
1074 row.prop(params, "matchParts")
1076 if(params.matchParts == 'custom'):
1077 row = col.row()
1078 row.prop(params, "matchCri1")
1079 row.prop(params, "matchCri2")
1080 row.prop(params, "matchCri3")
1082 row = col.row()
1083 row.operator("object.assign_shape_keys")
1084 else:
1085 col.prop(params, "markVertex", \
1086 toggle = True)
1089 def updatePanel(self, context):
1090 try:
1091 panel = AssignShapeKeysPanel
1092 if "bl_rna" in panel.__dict__:
1093 bpy.utils.unregister_class(panel)
1095 panel.bl_category = context.preferences.addons[__name__].preferences.category
1096 bpy.utils.register_class(panel)
1098 except Exception as e:
1099 print("Assign Shape Keys: Updating Panel locations has failed", e)
1101 class AssignShapeKeysPreferences(AddonPreferences):
1102 bl_idname = __name__
1104 category: StringProperty(
1105 name = "Tab Category",
1106 description = "Choose a name for the category of the panel",
1107 default = "Edit",
1108 update = updatePanel
1111 def draw(self, context):
1112 layout = self.layout
1113 row = layout.row()
1114 col = row.column()
1115 col.label(text="Tab Category:")
1116 col.prop(self, "category", text="")
1118 # registering and menu integration
1119 def register():
1120 bpy.utils.register_class(AssignShapeKeysPanel)
1121 bpy.utils.register_class(AssignShapeKeysOp)
1122 bpy.utils.register_class(AssignShapeKeyParams)
1123 bpy.types.WindowManager.AssignShapeKeyParams = \
1124 bpy.props.PointerProperty(type=AssignShapeKeyParams)
1125 bpy.utils.register_class(ModalMarkSegStartOp)
1126 bpy.utils.register_class(AssignShapeKeysPreferences)
1127 updatePanel(None, bpy.context)
1129 def unregister():
1130 bpy.utils.unregister_class(AssignShapeKeysOp)
1131 bpy.utils.unregister_class(AssignShapeKeysPanel)
1132 del bpy.types.WindowManager.AssignShapeKeyParams
1133 bpy.utils.unregister_class(AssignShapeKeyParams)
1134 bpy.utils.unregister_class(ModalMarkSegStartOp)
1135 bpy.utils.unregister_class(AssignShapeKeysPreferences)
1137 if __name__ == "__main__":
1138 register()