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: MIT (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
16 from collections
import OrderedDict
17 from mathutils
import Vector
18 from math
import sqrt
, floor
19 from functools
import cmp_to_key
23 "name": "Assign Shape Keys",
24 "author": "Shrinivas Kulkarni",
26 "location": "Properties > Active Tool and Workspace Settings > Assign Shape Keys",
27 "description": "Assigns one or more Bezier curves as shape keys to another Bezier curve",
29 "wiki_url": "https://github.com/Shriinivas/assignshapekey/blob/master/README.md",
30 "blender": (2, 80, 0),
33 matchList
= [('vCnt', 'Vertex Count', 'Match by vertex count'),
34 ('bbArea', 'Area', 'Match by surface area of the bounding box'), \
35 ('bbHeight', 'Height', 'Match by bounding box height'), \
36 ('bbWidth', 'Width', 'Match by bounding box width'),
37 ('bbDepth', 'Depth', 'Match by bounding box depth'),
38 ('minX', 'Min X', 'Match by bounding bon Min X'),
39 ('maxX', 'Max X', 'Match by bounding bon Max X'),
40 ('minY', 'Min Y', 'Match by bounding bon Min Y'),
41 ('maxY', 'Max Y', 'Match by bounding bon Max Y'),
42 ('minZ', 'Min Z', 'Match by bounding bon Min Z'),
43 ('maxZ', 'Max Z', 'Match by bounding bon Max Z')]
45 DEF_ERR_MARGIN
= 0.0001
48 return obj
.type == 'CURVE' and len(obj
.data
.splines
) > 0 \
49 and obj
.data
.splines
[0].type == 'BEZIER'
51 #Avoid errors due to floating point conversions/comparisons
52 #TODO: return -1, 0, 1
53 def floatCmpWithMargin(float1
, float2
, margin
= DEF_ERR_MARGIN
):
54 return abs(float1
- float2
) < margin
56 def vectCmpWithMargin(v1
, v2
, margin
= DEF_ERR_MARGIN
):
57 return all(floatCmpWithMargin(v1
[i
], v2
[i
], margin
) for i
in range(0, len(v1
)))
61 #pts[0] - start, pts[1] - ctrl1, pts[2] - ctrl2, , pts[3] - end
63 return pts
[0] + t
* (3 * (pts
[1] - pts
[0]) +
64 t
* (3 * (pts
[0] + pts
[2]) - 6 * pts
[1] +
65 t
* (-pts
[0] + 3 * (pts
[1] - pts
[2]) + pts
[3])))
67 def getSegLenRecurs(pts
, start
, end
, t1
= 0, t2
= 1, error
= DEF_ERR_MARGIN
):
69 mid
= Segment
.pointAtT(pts
, t1_5
)
70 l
= (end
- start
).length
71 l2
= (mid
- start
).length
+ (end
- mid
).length
73 return (Segment
.getSegLenRecurs(pts
, start
, mid
, t1
, t1_5
, error
) +
74 Segment
.getSegLenRecurs(pts
, mid
, end
, t1_5
, t2
, error
))
77 def __init__(self
, start
, ctrl1
, ctrl2
, end
):
82 pts
= [start
, ctrl1
, ctrl2
, end
]
83 self
.length
= Segment
.getSegLenRecurs(pts
, start
, end
)
85 #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
86 def partialSeg(self
, t0
, t1
):
87 pts
= [self
.start
, self
.ctrl1
, self
.ctrl2
, self
.end
]
94 #Let's make at least the line segments of predictable length :)
95 if(pts
[0] == pts
[1] and pts
[2] == pts
[3]):
96 pt0
= Vector([(1 - t0
) * pts
[0][i
] + t0
* pts
[2][i
] for i
in range(0, 3)])
97 pt1
= Vector([(1 - t1
) * pts
[0][i
] + t1
* pts
[2][i
] for i
in range(0, 3)])
98 return Segment(pt0
, pt0
, pt1
, pt1
)
103 qa
= [pts
[0][i
]*u0
*u0
+ pts
[1][i
]*2*t0
*u0
+ pts
[2][i
]*t0
*t0
for i
in range(0, 3)]
104 qb
= [pts
[0][i
]*u1
*u1
+ pts
[1][i
]*2*t1
*u1
+ pts
[2][i
]*t1
*t1
for i
in range(0, 3)]
105 qc
= [pts
[1][i
]*u0
*u0
+ pts
[2][i
]*2*t0
*u0
+ pts
[3][i
]*t0
*t0
for i
in range(0, 3)]
106 qd
= [pts
[1][i
]*u1
*u1
+ pts
[2][i
]*2*t1
*u1
+ pts
[3][i
]*t1
*t1
for i
in range(0, 3)]
108 pta
= Vector([qa
[i
]*u0
+ qc
[i
]*t0
for i
in range(0, 3)])
109 ptb
= Vector([qa
[i
]*u1
+ qc
[i
]*t1
for i
in range(0, 3)])
110 ptc
= Vector([qb
[i
]*u0
+ qd
[i
]*t0
for i
in range(0, 3)])
111 ptd
= Vector([qb
[i
]*u1
+ qd
[i
]*t1
for i
in range(0, 3)])
113 return Segment(pta
, ptb
, ptc
, ptd
)
115 #see https://stackoverflow.com/questions/24809978/calculating-the-bounding-box-of-cubic-bezier-curve
116 #(3 D - 9 C + 9 B - 3 A) t^2 + (6 A - 12 B + 6 C) t + 3 (B - A)
117 #pts[0] - start, pts[1] - ctrl1, pts[2] - ctrl2, , pts[3] - end
118 #TODO: Return Vectors to make world space calculations consistent
119 def bbox(self
, mw
= None):
120 def evalBez(AA
, BB
, CC
, DD
, t
):
121 return AA
* (1 - t
) * (1 - t
) * (1 - t
) + \
122 3 * BB
* t
* (1 - t
) * (1 - t
) + \
123 3 * CC
* t
* t
* (1 - t
) + \
137 MINXYZ
= [min([A
[i
], D
[i
]]) for i
in range(0, 3)]
138 MAXXYZ
= [max([A
[i
], D
[i
]]) for i
in range(0, 3)]
139 leftBotBack_rgtTopFront
= [MINXYZ
, MAXXYZ
]
141 a
= [3 * D
[i
] - 9 * C
[i
] + 9 * B
[i
] - 3 * A
[i
] for i
in range(0, 3)]
142 b
= [6 * A
[i
] - 12 * B
[i
] + 6 * C
[i
] for i
in range(0, 3)]
143 c
= [3 * (B
[i
] - A
[i
]) for i
in range(0, 3)]
146 for i
in range(0, 3):
150 solns
.append(0)#Independent of t so lets take the starting pt
152 solns
.append(c
[i
] / b
[i
])
154 rootFact
= b
[i
] * b
[i
] - 4 * a
[i
] * c
[i
]
156 #Two solutions with + and - sqrt
157 solns
.append((-b
[i
] + sqrt(rootFact
)) / (2 * a
[i
]))
158 solns
.append((-b
[i
] - sqrt(rootFact
)) / (2 * a
[i
]))
159 solnsxyz
.append(solns
)
161 for i
, soln
in enumerate(solnsxyz
):
162 for j
, t
in enumerate(soln
):
164 co
= evalBez(A
[i
], B
[i
], C
[i
], D
[i
], t
)
165 if(co
< leftBotBack_rgtTopFront
[0][i
]):
166 leftBotBack_rgtTopFront
[0][i
] = co
167 if(co
> leftBotBack_rgtTopFront
[1][i
]):
168 leftBotBack_rgtTopFront
[1][i
] = co
170 return leftBotBack_rgtTopFront
174 def __init__(self
, parent
, segs
, isClosed
):
179 self
.isClosed
= isClosed
181 #Indicates if this should be closed based on its counterparts in other paths
182 self
.toClose
= isClosed
184 self
.length
= sum(seg
.length
for seg
in self
.segs
)
186 self
.bboxWorldSpace
= None
188 def getSeg(self
, idx
):
189 return self
.segs
[idx
]
194 def getSegsCopy(self
, start
, end
):
199 return self
.segs
[start
:end
]
201 def getBBox(self
, worldSpace
):
202 #Avoid frequent calculations, as this will be called in compare method
203 if(not worldSpace
and self
.bbox
!= None):
206 if(worldSpace
and self
.bboxWorldSpace
!= None):
207 return self
.bboxWorldSpace
209 leftBotBack_rgtTopFront
= [[None]*3,[None]*3]
211 for seg
in self
.segs
:
214 bb
= seg
.bbox(self
.parent
.curve
.matrix_world
)
218 for i
in range(0, 3):
219 if (leftBotBack_rgtTopFront
[0][i
] == None or \
220 bb
[0][i
] < leftBotBack_rgtTopFront
[0][i
]):
221 leftBotBack_rgtTopFront
[0][i
] = bb
[0][i
]
223 for i
in range(0, 3):
224 if (leftBotBack_rgtTopFront
[1][i
] == None or \
225 bb
[1][i
] > leftBotBack_rgtTopFront
[1][i
]):
226 leftBotBack_rgtTopFront
[1][i
] = bb
[1][i
]
229 self
.bboxWorldSpace
= leftBotBack_rgtTopFront
231 self
.bbox
= leftBotBack_rgtTopFront
233 return leftBotBack_rgtTopFront
236 def getBBDiff(self
, axisIdx
, worldSpace
):
237 obj
= self
.parent
.curve
238 bbox
= self
.getBBox(worldSpace
)
239 diff
= abs(bbox
[1][axisIdx
] - bbox
[0][axisIdx
])
242 def getBBWidth(self
, worldSpace
):
243 return self
.getBBDiff(0, worldSpace
)
245 def getBBHeight(self
, worldSpace
):
246 return self
.getBBDiff(1, worldSpace
)
248 def getBBDepth(self
, worldSpace
):
249 return self
.getBBDiff(2, worldSpace
)
251 def bboxSurfaceArea(self
, worldSpace
):
252 leftBotBack_rgtTopFront
= self
.getBBox(worldSpace
)
253 w
= abs( leftBotBack_rgtTopFront
[1][0] - leftBotBack_rgtTopFront
[0][0] )
254 l
= abs( leftBotBack_rgtTopFront
[1][1] - leftBotBack_rgtTopFront
[0][1] )
255 d
= abs( leftBotBack_rgtTopFront
[1][2] - leftBotBack_rgtTopFront
[0][2] )
257 return 2 * (w
* l
+ w
* d
+ l
* d
)
260 return len(self
.segs
)
262 def getBezierPtsInfo(self
):
266 for j
, seg
in enumerate(self
.getSegs()):
269 handleRight
= seg
.ctrl1
273 handleLeft
= self
.getSeg(-1).ctrl2
277 handleLeft
= prevSeg
.ctrl2
279 bezierPtsInfo
.append([pt
, handleLeft
, handleRight
])
282 if(self
.toClose
== True):
283 bezierPtsInfo
[-1][2] = seg
.ctrl1
285 bezierPtsInfo
.append([prevSeg
.end
, prevSeg
.ctrl2
, prevSeg
.end
])
290 return str(self
.length
)
294 def __init__(self
, curve
, objData
= None, name
= None):
305 self
.parts
= [Part(self
, getSplineSegs(s
), s
.use_cyclic_u
) for s
in objData
.splines
]
307 def getPartCnt(self
):
308 return len(self
.parts
)
310 def getPartView(self
):
311 p
= Part(self
, [seg
for part
in self
.parts
for seg
in part
.getSegs()], None)
314 def getPartBoundaryIdxs(self
):
319 cumulCnt
+= p
.getSegCnt()
320 cumulCntList
.add(cumulCnt
)
324 def updatePartsList(self
, segCntsPerPart
, byPart
):
325 monolithicSegList
= [seg
for part
in self
.parts
for seg
in part
.getSegs()]
326 oldParts
= self
.parts
[:]
327 currPart
= oldParts
[0]
331 for i
in range(0, len(segCntsPerPart
)):
335 currIdx
= segCntsPerPart
[i
-1]
337 nextIdx
= segCntsPerPart
[i
]
340 if(vectCmpWithMargin(monolithicSegList
[currIdx
].start
, \
341 currPart
.getSegs()[0].start
) and \
342 vectCmpWithMargin(monolithicSegList
[nextIdx
-1].end
, \
343 currPart
.getSegs()[-1].end
)):
344 isClosed
= currPart
.isClosed
346 self
.parts
.append(Part(self
, \
347 monolithicSegList
[currIdx
:nextIdx
], isClosed
))
349 if(monolithicSegList
[nextIdx
-1] == currPart
.getSegs()[-1]):
351 if(partIdx
< len(oldParts
)):
352 currPart
= oldParts
[partIdx
]
354 def getBezierPtsBySpline(self
):
357 for i
, part
in enumerate(self
.parts
):
358 data
.append(part
.getBezierPtsInfo())
362 def getNewCurveData(self
):
364 newCurveData
= self
.curve
.data
.copy()
365 newCurveData
.splines
.clear()
367 splinesData
= self
.getBezierPtsBySpline()
369 for i
, newPoints
in enumerate(splinesData
):
371 spline
= newCurveData
.splines
.new('BEZIER')
372 spline
.bezier_points
.add(len(newPoints
)-1)
373 spline
.use_cyclic_u
= self
.parts
[i
].toClose
375 for j
in range(0, len(spline
.bezier_points
)):
376 newPoint
= newPoints
[j
]
377 spline
.bezier_points
[j
].co
= newPoint
[0]
378 spline
.bezier_points
[j
].handle_left
= newPoint
[1]
379 spline
.bezier_points
[j
].handle_right
= newPoint
[2]
380 spline
.bezier_points
[j
].handle_right_type
= 'FREE'
384 def updateCurve(self
):
385 curveData
= self
.curve
.data
386 #Remove existing shape keys first
387 if(curveData
.shape_keys
!= None):
388 keyblocks
= reversed(curveData
.shape_keys
.key_blocks
)
390 self
.curve
.shape_key_remove(sk
)
391 self
.curve
.data
= self
.getNewCurveData()
392 bpy
.data
.curves
.remove(curveData
)
394 def main(removeOriginal
, space
, matchParts
, matchCriteria
, alignBy
, alignValues
):
395 targetObj
= bpy
.context
.active_object
396 if(targetObj
== None or not isBezier(targetObj
)):
399 target
= Path(targetObj
)
401 shapekeys
= [Path(c
) for c
in bpy
.context
.selected_objects
if isBezier(c
) \
402 and c
!= bpy
.context
.active_object
]
404 if(len(shapekeys
) == 0):
407 shapekeys
= getExistingShapeKeyPaths(target
) + shapekeys
408 userSel
= [target
] + shapekeys
411 alignPath(path
, matchParts
, matchCriteria
, alignBy
, alignValues
)
413 addMissingSegs(userSel
, byPart
= (matchParts
!= "-None-"))
417 bIdxs
= bIdxs
.union(path
.getPartBoundaryIdxs())
420 path
.updatePartsList(sorted(list(bIdxs
)), byPart
= False)
422 #All will have the same part count by now
423 allToClose
= [all(path
.parts
[j
].isClosed
for path
in userSel
)
424 for j
in range(0, len(userSel
[0].parts
))]
426 #All paths will have the same no of splines with the same no of bezier points
428 for j
, part
in enumerate(path
.parts
):
429 part
.toClose
= allToClose
[j
]
433 target
.curve
.shape_key_add(name
= 'Basis')
435 addShapeKeys(target
.curve
, shapekeys
, space
)
439 if(path
.curve
!= target
.curve
):
440 safeRemoveCurveObj(path
.curve
)
444 def getSplineSegs(spline
):
445 p
= spline
.bezier_points
446 segs
= [Segment(p
[i
-1].co
, p
[i
-1].handle_right
, p
[i
].handle_left
, p
[i
].co
) \
447 for i
in range(1, len(p
))]
448 if(spline
.use_cyclic_u
):
449 segs
.append(Segment(p
[-1].co
, p
[-1].handle_right
, p
[0].handle_left
, p
[0].co
))
452 def subdivideSeg(origSeg
, noSegs
):
458 segLen
= origSeg
.length
/ noSegs
460 for i
in range(0, noSegs
-1):
461 t
= float(i
+1) / noSegs
462 seg
= origSeg
.partialSeg(oldT
, t
)
466 seg
= origSeg
.partialSeg(oldT
, 1)
472 def getSubdivCntPerSeg(part
, toAddCnt
):
475 def __init__(self
, idx
, seg
):
478 self
.length
= seg
.length
481 def __init__(self
, part
):
483 self
.segCnt
= len(part
.getSegs())
484 for idx
, seg
in enumerate(part
.getSegs()):
485 self
.segList
.append(SegWrapper(idx
, seg
))
487 partWrapper
= PartWrapper(part
)
488 partLen
= part
.length
489 avgLen
= partLen
/ (partWrapper
.segCnt
+ toAddCnt
)
491 segsToDivide
= [sr
for sr
in partWrapper
.segList
if sr
.seg
.length
>= avgLen
]
492 segToDivideCnt
= len(segsToDivide
)
493 avgLen
= sum(sr
.seg
.length
for sr
in segsToDivide
) / (segToDivideCnt
+ toAddCnt
)
495 segsToDivide
= sorted(segsToDivide
, key
=lambda x
: x
.length
, reverse
= True)
497 cnts
= [0] * partWrapper
.segCnt
501 for i
in range(0, segToDivideCnt
):
502 segLen
= segsToDivide
[i
].seg
.length
504 divideCnt
= int(round(segLen
/avgLen
)) - 1
508 if((addedCnt
+ divideCnt
) >= toAddCnt
):
509 cnts
[segsToDivide
[i
].idx
] = toAddCnt
- addedCnt
513 cnts
[segsToDivide
[i
].idx
] = divideCnt
515 addedCnt
+= divideCnt
517 #TODO: Verify if needed
518 while(toAddCnt
> addedCnt
):
519 for i
in range(0, segToDivideCnt
):
520 cnts
[segsToDivide
[i
].idx
] += 1
522 if(toAddCnt
== addedCnt
):
527 #Just distribute equally; this is likely a rare condition. So why complicate?
528 def distributeCnt(maxSegCntsByPart
, startIdx
, extraCnt
):
530 elemCnt
= len(maxSegCntsByPart
) - startIdx
531 cntPerElem
= floor(extraCnt
/ elemCnt
)
532 remainder
= extraCnt
% elemCnt
534 for i
in range(startIdx
, len(maxSegCntsByPart
)):
535 maxSegCntsByPart
[i
] += cntPerElem
536 if(i
< remainder
+ startIdx
):
537 maxSegCntsByPart
[i
] += 1
539 #Make all the paths to have the maximum number of segments in the set
541 def addMissingSegs(selPaths
, byPart
):
542 maxSegCntsByPart
= []
546 sortedPaths
= sorted(selPaths
, key
= lambda c
: -len(c
.parts
))
548 for i
, path
in enumerate(sortedPaths
):
550 segCnt
= path
.getPartView().getSegCnt()
551 if(segCnt
> maxSegCnt
):
555 for j
, part
in enumerate(path
.parts
):
556 partSegCnt
= part
.getSegCnt()
557 resSegCnt
[i
].append(partSegCnt
)
560 if(j
== len(maxSegCntsByPart
)):
561 maxSegCntsByPart
.append(partSegCnt
)
563 #last part of this path, but other paths in set have more parts
564 elif((j
== len(path
.parts
) - 1) and
565 len(maxSegCntsByPart
) > len(path
.parts
)):
567 remainingSegs
= sum(maxSegCntsByPart
[j
:])
568 if(partSegCnt
<= remainingSegs
):
569 resSegCnt
[i
][j
] = remainingSegs
571 #This part has more segs than the sum of the remaining part segs
572 #So distribute the extra count
573 distributeCnt(maxSegCntsByPart
, j
, (partSegCnt
- remainingSegs
))
575 #Also, adjust the seg count of the last part of the previous
576 #segments that had fewer than max number of parts
577 for k
in range(0, i
):
578 if(len(sortedPaths
[k
].parts
) < len(maxSegCntsByPart
)):
579 totalSegs
= sum(maxSegCntsByPart
)
580 existingSegs
= sum(maxSegCntsByPart
[:len(sortedPaths
[k
].parts
)-1])
581 resSegCnt
[k
][-1] = totalSegs
- existingSegs
583 elif(partSegCnt
> maxSegCntsByPart
[j
]):
584 maxSegCntsByPart
[j
] = partSegCnt
585 for i
, path
in enumerate(sortedPaths
):
588 partView
= path
.getPartView()
589 segCnt
= partView
.getSegCnt()
590 diff
= maxSegCnt
- segCnt
593 cnts
= getSubdivCntPerSeg(partView
, diff
)
595 for j
in range(0, len(path
.parts
)):
598 for k
, seg
in enumerate(part
.getSegs()):
599 numSubdivs
= cnts
[cumulSegIdx
] + 1
600 newSegs
+= subdivideSeg(seg
, numSubdivs
)
603 path
.parts
[j
] = Part(path
, newSegs
, part
.isClosed
)
605 for j
in range(0, len(path
.parts
)):
609 partSegCnt
= part
.getSegCnt()
611 #TODO: Adding everything in the last part?
612 if(j
== (len(path
.parts
)-1) and
613 len(maxSegCntsByPart
) > len(path
.parts
)):
614 diff
= resSegCnt
[i
][j
] - partSegCnt
616 diff
= maxSegCntsByPart
[j
] - partSegCnt
619 cnts
= getSubdivCntPerSeg(part
, diff
)
621 for k
, seg
in enumerate(part
.getSegs()):
623 subdivCnt
= cnts
[k
] + 1 #1 for the existing one
624 newSegs
+= subdivideSeg(seg
, subdivCnt
)
626 #isClosed won't be used, but let's update anyway
627 path
.parts
[j
] = Part(path
, newSegs
, part
.isClosed
)
629 #TODO: Simplify (Not very readable)
630 def alignPath(path
, matchParts
, matchCriteria
, alignBy
, alignValues
):
632 parts
= path
.parts
[:]
634 if(matchParts
== 'custom'):
635 fnMap
= {'vCnt' : lambda part
: -1 * part
.getSegCnt(), \
636 'bbArea': lambda part
: -1 * part
.bboxSurfaceArea(worldSpace
= True), \
637 'bbHeight' : lambda part
: -1 * part
.getBBHeight(worldSpace
= True), \
638 'bbWidth' : lambda part
: -1 * part
.getBBWidth(worldSpace
= True), \
639 'bbDepth' : lambda part
: -1 * part
.getBBDepth(worldSpace
= True)
642 for criterion
in matchCriteria
:
643 fn
= fnMap
.get(criterion
)
645 minmax
= criterion
[:3] == 'max' #0 if min; 1 if max
646 axisIdx
= ord(criterion
[3:]) - ord('X')
648 fn
= eval('lambda part: part.getBBox(worldSpace = True)[' + \
649 str(minmax
) + '][' + str(axisIdx
) + ']')
651 matchPartCmpFns
.append(fn
)
653 def comparer(left
, right
):
654 for fn
in matchPartCmpFns
:
658 if(floatCmpWithMargin(a
, b
)):
661 return (a
> b
) - ( a
< b
) #No cmp in python3
665 parts
= sorted(parts
, key
= cmp_to_key(comparer
))
668 if(alignBy
== 'vertCo'):
669 def evalCmp(criteria
, pt1
, pt2
):
670 if(len(criteria
) == 0):
673 minmax
= criteria
[0][0]
674 axisIdx
= criteria
[0][1]
678 if(floatCmpWithMargin(val1
, val2
)):
679 criteria
= criteria
[:]
681 return evalCmp(criteria
, pt1
, pt2
)
683 return val1
< val2
if minmax
== 'min' else val1
> val2
685 alignCri
= [[a
[:3], ord(a
[3:]) - ord('X')] for a
in alignValues
]
686 alignCmpFn
= lambda pt1
, pt2
, curve
: (evalCmp(alignCri
, \
687 curve
.matrix_world
@ pt1
, curve
.matrix_world
@ pt2
))
692 for i
in range(0, len(parts
)):
693 #Only truly closed parts
694 if(alignCmpFn
!= None and parts
[i
].isClosed
):
695 for j
in range(0, parts
[i
].getSegCnt()):
696 seg
= parts
[i
].getSeg(j
)
697 if(j
== 0 or alignCmpFn(seg
.start
, startPt
, path
.curve
)):
701 path
.parts
[i
]= Part(path
, parts
[i
].getSegsCopy(startIdx
, None) + \
702 parts
[i
].getSegsCopy(None, startIdx
), parts
[i
].isClosed
)
704 path
.parts
[i
] = parts
[i
]
706 #TODO: Other shape key attributes like interpolation...?
707 def getExistingShapeKeyPaths(path
):
711 if(obj
.data
.shape_keys
!= None):
712 keyblocks
= obj
.data
.shape_keys
.key_blocks
[1:]#Skip basis
713 for key
in keyblocks
:
714 datacopy
= obj
.data
.copy()
716 for spline
in datacopy
.splines
:
717 for pt
in spline
.bezier_points
:
718 pt
.co
= key
.data
[i
].co
719 pt
.handle_left
= key
.data
[i
].handle_left
720 pt
.handle_right
= key
.data
[i
].handle_right
722 paths
.append(Path(obj
, datacopy
, key
.name
))
725 def addShapeKeys(curve
, paths
, space
):
727 key
= curve
.shape_key_add(name
= path
.name
)
728 pts
= [pt
for pset
in path
.getBezierPtsBySpline() for pt
in pset
]
729 for i
, pt
in enumerate(pts
):
730 if(space
== 'worldspace'):
731 pt
= [curve
.matrix_world
.inverted() @ (path
.curve
.matrix_world
@ p
) for p
in pt
]
732 key
.data
[i
].co
= pt
[0]
733 key
.data
[i
].handle_left
= pt
[1]
734 key
.data
[i
].handle_right
= pt
[2]
737 def safeRemoveCurveObj(obj
):
739 collections
= obj
.users_collection
741 for c
in collections
:
742 c
.objects
.unlink(obj
)
744 if(obj
.name
in bpy
.context
.scene
.collection
.objects
):
745 bpy
.context
.scene
.collection
.objects
.unlink(obj
)
747 if(obj
.data
.users
== 1):
748 if(obj
.type == 'CURVE'):
749 bpy
.data
.curves
.remove(obj
.data
) #This also removes object?
750 elif(obj
.type == 'MESH'):
751 bpy
.data
.meshes
.remove(obj
.data
)
753 bpy
.data
.objects
.remove(obj
)
758 def markVertHandler(self
, context
):
760 bpy
.ops
.wm
.mark_vertex()
763 #################### UI and Registration ####################
765 class AssignShapeKeysOp(bpy
.types
.Operator
):
766 bl_idname
= "object.assign_shape_keys"
767 bl_label
= "Assign Shape Keys"
768 bl_options
= {'REGISTER', 'UNDO'}
770 def execute(self
, context
):
771 params
= context
.window_manager
.AssignShapeKeyParams
772 removeOriginal
= params
.removeOriginal
775 matchParts
= params
.matchParts
776 matchCri1
= params
.matchCri1
777 matchCri2
= params
.matchCri2
778 matchCri3
= params
.matchCri3
780 alignBy
= params
.alignList
781 alignVal1
= params
.alignVal1
782 alignVal2
= params
.alignVal2
783 alignVal3
= params
.alignVal3
785 createdObjsMap
= main(removeOriginal
, space
, \
786 matchParts
, [matchCri1
, matchCri2
, matchCri3
], \
787 alignBy
, [alignVal1
, alignVal2
, alignVal3
])
792 class MarkerController
:
793 drawHandlerRef
= None
795 ptColor
= (0, .8, .8, 1)
797 def createSMMap(self
, context
):
798 objs
= context
.selected_objects
801 if(not isBezier(curve
)):
804 smMap
[curve
.name
] = {}
805 mw
= curve
.matrix_world
806 for splineIdx
, spline
in enumerate(curve
.data
.splines
):
807 if(not spline
.use_cyclic_u
):
810 #initialize to the curr start vert co and idx
811 smMap
[curve
.name
][splineIdx
] = \
812 [mw
@ curve
.data
.splines
[splineIdx
].bezier_points
[0].co
, 0]
814 for pt
in spline
.bezier_points
:
815 pt
.select_control_point
= False
817 if(len(smMap
[curve
.name
]) == 0):
818 del smMap
[curve
.name
]
822 def createBatch(self
, context
):
823 positions
= [s
[0] for cn
in self
.smMap
.values() for s
in cn
.values()]
824 colors
= [MarkerController
.ptColor
for i
in range(0, len(positions
))]
826 self
.batch
= batch_for_shader(self
.shader
, \
827 "POINTS", {"pos": positions
, "color": colors
})
830 context
.area
.tag_redraw()
832 def drawHandler(self
):
833 bgl
.glPointSize(MarkerController
.defPointSize
)
834 self
.batch
.draw(self
.shader
)
836 def removeMarkers(self
, context
):
837 if(MarkerController
.drawHandlerRef
!= None):
838 bpy
.types
.SpaceView3D
.draw_handler_remove(MarkerController
.drawHandlerRef
, \
841 if(context
.area
and hasattr(context
.space_data
, 'region_3d')):
842 context
.area
.tag_redraw()
844 MarkerController
.drawHandlerRef
= None
848 def __init__(self
, context
):
849 self
.smMap
= self
.createSMMap(context
)
850 self
.shader
= gpu
.shader
.from_builtin('3D_FLAT_COLOR')
853 MarkerController
.drawHandlerRef
= \
854 bpy
.types
.SpaceView3D
.draw_handler_add(self
.drawHandler
, \
855 (), "WINDOW", "POST_VIEW")
857 self
.createBatch(context
)
859 def saveStartVerts(self
):
860 for curveName
in self
.smMap
.keys():
861 curve
= bpy
.data
.objects
[curveName
]
862 splines
= curve
.data
.splines
863 spMap
= self
.smMap
[curveName
]
865 for splineIdx
in spMap
.keys():
866 markerInfo
= spMap
[splineIdx
]
867 if(markerInfo
[1] != 0):
868 pts
= splines
[splineIdx
].bezier_points
869 loc
, idx
= markerInfo
[0], markerInfo
[1]
872 ptCopy
= [[p
.co
.copy(), p
.handle_right
.copy(), \
873 p
.handle_left
.copy(), p
.handle_right_type
, \
874 p
.handle_left_type
] for p
in pts
]
876 for i
, pt
in enumerate(pts
):
877 srcIdx
= (idx
+ i
) % cnt
880 #Must set the types first
881 pt
.handle_right_type
= p
[3]
882 pt
.handle_left_type
= p
[4]
884 pt
.handle_right
= p
[1]
885 pt
.handle_left
= p
[2]
887 def updateSMMap(self
):
888 for curveName
in self
.smMap
.keys():
889 curve
= bpy
.data
.objects
[curveName
]
890 spMap
= self
.smMap
[curveName
]
891 mw
= curve
.matrix_world
893 for splineIdx
in spMap
.keys():
894 markerInfo
= spMap
[splineIdx
]
895 loc
, idx
= markerInfo
[0], markerInfo
[1]
896 pts
= curve
.data
.splines
[splineIdx
].bezier_points
898 selIdxs
= [x
for x
in range(0, len(pts
)) \
899 if pts
[x
].select_control_point
== True]
901 selIdx
= selIdxs
[0] if(len(selIdxs
) > 0 ) else idx
902 co
= mw
@ pts
[selIdx
].co
903 self
.smMap
[curveName
][splineIdx
] = [co
, selIdx
]
905 def deselectAll(self
):
906 for curveName
in self
.smMap
.keys():
907 curve
= bpy
.data
.objects
[curveName
]
908 for spline
in curve
.data
.splines
:
909 for pt
in spline
.bezier_points
:
910 pt
.select_control_point
= False
912 def getSpaces3D(context
):
913 areas3d
= [area
for area
in context
.window
.screen
.areas \
914 if area
.type == 'VIEW_3D']
916 return [s
for a
in areas3d
for s
in a
.spaces
if s
.type == 'VIEW_3D']
918 def hideHandles(context
):
920 spaces
= MarkerController
.getSpaces3D(context
)
922 states
.append(s
.overlay
.show_curve_handles
)
923 s
.overlay
.show_curve_handles
= False
926 def resetShowHandleState(context
, handleStates
):
927 spaces
= MarkerController
.getSpaces3D(context
)
928 for i
, s
in enumerate(spaces
):
929 s
.overlay
.show_curve_handles
= handleStates
[i
]
932 class ModalMarkSegStartOp(bpy
.types
.Operator
):
933 bl_description
= "Mark Vertex"
934 bl_idname
= "wm.mark_vertex"
935 bl_label
= "Mark Start Vertex"
937 def cleanup(self
, context
):
938 wm
= context
.window_manager
939 wm
.event_timer_remove(self
._timer
)
940 self
.markerState
.removeMarkers(context
)
941 MarkerController
.resetShowHandleState(context
, self
.handleStates
)
942 context
.window_manager
.AssignShapeKeyParams
.markVertex
= False
944 def modal (self
, context
, event
):
945 params
= context
.window_manager
.AssignShapeKeyParams
947 if(context
.mode
== 'OBJECT' or event
.type == "ESC" or\
948 not context
.window_manager
.AssignShapeKeyParams
.markVertex
):
949 self
.cleanup(context
)
952 elif(event
.type == "RET"):
953 self
.markerState
.saveStartVerts()
954 self
.cleanup(context
)
957 if(event
.type == 'TIMER'):
958 self
.markerState
.updateSMMap()
959 self
.markerState
.createBatch(context
)
961 elif(event
.type in {'LEFT_CTRL', 'RIGHT_CTRL'}):
962 self
.ctrl
= (event
.value
== 'PRESS')
964 elif(event
.type in {'LEFT_SHIFT', 'RIGHT_SHIFT'}):
965 self
.shift
= (event
.value
== 'PRESS')
967 if(event
.type not in {"MIDDLEMOUSE", "TAB", "LEFTMOUSE", \
968 "RIGHTMOUSE", 'WHEELDOWNMOUSE', 'WHEELUPMOUSE'} and \
969 not event
.type.startswith("NUMPAD_")):
970 return {'RUNNING_MODAL'}
972 return {"PASS_THROUGH"}
974 def execute(self
, context
):
975 #TODO: Why such small step?
976 self
._timer
= context
.window_manager
.event_timer_add(time_step
= 0.0001, \
977 window
= context
.window
)
981 context
.window_manager
.modal_handler_add(self
)
982 self
.markerState
= MarkerController(context
)
984 #Hide so that users don't accidentally select handles instead of points
985 self
.handleStates
= MarkerController
.hideHandles(context
)
987 return {"RUNNING_MODAL"}
990 class AssignShapeKeyParams(bpy
.types
.PropertyGroup
):
992 removeOriginal
: BoolProperty(name
= "Remove Shape Key Objects", \
993 description
= "Remove shape key objects after assigning to target", \
996 space
: EnumProperty(name
= "Space", \
997 items
= [('worldspace', 'World Space', 'worldspace'),
998 ('localspace', 'Local Space', 'localspace')], \
999 description
= 'Space that shape keys are evluated in')
1001 alignList
: EnumProperty(name
="Vertex Alignment", items
= \
1002 [("-None-", 'Manual Alignment', "Align curve segments based on starting vertex"), \
1003 ('vertCo', 'Vertex Coordinates', 'Align curve segments based on vertex coordinates')], \
1004 description
= 'Start aligning the vertices of target and shape keys from',
1007 alignVal1
: EnumProperty(name
="Value 1",
1008 items
= matchList
, default
= 'minX', description
='First align criterion')
1010 alignVal2
: EnumProperty(name
="Value 2",
1011 items
= matchList
, default
= 'maxY', description
='Second align criterion')
1013 alignVal3
: EnumProperty(name
="Value 3",
1014 items
= matchList
, default
= 'minZ', description
='Third align criterion')
1016 matchParts
: EnumProperty(name
="Match Parts", items
= \
1017 [("-None-", 'None', "Don't match parts"), \
1018 ('default', 'Default', 'Use part (spline) order as in curve'), \
1019 ('custom', 'Custom', 'Use one of the custom criteria for part matching')], \
1020 description
='Match disconnected parts', default
= 'default')
1022 matchCri1
: EnumProperty(name
="Value 1",
1023 items
= matchList
, default
= 'minX', description
='First match criterion')
1025 matchCri2
: EnumProperty(name
="Value 2",
1026 items
= matchList
, default
= 'maxY', description
='Second match criterion')
1028 matchCri3
: EnumProperty(name
="Value 3",
1029 items
= matchList
, default
= 'minZ', description
='Third match criterion')
1031 markVertex
: BoolProperty(name
="Mark Starting Vertices", \
1032 description
='Mark first vertices in all splines of selected curves', \
1033 default
= False, update
= markVertHandler
)
1036 class AssignShapeKeysPanel(bpy
.types
.Panel
):
1038 bl_label
= "Assign Shape Keys"
1039 bl_idname
= "CURVE_PT_assign_shape_keys"
1040 bl_space_type
= 'VIEW_3D'
1041 bl_region_type
= 'UI'
1042 bl_category
= "Tool"
1045 def poll(cls
, context
):
1046 return context
.mode
in {'OBJECT', 'EDIT_CURVE'}
1048 def draw(self
, context
):
1050 layout
= self
.layout
1051 col
= layout
.column()
1052 params
= context
.window_manager
.AssignShapeKeyParams
1054 if(context
.mode
== 'OBJECT'):
1056 row
.prop(params
, "removeOriginal")
1059 row
.prop(params
, "space")
1062 row
.prop(params
, "alignList")
1064 if(params
.alignList
== 'vertCo'):
1066 row
.prop(params
, "alignVal1")
1067 row
.prop(params
, "alignVal2")
1068 row
.prop(params
, "alignVal3")
1071 row
.prop(params
, "matchParts")
1073 if(params
.matchParts
== 'custom'):
1075 row
.prop(params
, "matchCri1")
1076 row
.prop(params
, "matchCri2")
1077 row
.prop(params
, "matchCri3")
1080 row
.operator("object.assign_shape_keys")
1082 col
.prop(params
, "markVertex", \
1086 # registering and menu integration
1088 bpy
.utils
.register_class(AssignShapeKeysPanel
)
1089 bpy
.utils
.register_class(AssignShapeKeysOp
)
1090 bpy
.utils
.register_class(AssignShapeKeyParams
)
1091 bpy
.types
.WindowManager
.AssignShapeKeyParams
= \
1092 bpy
.props
.PointerProperty(type=AssignShapeKeyParams
)
1093 bpy
.utils
.register_class(ModalMarkSegStartOp
)
1096 bpy
.utils
.unregister_class(AssignShapeKeysOp
)
1097 bpy
.utils
.unregister_class(AssignShapeKeysPanel
)
1098 del bpy
.types
.WindowManager
.AssignShapeKeyParams
1099 bpy
.utils
.unregister_class(AssignShapeKeyParams
)
1100 bpy
.utils
.unregister_class(ModalMarkSegStartOp
)
1102 if __name__
== "__main__":