3 # This Blender add-on assigns one or more Bezier Curves as shape keys to another
6 # Supported Blender Versions: 2.8x
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
24 "name": "Assign Shape Keys",
25 "author": "Shrinivas Kulkarni",
27 "blender": (2, 80, 0),
28 "location": "View 3D > Sidebar > Edit Tab",
29 "description": "Assigns one or more Bezier curves as shape keys to another Bezier curve",
30 "doc_url": "{BLENDER_MANUAL_URL}/addons/add_curve/assign_shape_keys.html",
31 "category": "Add Curve",
34 alignList
= [('minX', 'Min X', 'Align vertices with Min X'),
35 ('maxX', 'Max X', 'Align vertices with Max X'),
36 ('minY', 'Min Y', 'Align vertices with Min Y'),
37 ('maxY', 'Max Y', 'Align vertices with Max Y'),
38 ('minZ', 'Min Z', 'Align vertices with Min Z'),
39 ('maxZ', 'Max Z', 'Align vertices with Max Z')]
41 matchList
= [('vCnt', 'Vertex Count', 'Match by vertex count'),
42 ('bbArea', 'Area', 'Match by surface area of the bounding box'), \
43 ('bbHeight', 'Height', 'Match by bounding box height'), \
44 ('bbWidth', 'Width', 'Match by bounding box width'),
45 ('bbDepth', 'Depth', 'Match by bounding box depth'),
46 ('minX', 'Min X', 'Match by bounding box Min X'),
47 ('maxX', 'Max X', 'Match by bounding box Max X'),
48 ('minY', 'Min Y', 'Match by bounding box Min Y'),
49 ('maxY', 'Max Y', 'Match by bounding box Max Y'),
50 ('minZ', 'Min Z', 'Match by bounding box Min Z'),
51 ('maxZ', 'Max Z', 'Match by bounding box Max Z')]
53 DEF_ERR_MARGIN
= 0.0001
56 return obj
.type == 'CURVE' and len(obj
.data
.splines
) > 0 \
57 and obj
.data
.splines
[0].type == 'BEZIER'
59 #Avoid errors due to floating point conversions/comparisons
60 #TODO: return -1, 0, 1
61 def floatCmpWithMargin(float1
, float2
, margin
= DEF_ERR_MARGIN
):
62 return abs(float1
- float2
) < margin
64 def vectCmpWithMargin(v1
, v2
, margin
= DEF_ERR_MARGIN
):
65 return all(floatCmpWithMargin(v1
[i
], v2
[i
], margin
) for i
in range(0, len(v1
)))
69 #pts[0] - start, pts[1] - ctrl1, pts[2] - ctrl2, , pts[3] - end
71 return pts
[0] + t
* (3 * (pts
[1] - pts
[0]) +
72 t
* (3 * (pts
[0] + pts
[2]) - 6 * pts
[1] +
73 t
* (-pts
[0] + 3 * (pts
[1] - pts
[2]) + pts
[3])))
75 def getSegLenRecurs(pts
, start
, end
, t1
= 0, t2
= 1, error
= DEF_ERR_MARGIN
):
77 mid
= Segment
.pointAtT(pts
, t1_5
)
78 l
= (end
- start
).length
79 l2
= (mid
- start
).length
+ (end
- mid
).length
81 return (Segment
.getSegLenRecurs(pts
, start
, mid
, t1
, t1_5
, error
) +
82 Segment
.getSegLenRecurs(pts
, mid
, end
, t1_5
, t2
, error
))
85 def __init__(self
, start
, ctrl1
, ctrl2
, end
):
90 pts
= [start
, ctrl1
, ctrl2
, end
]
91 self
.length
= Segment
.getSegLenRecurs(pts
, start
, end
)
93 #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
94 def partialSeg(self
, t0
, t1
):
95 pts
= [self
.start
, self
.ctrl1
, self
.ctrl2
, self
.end
]
102 #Let's make at least the line segments of predictable length :)
103 if(pts
[0] == pts
[1] and pts
[2] == pts
[3]):
104 pt0
= Vector([(1 - t0
) * pts
[0][i
] + t0
* pts
[2][i
] for i
in range(0, 3)])
105 pt1
= Vector([(1 - t1
) * pts
[0][i
] + t1
* pts
[2][i
] for i
in range(0, 3)])
106 return Segment(pt0
, pt0
, pt1
, pt1
)
111 qa
= [pts
[0][i
]*u0
*u0
+ pts
[1][i
]*2*t0
*u0
+ pts
[2][i
]*t0
*t0
for i
in range(0, 3)]
112 qb
= [pts
[0][i
]*u1
*u1
+ pts
[1][i
]*2*t1
*u1
+ pts
[2][i
]*t1
*t1
for i
in range(0, 3)]
113 qc
= [pts
[1][i
]*u0
*u0
+ pts
[2][i
]*2*t0
*u0
+ pts
[3][i
]*t0
*t0
for i
in range(0, 3)]
114 qd
= [pts
[1][i
]*u1
*u1
+ pts
[2][i
]*2*t1
*u1
+ pts
[3][i
]*t1
*t1
for i
in range(0, 3)]
116 pta
= Vector([qa
[i
]*u0
+ qc
[i
]*t0
for i
in range(0, 3)])
117 ptb
= Vector([qa
[i
]*u1
+ qc
[i
]*t1
for i
in range(0, 3)])
118 ptc
= Vector([qb
[i
]*u0
+ qd
[i
]*t0
for i
in range(0, 3)])
119 ptd
= Vector([qb
[i
]*u1
+ qd
[i
]*t1
for i
in range(0, 3)])
121 return Segment(pta
, ptb
, ptc
, ptd
)
123 #see https://stackoverflow.com/questions/24809978/calculating-the-bounding-box-of-cubic-bezier-curve
124 #(3 D - 9 C + 9 B - 3 A) t^2 + (6 A - 12 B + 6 C) t + 3 (B - A)
125 #pts[0] - start, pts[1] - ctrl1, pts[2] - ctrl2, , pts[3] - end
126 #TODO: Return Vectors to make world space calculations consistent
127 def bbox(self
, mw
= None):
128 def evalBez(AA
, BB
, CC
, DD
, t
):
129 return AA
* (1 - t
) * (1 - t
) * (1 - t
) + \
130 3 * BB
* t
* (1 - t
) * (1 - t
) + \
131 3 * CC
* t
* t
* (1 - t
) + \
145 MINXYZ
= [min([A
[i
], D
[i
]]) for i
in range(0, 3)]
146 MAXXYZ
= [max([A
[i
], D
[i
]]) for i
in range(0, 3)]
147 leftBotBack_rgtTopFront
= [MINXYZ
, MAXXYZ
]
149 a
= [3 * D
[i
] - 9 * C
[i
] + 9 * B
[i
] - 3 * A
[i
] for i
in range(0, 3)]
150 b
= [6 * A
[i
] - 12 * B
[i
] + 6 * C
[i
] for i
in range(0, 3)]
151 c
= [3 * (B
[i
] - A
[i
]) for i
in range(0, 3)]
154 for i
in range(0, 3):
158 solns
.append(0)#Independent of t so lets take the starting pt
160 solns
.append(c
[i
] / b
[i
])
162 rootFact
= b
[i
] * b
[i
] - 4 * a
[i
] * c
[i
]
164 #Two solutions with + and - sqrt
165 solns
.append((-b
[i
] + sqrt(rootFact
)) / (2 * a
[i
]))
166 solns
.append((-b
[i
] - sqrt(rootFact
)) / (2 * a
[i
]))
167 solnsxyz
.append(solns
)
169 for i
, soln
in enumerate(solnsxyz
):
170 for j
, t
in enumerate(soln
):
172 co
= evalBez(A
[i
], B
[i
], C
[i
], D
[i
], t
)
173 if(co
< leftBotBack_rgtTopFront
[0][i
]):
174 leftBotBack_rgtTopFront
[0][i
] = co
175 if(co
> leftBotBack_rgtTopFront
[1][i
]):
176 leftBotBack_rgtTopFront
[1][i
] = co
178 return leftBotBack_rgtTopFront
182 def __init__(self
, parent
, segs
, isClosed
):
187 self
.isClosed
= isClosed
189 #Indicates if this should be closed based on its counterparts in other paths
190 self
.toClose
= isClosed
192 self
.length
= sum(seg
.length
for seg
in self
.segs
)
194 self
.bboxWorldSpace
= None
196 def getSeg(self
, idx
):
197 return self
.segs
[idx
]
202 def getSegsCopy(self
, start
, end
):
207 return self
.segs
[start
:end
]
209 def getBBox(self
, worldSpace
):
210 #Avoid frequent calculations, as this will be called in compare method
211 if(not worldSpace
and self
.bbox
!= None):
214 if(worldSpace
and self
.bboxWorldSpace
!= None):
215 return self
.bboxWorldSpace
217 leftBotBack_rgtTopFront
= [[None]*3,[None]*3]
219 for seg
in self
.segs
:
222 bb
= seg
.bbox(self
.parent
.curve
.matrix_world
)
226 for i
in range(0, 3):
227 if (leftBotBack_rgtTopFront
[0][i
] == None or \
228 bb
[0][i
] < leftBotBack_rgtTopFront
[0][i
]):
229 leftBotBack_rgtTopFront
[0][i
] = bb
[0][i
]
231 for i
in range(0, 3):
232 if (leftBotBack_rgtTopFront
[1][i
] == None or \
233 bb
[1][i
] > leftBotBack_rgtTopFront
[1][i
]):
234 leftBotBack_rgtTopFront
[1][i
] = bb
[1][i
]
237 self
.bboxWorldSpace
= leftBotBack_rgtTopFront
239 self
.bbox
= leftBotBack_rgtTopFront
241 return leftBotBack_rgtTopFront
244 def getBBDiff(self
, axisIdx
, worldSpace
):
245 obj
= self
.parent
.curve
246 bbox
= self
.getBBox(worldSpace
)
247 diff
= abs(bbox
[1][axisIdx
] - bbox
[0][axisIdx
])
250 def getBBWidth(self
, worldSpace
):
251 return self
.getBBDiff(0, worldSpace
)
253 def getBBHeight(self
, worldSpace
):
254 return self
.getBBDiff(1, worldSpace
)
256 def getBBDepth(self
, worldSpace
):
257 return self
.getBBDiff(2, worldSpace
)
259 def bboxSurfaceArea(self
, worldSpace
):
260 leftBotBack_rgtTopFront
= self
.getBBox(worldSpace
)
261 w
= abs( leftBotBack_rgtTopFront
[1][0] - leftBotBack_rgtTopFront
[0][0] )
262 l
= abs( leftBotBack_rgtTopFront
[1][1] - leftBotBack_rgtTopFront
[0][1] )
263 d
= abs( leftBotBack_rgtTopFront
[1][2] - leftBotBack_rgtTopFront
[0][2] )
265 return 2 * (w
* l
+ w
* d
+ l
* d
)
268 return len(self
.segs
)
270 def getBezierPtsInfo(self
):
274 for j
, seg
in enumerate(self
.getSegs()):
277 handleRight
= seg
.ctrl1
281 handleLeft
= self
.getSeg(-1).ctrl2
285 handleLeft
= prevSeg
.ctrl2
287 bezierPtsInfo
.append([pt
, handleLeft
, handleRight
])
290 if(self
.toClose
== True):
291 bezierPtsInfo
[-1][2] = seg
.ctrl1
293 bezierPtsInfo
.append([prevSeg
.end
, prevSeg
.ctrl2
, prevSeg
.end
])
298 return str(self
.length
)
302 def __init__(self
, curve
, objData
= None, name
= None):
313 self
.parts
= [Part(self
, getSplineSegs(s
), s
.use_cyclic_u
) for s
in objData
.splines
]
315 def getPartCnt(self
):
316 return len(self
.parts
)
318 def getPartView(self
):
319 p
= Part(self
, [seg
for part
in self
.parts
for seg
in part
.getSegs()], None)
322 def getPartBoundaryIdxs(self
):
327 cumulCnt
+= p
.getSegCnt()
328 cumulCntList
.add(cumulCnt
)
332 def updatePartsList(self
, segCntsPerPart
, byPart
):
333 monolithicSegList
= [seg
for part
in self
.parts
for seg
in part
.getSegs()]
334 oldParts
= self
.parts
[:]
335 currPart
= oldParts
[0]
339 for i
in range(0, len(segCntsPerPart
)):
343 currIdx
= segCntsPerPart
[i
-1]
345 nextIdx
= segCntsPerPart
[i
]
348 if(vectCmpWithMargin(monolithicSegList
[currIdx
].start
, \
349 currPart
.getSegs()[0].start
) and \
350 vectCmpWithMargin(monolithicSegList
[nextIdx
-1].end
, \
351 currPart
.getSegs()[-1].end
)):
352 isClosed
= currPart
.isClosed
354 self
.parts
.append(Part(self
, \
355 monolithicSegList
[currIdx
:nextIdx
], isClosed
))
357 if(monolithicSegList
[nextIdx
-1] == currPart
.getSegs()[-1]):
359 if(partIdx
< len(oldParts
)):
360 currPart
= oldParts
[partIdx
]
362 def getBezierPtsBySpline(self
):
365 for i
, part
in enumerate(self
.parts
):
366 data
.append(part
.getBezierPtsInfo())
370 def getNewCurveData(self
):
372 newCurveData
= self
.curve
.data
.copy()
373 newCurveData
.splines
.clear()
375 splinesData
= self
.getBezierPtsBySpline()
377 for i
, newPoints
in enumerate(splinesData
):
379 spline
= newCurveData
.splines
.new('BEZIER')
380 spline
.bezier_points
.add(len(newPoints
)-1)
381 spline
.use_cyclic_u
= self
.parts
[i
].toClose
383 for j
in range(0, len(spline
.bezier_points
)):
384 newPoint
= newPoints
[j
]
385 spline
.bezier_points
[j
].co
= newPoint
[0]
386 spline
.bezier_points
[j
].handle_left
= newPoint
[1]
387 spline
.bezier_points
[j
].handle_right
= newPoint
[2]
388 spline
.bezier_points
[j
].handle_right_type
= 'FREE'
392 def updateCurve(self
):
393 curveData
= self
.curve
.data
394 #Remove existing shape keys first
395 if(curveData
.shape_keys
!= None):
396 keyblocks
= reversed(curveData
.shape_keys
.key_blocks
)
398 self
.curve
.shape_key_remove(sk
)
399 self
.curve
.data
= self
.getNewCurveData()
400 bpy
.data
.curves
.remove(curveData
)
402 def main(targetObj
, shapekeyObjs
, removeOriginal
, space
, matchParts
, \
403 matchCriteria
, alignBy
, alignValues
):
405 target
= Path(targetObj
)
407 shapekeys
= [Path(c
) for c
in shapekeyObjs
]
409 existingKeys
= getExistingShapeKeyPaths(target
)
410 shapekeys
= existingKeys
+ shapekeys
411 userSel
= [target
] + shapekeys
414 alignPath(path
, matchParts
, matchCriteria
, alignBy
, alignValues
)
416 addMissingSegs(userSel
, byPart
= (matchParts
!= "-None-"))
420 bIdxs
= bIdxs
.union(path
.getPartBoundaryIdxs())
423 path
.updatePartsList(sorted(list(bIdxs
)), byPart
= False)
425 #All will have the same part count by now
426 allToClose
= [all(path
.parts
[j
].isClosed
for path
in userSel
)
427 for j
in range(0, len(userSel
[0].parts
))]
429 #All paths will have the same no of splines with the same no of bezier points
431 for j
, part
in enumerate(path
.parts
):
432 part
.toClose
= allToClose
[j
]
436 if(len(existingKeys
) == 0):
437 target
.curve
.shape_key_add(name
= 'Basis')
439 addShapeKeys(target
.curve
, shapekeys
, space
)
443 if(path
.curve
!= target
.curve
):
444 safeRemoveObj(path
.curve
)
446 def getSplineSegs(spline
):
447 p
= spline
.bezier_points
448 segs
= [Segment(p
[i
-1].co
, p
[i
-1].handle_right
, p
[i
].handle_left
, p
[i
].co
) \
449 for i
in range(1, len(p
))]
450 if(spline
.use_cyclic_u
):
451 segs
.append(Segment(p
[-1].co
, p
[-1].handle_right
, p
[0].handle_left
, p
[0].co
))
454 def subdivideSeg(origSeg
, noSegs
):
460 segLen
= origSeg
.length
/ noSegs
462 for i
in range(0, noSegs
-1):
463 t
= float(i
+1) / noSegs
464 seg
= origSeg
.partialSeg(oldT
, t
)
468 seg
= origSeg
.partialSeg(oldT
, 1)
474 def getSubdivCntPerSeg(part
, toAddCnt
):
477 def __init__(self
, idx
, seg
):
480 self
.length
= seg
.length
483 def __init__(self
, part
):
485 self
.segCnt
= len(part
.getSegs())
486 for idx
, seg
in enumerate(part
.getSegs()):
487 self
.segList
.append(SegWrapper(idx
, seg
))
489 partWrapper
= PartWrapper(part
)
490 partLen
= part
.length
491 avgLen
= partLen
/ (partWrapper
.segCnt
+ toAddCnt
)
493 segsToDivide
= [sr
for sr
in partWrapper
.segList
if sr
.seg
.length
>= avgLen
]
494 segToDivideCnt
= len(segsToDivide
)
495 avgLen
= sum(sr
.seg
.length
for sr
in segsToDivide
) / (segToDivideCnt
+ toAddCnt
)
497 segsToDivide
= sorted(segsToDivide
, key
=lambda x
: x
.length
, reverse
= True)
499 cnts
= [0] * partWrapper
.segCnt
503 for i
in range(0, segToDivideCnt
):
504 segLen
= segsToDivide
[i
].seg
.length
506 divideCnt
= int(round(segLen
/avgLen
)) - 1
510 if((addedCnt
+ divideCnt
) >= toAddCnt
):
511 cnts
[segsToDivide
[i
].idx
] = toAddCnt
- addedCnt
515 cnts
[segsToDivide
[i
].idx
] = divideCnt
517 addedCnt
+= divideCnt
519 #TODO: Verify if needed
520 while(toAddCnt
> addedCnt
):
521 for i
in range(0, segToDivideCnt
):
522 cnts
[segsToDivide
[i
].idx
] += 1
524 if(toAddCnt
== addedCnt
):
529 #Just distribute equally; this is likely a rare condition. So why complicate?
530 def distributeCnt(maxSegCntsByPart
, startIdx
, extraCnt
):
532 elemCnt
= len(maxSegCntsByPart
) - startIdx
533 cntPerElem
= floor(extraCnt
/ elemCnt
)
534 remainder
= extraCnt
% elemCnt
536 for i
in range(startIdx
, len(maxSegCntsByPart
)):
537 maxSegCntsByPart
[i
] += cntPerElem
538 if(i
< remainder
+ startIdx
):
539 maxSegCntsByPart
[i
] += 1
541 #Make all the paths to have the maximum number of segments in the set
543 def addMissingSegs(selPaths
, byPart
):
544 maxSegCntsByPart
= []
548 sortedPaths
= sorted(selPaths
, key
= lambda c
: -len(c
.parts
))
550 for i
, path
in enumerate(sortedPaths
):
552 segCnt
= path
.getPartView().getSegCnt()
553 if(segCnt
> maxSegCnt
):
557 for j
, part
in enumerate(path
.parts
):
558 partSegCnt
= part
.getSegCnt()
559 resSegCnt
[i
].append(partSegCnt
)
562 if(j
== len(maxSegCntsByPart
)):
563 maxSegCntsByPart
.append(partSegCnt
)
565 #last part of this path, but other paths in set have more parts
566 elif((j
== len(path
.parts
) - 1) and
567 len(maxSegCntsByPart
) > len(path
.parts
)):
569 remainingSegs
= sum(maxSegCntsByPart
[j
:])
570 if(partSegCnt
<= remainingSegs
):
571 resSegCnt
[i
][j
] = remainingSegs
573 #This part has more segs than the sum of the remaining part segs
574 #So distribute the extra count
575 distributeCnt(maxSegCntsByPart
, j
, (partSegCnt
- remainingSegs
))
577 #Also, adjust the seg count of the last part of the previous
578 #segments that had fewer than max number of parts
579 for k
in range(0, i
):
580 if(len(sortedPaths
[k
].parts
) < len(maxSegCntsByPart
)):
581 totalSegs
= sum(maxSegCntsByPart
)
582 existingSegs
= sum(maxSegCntsByPart
[:len(sortedPaths
[k
].parts
)-1])
583 resSegCnt
[k
][-1] = totalSegs
- existingSegs
585 elif(partSegCnt
> maxSegCntsByPart
[j
]):
586 maxSegCntsByPart
[j
] = partSegCnt
587 for i
, path
in enumerate(sortedPaths
):
590 partView
= path
.getPartView()
591 segCnt
= partView
.getSegCnt()
592 diff
= maxSegCnt
- segCnt
595 cnts
= getSubdivCntPerSeg(partView
, diff
)
597 for j
in range(0, len(path
.parts
)):
600 for k
, seg
in enumerate(part
.getSegs()):
601 numSubdivs
= cnts
[cumulSegIdx
] + 1
602 newSegs
+= subdivideSeg(seg
, numSubdivs
)
605 path
.parts
[j
] = Part(path
, newSegs
, part
.isClosed
)
607 for j
in range(0, len(path
.parts
)):
611 partSegCnt
= part
.getSegCnt()
613 #TODO: Adding everything in the last part?
614 if(j
== (len(path
.parts
)-1) and
615 len(maxSegCntsByPart
) > len(path
.parts
)):
616 diff
= resSegCnt
[i
][j
] - partSegCnt
618 diff
= maxSegCntsByPart
[j
] - partSegCnt
621 cnts
= getSubdivCntPerSeg(part
, diff
)
623 for k
, seg
in enumerate(part
.getSegs()):
625 subdivCnt
= cnts
[k
] + 1 #1 for the existing one
626 newSegs
+= subdivideSeg(seg
, subdivCnt
)
628 #isClosed won't be used, but let's update anyway
629 path
.parts
[j
] = Part(path
, newSegs
, part
.isClosed
)
631 #TODO: Simplify (Not very readable)
632 def alignPath(path
, matchParts
, matchCriteria
, alignBy
, alignValues
):
634 parts
= path
.parts
[:]
636 if(matchParts
== 'custom'):
637 fnMap
= {'vCnt' : lambda part
: -1 * part
.getSegCnt(), \
638 'bbArea': lambda part
: -1 * part
.bboxSurfaceArea(worldSpace
= True), \
639 'bbHeight' : lambda part
: -1 * part
.getBBHeight(worldSpace
= True), \
640 'bbWidth' : lambda part
: -1 * part
.getBBWidth(worldSpace
= True), \
641 'bbDepth' : lambda part
: -1 * part
.getBBDepth(worldSpace
= True)
644 for criterion
in matchCriteria
:
645 fn
= fnMap
.get(criterion
)
647 minmax
= criterion
[:3] == 'max' #0 if min; 1 if max
648 axisIdx
= ord(criterion
[3:]) - ord('X')
650 fn
= eval('lambda part: part.getBBox(worldSpace = True)[' + \
651 str(minmax
) + '][' + str(axisIdx
) + ']')
653 matchPartCmpFns
.append(fn
)
655 def comparer(left
, right
):
656 for fn
in matchPartCmpFns
:
660 if(floatCmpWithMargin(a
, b
)):
663 return (a
> b
) - ( a
< b
) #No cmp in python3
667 parts
= sorted(parts
, key
= cmp_to_key(comparer
))
670 if(alignBy
== 'vertCo'):
671 def evalCmp(criteria
, pt1
, pt2
):
672 if(len(criteria
) == 0):
675 minmax
= criteria
[0][0]
676 axisIdx
= criteria
[0][1]
680 if(floatCmpWithMargin(val1
, val2
)):
681 criteria
= criteria
[:]
683 return evalCmp(criteria
, pt1
, pt2
)
685 return val1
< val2
if minmax
== 'min' else val1
> val2
687 alignCri
= [[a
[:3], ord(a
[3:]) - ord('X')] for a
in alignValues
]
688 alignCmpFn
= lambda pt1
, pt2
, curve
: (evalCmp(alignCri
, \
689 curve
.matrix_world
@ pt1
, curve
.matrix_world
@ pt2
))
694 for i
in range(0, len(parts
)):
695 #Only truly closed parts
696 if(alignCmpFn
!= None and parts
[i
].isClosed
):
697 for j
in range(0, parts
[i
].getSegCnt()):
698 seg
= parts
[i
].getSeg(j
)
699 if(j
== 0 or alignCmpFn(seg
.start
, startPt
, path
.curve
)):
703 path
.parts
[i
]= Part(path
, parts
[i
].getSegsCopy(startIdx
, None) + \
704 parts
[i
].getSegsCopy(None, startIdx
), parts
[i
].isClosed
)
706 path
.parts
[i
] = parts
[i
]
708 #TODO: Other shape key attributes like interpolation...?
709 def getExistingShapeKeyPaths(path
):
713 if(obj
.data
.shape_keys
!= None):
714 keyblocks
= obj
.data
.shape_keys
.key_blocks
[:]
715 for key
in keyblocks
:
716 datacopy
= obj
.data
.copy()
718 for spline
in datacopy
.splines
:
719 for pt
in spline
.bezier_points
:
720 pt
.co
= key
.data
[i
].co
721 pt
.handle_left
= key
.data
[i
].handle_left
722 pt
.handle_right
= key
.data
[i
].handle_right
724 paths
.append(Path(obj
, datacopy
, key
.name
))
727 def addShapeKeys(curve
, paths
, space
):
729 key
= curve
.shape_key_add(name
= path
.name
)
730 pts
= [pt
for pset
in path
.getBezierPtsBySpline() for pt
in pset
]
731 for i
, pt
in enumerate(pts
):
732 if(space
== 'worldspace'):
733 pt
= [curve
.matrix_world
.inverted() @ (path
.curve
.matrix_world
@ p
) for p
in pt
]
734 key
.data
[i
].co
= pt
[0]
735 key
.data
[i
].handle_left
= pt
[1]
736 key
.data
[i
].handle_right
= pt
[2]
739 def safeRemoveObj(obj
):
741 collections
= obj
.users_collection
743 for c
in collections
:
744 c
.objects
.unlink(obj
)
746 if(obj
.name
in bpy
.context
.scene
.collection
.objects
):
747 bpy
.context
.scene
.collection
.objects
.unlink(obj
)
749 if(obj
.data
.users
== 1):
750 if(obj
.type == 'CURVE'):
751 bpy
.data
.curves
.remove(obj
.data
) #This also removes object?
752 elif(obj
.type == 'MESH'):
753 bpy
.data
.meshes
.remove(obj
.data
)
755 bpy
.data
.objects
.remove(obj
)
760 def markVertHandler(self
, context
):
762 bpy
.ops
.wm
.mark_vertex()
765 #################### UI and Registration ####################
767 class AssignShapeKeysOp(Operator
):
768 bl_idname
= "object.assign_shape_keys"
769 bl_label
= "Assign Shape Keys"
770 bl_options
= {'REGISTER', 'UNDO'}
772 def execute(self
, context
):
773 params
= context
.window_manager
.AssignShapeKeyParams
774 removeOriginal
= params
.removeOriginal
777 matchParts
= params
.matchParts
778 matchCri1
= params
.matchCri1
779 matchCri2
= params
.matchCri2
780 matchCri3
= params
.matchCri3
782 alignBy
= params
.alignCos
783 alignVal1
= params
.alignVal1
784 alignVal2
= params
.alignVal2
785 alignVal3
= params
.alignVal3
787 targetObj
= bpy
.context
.active_object
788 shapekeyObjs
= [obj
for obj
in bpy
.context
.selected_objects
if isBezier(obj
) \
789 and obj
!= targetObj
]
791 if(targetObj
!= None and isBezier(targetObj
) and len(shapekeyObjs
) > 0):
792 main(targetObj
, shapekeyObjs
, removeOriginal
, space
, \
793 matchParts
, [matchCri1
, matchCri2
, matchCri3
], \
794 alignBy
, [alignVal1
, alignVal2
, alignVal3
])
799 class MarkerController
:
800 drawHandlerRef
= None
802 ptColor
= (0, .8, .8, 1)
804 def createSMMap(self
, context
):
805 objs
= context
.selected_objects
808 if(not isBezier(curve
)):
811 smMap
[curve
.name
] = {}
812 mw
= curve
.matrix_world
813 for splineIdx
, spline
in enumerate(curve
.data
.splines
):
814 if(not spline
.use_cyclic_u
):
817 #initialize to the curr start vert co and idx
818 smMap
[curve
.name
][splineIdx
] = \
819 [mw
@ curve
.data
.splines
[splineIdx
].bezier_points
[0].co
, 0]
821 for pt
in spline
.bezier_points
:
822 pt
.select_control_point
= False
824 if(len(smMap
[curve
.name
]) == 0):
825 del smMap
[curve
.name
]
829 def createBatch(self
, context
):
830 positions
= [s
[0] for cn
in self
.smMap
.values() for s
in cn
.values()]
831 colors
= [MarkerController
.ptColor
for i
in range(0, len(positions
))]
833 self
.batch
= batch_for_shader(self
.shader
, \
834 "POINTS", {"pos": positions
, "color": colors
})
837 context
.area
.tag_redraw()
839 def drawHandler(self
):
840 bgl
.glPointSize(MarkerController
.defPointSize
)
841 self
.batch
.draw(self
.shader
)
843 def removeMarkers(self
, context
):
844 if(MarkerController
.drawHandlerRef
!= None):
845 bpy
.types
.SpaceView3D
.draw_handler_remove(MarkerController
.drawHandlerRef
, \
848 if(context
.area
and hasattr(context
.space_data
, 'region_3d')):
849 context
.area
.tag_redraw()
851 MarkerController
.drawHandlerRef
= None
855 def __init__(self
, context
):
856 self
.smMap
= self
.createSMMap(context
)
857 self
.shader
= gpu
.shader
.from_builtin('3D_FLAT_COLOR')
860 MarkerController
.drawHandlerRef
= \
861 bpy
.types
.SpaceView3D
.draw_handler_add(self
.drawHandler
, \
862 (), "WINDOW", "POST_VIEW")
864 self
.createBatch(context
)
866 def saveStartVerts(self
):
867 for curveName
in self
.smMap
.keys():
868 curve
= bpy
.data
.objects
[curveName
]
869 splines
= curve
.data
.splines
870 spMap
= self
.smMap
[curveName
]
872 for splineIdx
in spMap
.keys():
873 markerInfo
= spMap
[splineIdx
]
874 if(markerInfo
[1] != 0):
875 pts
= splines
[splineIdx
].bezier_points
876 loc
, idx
= markerInfo
[0], markerInfo
[1]
879 ptCopy
= [[p
.co
.copy(), p
.handle_right
.copy(), \
880 p
.handle_left
.copy(), p
.handle_right_type
, \
881 p
.handle_left_type
] for p
in pts
]
883 for i
, pt
in enumerate(pts
):
884 srcIdx
= (idx
+ i
) % cnt
887 #Must set the types first
888 pt
.handle_right_type
= p
[3]
889 pt
.handle_left_type
= p
[4]
891 pt
.handle_right
= p
[1]
892 pt
.handle_left
= p
[2]
894 def updateSMMap(self
):
895 for curveName
in self
.smMap
.keys():
896 curve
= bpy
.data
.objects
[curveName
]
897 spMap
= self
.smMap
[curveName
]
898 mw
= curve
.matrix_world
900 for splineIdx
in spMap
.keys():
901 markerInfo
= spMap
[splineIdx
]
902 loc
, idx
= markerInfo
[0], markerInfo
[1]
903 pts
= curve
.data
.splines
[splineIdx
].bezier_points
905 selIdxs
= [x
for x
in range(0, len(pts
)) \
906 if pts
[x
].select_control_point
== True]
908 selIdx
= selIdxs
[0] if(len(selIdxs
) > 0 ) else idx
909 co
= mw
@ pts
[selIdx
].co
910 self
.smMap
[curveName
][splineIdx
] = [co
, selIdx
]
912 def deselectAll(self
):
913 for curveName
in self
.smMap
.keys():
914 curve
= bpy
.data
.objects
[curveName
]
915 for spline
in curve
.data
.splines
:
916 for pt
in spline
.bezier_points
:
917 pt
.select_control_point
= False
919 def getSpaces3D(context
):
920 areas3d
= [area
for area
in context
.window
.screen
.areas \
921 if area
.type == 'VIEW_3D']
923 return [s
for a
in areas3d
for s
in a
.spaces
if s
.type == 'VIEW_3D']
925 def hideHandles(context
):
927 spaces
= MarkerController
.getSpaces3D(context
)
929 if(hasattr(s
.overlay
, 'show_curve_handles')):
930 states
.append(s
.overlay
.show_curve_handles
)
931 s
.overlay
.show_curve_handles
= False
932 elif(hasattr(s
.overlay
, 'display_handle')): # 2.90
933 states
.append(s
.overlay
.display_handle
)
934 s
.overlay
.display_handle
= 'NONE'
937 def resetShowHandleState(context
, handleStates
):
938 spaces
= MarkerController
.getSpaces3D(context
)
939 for i
, s
in enumerate(spaces
):
940 if(hasattr(s
.overlay
, 'show_curve_handles')):
941 s
.overlay
.show_curve_handles
= handleStates
[i
]
942 elif(hasattr(s
.overlay
, 'display_handle')): # 2.90
943 s
.overlay
.display_handle
= handleStates
[i
]
946 class ModalMarkSegStartOp(Operator
):
947 bl_description
= "Mark Vertex"
948 bl_idname
= "wm.mark_vertex"
949 bl_label
= "Mark Start Vertex"
951 def cleanup(self
, context
):
952 wm
= context
.window_manager
953 wm
.event_timer_remove(self
._timer
)
954 self
.markerState
.removeMarkers(context
)
955 MarkerController
.resetShowHandleState(context
, self
.handleStates
)
956 context
.window_manager
.AssignShapeKeyParams
.markVertex
= False
958 def modal (self
, context
, event
):
959 params
= context
.window_manager
.AssignShapeKeyParams
961 if(context
.mode
== 'OBJECT' or event
.type == "ESC" or\
962 not context
.window_manager
.AssignShapeKeyParams
.markVertex
):
963 self
.cleanup(context
)
966 elif(event
.type == "RET"):
967 self
.markerState
.saveStartVerts()
968 self
.cleanup(context
)
971 if(event
.type == 'TIMER'):
972 self
.markerState
.updateSMMap()
973 self
.markerState
.createBatch(context
)
975 return {"PASS_THROUGH"}
977 def execute(self
, context
):
978 #TODO: Why such small step?
979 self
._timer
= context
.window_manager
.event_timer_add(time_step
= 0.0001, \
980 window
= context
.window
)
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", \
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 alignCos
: 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',
1008 alignVal1
: EnumProperty(name
="Value 1",
1009 items
= alignList
, default
= 'minX', description
='First align criterion')
1011 alignVal2
: EnumProperty(name
="Value 2",
1012 items
= alignList
, default
= 'maxY', description
='Second align criterion')
1014 alignVal3
: EnumProperty(name
="Value 3",
1015 items
= alignList
, 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'}
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'):
1059 row
.prop(params
, "removeOriginal")
1062 row
.prop(params
, "space")
1065 row
.prop(params
, "alignCos")
1067 if(params
.alignCos
== 'vertCo'):
1069 row
.prop(params
, "alignVal1")
1070 row
.prop(params
, "alignVal2")
1071 row
.prop(params
, "alignVal3")
1074 row
.prop(params
, "matchParts")
1076 if(params
.matchParts
== 'custom'):
1078 row
.prop(params
, "matchCri1")
1079 row
.prop(params
, "matchCri2")
1080 row
.prop(params
, "matchCri3")
1083 row
.operator("object.assign_shape_keys")
1085 col
.prop(params
, "markVertex", \
1089 def updatePanel(self
, context
):
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",
1108 update
= updatePanel
1111 def draw(self
, context
):
1112 layout
= self
.layout
1115 col
.label(text
="Tab Category:")
1116 col
.prop(self
, "category", text
="")
1118 # registering and menu integration
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
)
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__":