3 # This Blender add-on assigns one or more Bezier Curves as shape keys to another
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
24 "name": "Assign Shape Keys",
25 "author": "Shrinivas Kulkarni",
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
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
)))
62 #pts[0] - start, pts[1] - ctrl1, pts[2] - ctrl2, , pts[3] - end
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
):
70 mid
= Segment
.pointAtT(pts
, t1_5
)
71 l
= (end
- start
).length
72 l2
= (mid
- start
).length
+ (end
- mid
).length
74 return (Segment
.getSegLenRecurs(pts
, start
, mid
, t1
, t1_5
, error
) +
75 Segment
.getSegLenRecurs(pts
, mid
, end
, t1_5
, t2
, error
))
78 def __init__(self
, start
, ctrl1
, ctrl2
, 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
]
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
)
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
) + \
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)]
147 for i
in range(0, 3):
151 solns
.append(0)#Independent of t so lets take the starting pt
153 solns
.append(c
[i
] / b
[i
])
155 rootFact
= b
[i
] * b
[i
] - 4 * a
[i
] * c
[i
]
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
):
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
175 def __init__(self
, parent
, segs
, isClosed
):
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
)
187 self
.bboxWorldSpace
= None
189 def getSeg(self
, idx
):
190 return self
.segs
[idx
]
195 def getSegsCopy(self
, start
, end
):
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):
207 if(worldSpace
and self
.bboxWorldSpace
!= None):
208 return self
.bboxWorldSpace
210 leftBotBack_rgtTopFront
= [[None]*3,[None]*3]
212 for seg
in self
.segs
:
215 bb
= seg
.bbox(self
.parent
.curve
.matrix_world
)
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
]
230 self
.bboxWorldSpace
= leftBotBack_rgtTopFront
232 self
.bbox
= leftBotBack_rgtTopFront
234 return leftBotBack_rgtTopFront
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
])
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
)
261 return len(self
.segs
)
263 def getBezierPtsInfo(self
):
267 for j
, seg
in enumerate(self
.getSegs()):
270 handleRight
= seg
.ctrl1
274 handleLeft
= self
.getSeg(-1).ctrl2
278 handleLeft
= prevSeg
.ctrl2
280 bezierPtsInfo
.append([pt
, handleLeft
, handleRight
])
283 if(self
.toClose
== True):
284 bezierPtsInfo
[-1][2] = seg
.ctrl1
286 bezierPtsInfo
.append([prevSeg
.end
, prevSeg
.ctrl2
, prevSeg
.end
])
291 return str(self
.length
)
295 def __init__(self
, curve
, objData
= None, name
= None):
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)
315 def getPartBoundaryIdxs(self
):
320 cumulCnt
+= p
.getSegCnt()
321 cumulCntList
.add(cumulCnt
)
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]
332 for i
in range(0, len(segCntsPerPart
)):
336 currIdx
= segCntsPerPart
[i
-1]
338 nextIdx
= segCntsPerPart
[i
]
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]):
352 if(partIdx
< len(oldParts
)):
353 currPart
= oldParts
[partIdx
]
355 def getBezierPtsBySpline(self
):
358 for i
, part
in enumerate(self
.parts
):
359 data
.append(part
.getBezierPtsInfo())
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'
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
)
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
)):
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):
408 shapekeys
= getExistingShapeKeyPaths(target
) + shapekeys
409 userSel
= [target
] + shapekeys
412 alignPath(path
, matchParts
, matchCriteria
, alignBy
, alignValues
)
414 addMissingSegs(userSel
, byPart
= (matchParts
!= "-None-"))
418 bIdxs
= bIdxs
.union(path
.getPartBoundaryIdxs())
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
429 for j
, part
in enumerate(path
.parts
):
430 part
.toClose
= allToClose
[j
]
434 target
.curve
.shape_key_add(name
= 'Basis')
436 addShapeKeys(target
.curve
, shapekeys
, space
)
440 if(path
.curve
!= target
.curve
):
441 safeRemoveCurveObj(path
.curve
)
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
))
453 def subdivideSeg(origSeg
, noSegs
):
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
)
467 seg
= origSeg
.partialSeg(oldT
, 1)
473 def getSubdivCntPerSeg(part
, toAddCnt
):
476 def __init__(self
, idx
, seg
):
479 self
.length
= seg
.length
482 def __init__(self
, part
):
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
502 for i
in range(0, segToDivideCnt
):
503 segLen
= segsToDivide
[i
].seg
.length
505 divideCnt
= int(round(segLen
/avgLen
)) - 1
509 if((addedCnt
+ divideCnt
) >= toAddCnt
):
510 cnts
[segsToDivide
[i
].idx
] = toAddCnt
- addedCnt
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
523 if(toAddCnt
== addedCnt
):
528 #Just distribute equally; this is likely a rare condition. So why complicate?
529 def distributeCnt(maxSegCntsByPart
, startIdx
, extraCnt
):
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
542 def addMissingSegs(selPaths
, byPart
):
543 maxSegCntsByPart
= []
547 sortedPaths
= sorted(selPaths
, key
= lambda c
: -len(c
.parts
))
549 for i
, path
in enumerate(sortedPaths
):
551 segCnt
= path
.getPartView().getSegCnt()
552 if(segCnt
> maxSegCnt
):
556 for j
, part
in enumerate(path
.parts
):
557 partSegCnt
= part
.getSegCnt()
558 resSegCnt
[i
].append(partSegCnt
)
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
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
):
589 partView
= path
.getPartView()
590 segCnt
= partView
.getSegCnt()
591 diff
= maxSegCnt
- segCnt
594 cnts
= getSubdivCntPerSeg(partView
, diff
)
596 for j
in range(0, len(path
.parts
)):
599 for k
, seg
in enumerate(part
.getSegs()):
600 numSubdivs
= cnts
[cumulSegIdx
] + 1
601 newSegs
+= subdivideSeg(seg
, numSubdivs
)
604 path
.parts
[j
] = Part(path
, newSegs
, part
.isClosed
)
606 for j
in range(0, len(path
.parts
)):
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
617 diff
= maxSegCntsByPart
[j
] - partSegCnt
620 cnts
= getSubdivCntPerSeg(part
, diff
)
622 for k
, seg
in enumerate(part
.getSegs()):
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)
643 for criterion
in matchCriteria
:
644 fn
= fnMap
.get(criterion
)
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
:
659 if(floatCmpWithMargin(a
, b
)):
662 return (a
> b
) - ( a
< b
) #No cmp in python3
666 parts
= sorted(parts
, key
= cmp_to_key(comparer
))
669 if(alignBy
== 'vertCo'):
670 def evalCmp(criteria
, pt1
, pt2
):
671 if(len(criteria
) == 0):
674 minmax
= criteria
[0][0]
675 axisIdx
= criteria
[0][1]
679 if(floatCmpWithMargin(val1
, val2
)):
680 criteria
= criteria
[:]
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
))
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
)):
702 path
.parts
[i
]= Part(path
, parts
[i
].getSegsCopy(startIdx
, None) + \
703 parts
[i
].getSegsCopy(None, startIdx
), parts
[i
].isClosed
)
705 path
.parts
[i
] = parts
[i
]
707 #TODO: Other shape key attributes like interpolation...?
708 def getExistingShapeKeyPaths(path
):
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()
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
723 paths
.append(Path(obj
, datacopy
, key
.name
))
726 def addShapeKeys(curve
, paths
, space
):
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]
738 def safeRemoveCurveObj(obj
):
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
)
759 def markVertHandler(self
, context
):
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
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
])
793 class MarkerController
:
794 drawHandlerRef
= None
796 ptColor
= (0, .8, .8, 1)
798 def createSMMap(self
, context
):
799 objs
= context
.selected_objects
802 if(not isBezier(curve
)):
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
):
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
]
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
})
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
, \
842 if(context
.area
and hasattr(context
.space_data
, 'region_3d')):
843 context
.area
.tag_redraw()
845 MarkerController
.drawHandlerRef
= None
849 def __init__(self
, context
):
850 self
.smMap
= self
.createSMMap(context
)
851 self
.shader
= gpu
.shader
.from_builtin('3D_FLAT_COLOR')
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]
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
881 #Must set the types first
882 pt
.handle_right_type
= p
[3]
883 pt
.handle_left_type
= p
[4]
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
):
921 spaces
= MarkerController
.getSpaces3D(context
)
923 states
.append(s
.overlay
.show_curve_handles
)
924 s
.overlay
.show_curve_handles
= False
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
)
953 elif(event
.type == "RET"):
954 self
.markerState
.saveStartVerts()
955 self
.cleanup(context
)
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
)
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 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',
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'}
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
, "alignList")
1067 if(params
.alignList
== '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__":