1 # SPDX-FileCopyrightText: 2019 Shrinivas Kulkarni
3 # SPDX-License-Identifier: GPL-3.0-or-later
5 # This Blender add-on assigns one or more Bezier Curves as shape keys to another
8 # Supported Blender Versions: 2.8x
10 # https://github.com/Shriinivas/assignshapekey/blob/master/LICENSE
12 import bpy
, bmesh
, gpu
13 from gpu_extras
.batch
import batch_for_shader
14 from bpy
.props
import BoolProperty
, EnumProperty
, StringProperty
15 from collections
import OrderedDict
16 from mathutils
import Vector
17 from math
import sqrt
, floor
18 from functools
import cmp_to_key
19 from bpy
.types
import Panel
, Operator
, AddonPreferences
23 "name": "Assign Shape Keys",
24 "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 "doc_url": "{BLENDER_MANUAL_URL}/addons/add_curve/assign_shape_keys.html",
30 "category": "Add Curve",
33 alignList
= [('minX', 'Min X', 'Align vertices with Min X'),
34 ('maxX', 'Max X', 'Align vertices with Max X'),
35 ('minY', 'Min Y', 'Align vertices with Min Y'),
36 ('maxY', 'Max Y', 'Align vertices with Max Y'),
37 ('minZ', 'Min Z', 'Align vertices with Min Z'),
38 ('maxZ', 'Max Z', 'Align vertices with Max Z')]
40 matchList
= [('vCnt', 'Vertex Count', 'Match by vertex count'),
41 ('bbArea', 'Area', 'Match by surface area of the bounding box'), \
42 ('bbHeight', 'Height', 'Match by bounding box height'), \
43 ('bbWidth', 'Width', 'Match by bounding box width'),
44 ('bbDepth', 'Depth', 'Match by bounding box depth'),
45 ('minX', 'Min X', 'Match by bounding box Min X'),
46 ('maxX', 'Max X', 'Match by bounding box Max X'),
47 ('minY', 'Min Y', 'Match by bounding box Min Y'),
48 ('maxY', 'Max Y', 'Match by bounding box Max Y'),
49 ('minZ', 'Min Z', 'Match by bounding box Min Z'),
50 ('maxZ', 'Max Z', 'Match by bounding box Max Z')]
52 DEF_ERR_MARGIN
= 0.0001
55 return obj
.type == 'CURVE' and len(obj
.data
.splines
) > 0 \
56 and obj
.data
.splines
[0].type == 'BEZIER'
58 #Avoid errors due to floating point conversions/comparisons
59 #TODO: return -1, 0, 1
60 def floatCmpWithMargin(float1
, float2
, margin
= DEF_ERR_MARGIN
):
61 return abs(float1
- float2
) < margin
63 def vectCmpWithMargin(v1
, v2
, margin
= DEF_ERR_MARGIN
):
64 return all(floatCmpWithMargin(v1
[i
], v2
[i
], margin
) for i
in range(0, len(v1
)))
68 #pts[0] - start, pts[1] - ctrl1, pts[2] - ctrl2, , pts[3] - end
70 return pts
[0] + t
* (3 * (pts
[1] - pts
[0]) +
71 t
* (3 * (pts
[0] + pts
[2]) - 6 * pts
[1] +
72 t
* (-pts
[0] + 3 * (pts
[1] - pts
[2]) + pts
[3])))
74 def getSegLenRecurs(pts
, start
, end
, t1
= 0, t2
= 1, error
= DEF_ERR_MARGIN
):
76 mid
= Segment
.pointAtT(pts
, t1_5
)
77 l
= (end
- start
).length
78 l2
= (mid
- start
).length
+ (end
- mid
).length
80 return (Segment
.getSegLenRecurs(pts
, start
, mid
, t1
, t1_5
, error
) +
81 Segment
.getSegLenRecurs(pts
, mid
, end
, t1_5
, t2
, error
))
84 def __init__(self
, start
, ctrl1
, ctrl2
, end
):
89 pts
= [start
, ctrl1
, ctrl2
, end
]
90 self
.length
= Segment
.getSegLenRecurs(pts
, start
, end
)
92 #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
93 def partialSeg(self
, t0
, t1
):
94 pts
= [self
.start
, self
.ctrl1
, self
.ctrl2
, self
.end
]
101 #Let's make at least the line segments of predictable length :)
102 if(pts
[0] == pts
[1] and pts
[2] == pts
[3]):
103 pt0
= Vector([(1 - t0
) * pts
[0][i
] + t0
* pts
[2][i
] for i
in range(0, 3)])
104 pt1
= Vector([(1 - t1
) * pts
[0][i
] + t1
* pts
[2][i
] for i
in range(0, 3)])
105 return Segment(pt0
, pt0
, pt1
, pt1
)
110 qa
= [pts
[0][i
]*u0
*u0
+ pts
[1][i
]*2*t0
*u0
+ pts
[2][i
]*t0
*t0
for i
in range(0, 3)]
111 qb
= [pts
[0][i
]*u1
*u1
+ pts
[1][i
]*2*t1
*u1
+ pts
[2][i
]*t1
*t1
for i
in range(0, 3)]
112 qc
= [pts
[1][i
]*u0
*u0
+ pts
[2][i
]*2*t0
*u0
+ pts
[3][i
]*t0
*t0
for i
in range(0, 3)]
113 qd
= [pts
[1][i
]*u1
*u1
+ pts
[2][i
]*2*t1
*u1
+ pts
[3][i
]*t1
*t1
for i
in range(0, 3)]
115 pta
= Vector([qa
[i
]*u0
+ qc
[i
]*t0
for i
in range(0, 3)])
116 ptb
= Vector([qa
[i
]*u1
+ qc
[i
]*t1
for i
in range(0, 3)])
117 ptc
= Vector([qb
[i
]*u0
+ qd
[i
]*t0
for i
in range(0, 3)])
118 ptd
= Vector([qb
[i
]*u1
+ qd
[i
]*t1
for i
in range(0, 3)])
120 return Segment(pta
, ptb
, ptc
, ptd
)
122 #see https://stackoverflow.com/questions/24809978/calculating-the-bounding-box-of-cubic-bezier-curve
123 #(3 D - 9 C + 9 B - 3 A) t^2 + (6 A - 12 B + 6 C) t + 3 (B - A)
124 #pts[0] - start, pts[1] - ctrl1, pts[2] - ctrl2, , pts[3] - end
125 #TODO: Return Vectors to make world space calculations consistent
126 def bbox(self
, mw
= None):
127 def evalBez(AA
, BB
, CC
, DD
, t
):
128 return AA
* (1 - t
) * (1 - t
) * (1 - t
) + \
129 3 * BB
* t
* (1 - t
) * (1 - t
) + \
130 3 * CC
* t
* t
* (1 - t
) + \
144 MINXYZ
= [min([A
[i
], D
[i
]]) for i
in range(0, 3)]
145 MAXXYZ
= [max([A
[i
], D
[i
]]) for i
in range(0, 3)]
146 leftBotBack_rgtTopFront
= [MINXYZ
, MAXXYZ
]
148 a
= [3 * D
[i
] - 9 * C
[i
] + 9 * B
[i
] - 3 * A
[i
] for i
in range(0, 3)]
149 b
= [6 * A
[i
] - 12 * B
[i
] + 6 * C
[i
] for i
in range(0, 3)]
150 c
= [3 * (B
[i
] - A
[i
]) for i
in range(0, 3)]
153 for i
in range(0, 3):
157 solns
.append(0)#Independent of t so lets take the starting pt
159 solns
.append(c
[i
] / b
[i
])
161 rootFact
= b
[i
] * b
[i
] - 4 * a
[i
] * c
[i
]
163 #Two solutions with + and - sqrt
164 solns
.append((-b
[i
] + sqrt(rootFact
)) / (2 * a
[i
]))
165 solns
.append((-b
[i
] - sqrt(rootFact
)) / (2 * a
[i
]))
166 solnsxyz
.append(solns
)
168 for i
, soln
in enumerate(solnsxyz
):
169 for j
, t
in enumerate(soln
):
171 co
= evalBez(A
[i
], B
[i
], C
[i
], D
[i
], t
)
172 if(co
< leftBotBack_rgtTopFront
[0][i
]):
173 leftBotBack_rgtTopFront
[0][i
] = co
174 if(co
> leftBotBack_rgtTopFront
[1][i
]):
175 leftBotBack_rgtTopFront
[1][i
] = co
177 return leftBotBack_rgtTopFront
181 def __init__(self
, parent
, segs
, isClosed
):
186 self
.isClosed
= isClosed
188 #Indicates if this should be closed based on its counterparts in other paths
189 self
.toClose
= isClosed
191 self
.length
= sum(seg
.length
for seg
in self
.segs
)
193 self
.bboxWorldSpace
= None
195 def getSeg(self
, idx
):
196 return self
.segs
[idx
]
201 def getSegsCopy(self
, start
, end
):
206 return self
.segs
[start
:end
]
208 def getBBox(self
, worldSpace
):
209 #Avoid frequent calculations, as this will be called in compare method
210 if(not worldSpace
and self
.bbox
!= None):
213 if(worldSpace
and self
.bboxWorldSpace
!= None):
214 return self
.bboxWorldSpace
216 leftBotBack_rgtTopFront
= [[None]*3,[None]*3]
218 for seg
in self
.segs
:
221 bb
= seg
.bbox(self
.parent
.curve
.matrix_world
)
225 for i
in range(0, 3):
226 if (leftBotBack_rgtTopFront
[0][i
] == None or \
227 bb
[0][i
] < leftBotBack_rgtTopFront
[0][i
]):
228 leftBotBack_rgtTopFront
[0][i
] = bb
[0][i
]
230 for i
in range(0, 3):
231 if (leftBotBack_rgtTopFront
[1][i
] == None or \
232 bb
[1][i
] > leftBotBack_rgtTopFront
[1][i
]):
233 leftBotBack_rgtTopFront
[1][i
] = bb
[1][i
]
236 self
.bboxWorldSpace
= leftBotBack_rgtTopFront
238 self
.bbox
= leftBotBack_rgtTopFront
240 return leftBotBack_rgtTopFront
243 def getBBDiff(self
, axisIdx
, worldSpace
):
244 obj
= self
.parent
.curve
245 bbox
= self
.getBBox(worldSpace
)
246 diff
= abs(bbox
[1][axisIdx
] - bbox
[0][axisIdx
])
249 def getBBWidth(self
, worldSpace
):
250 return self
.getBBDiff(0, worldSpace
)
252 def getBBHeight(self
, worldSpace
):
253 return self
.getBBDiff(1, worldSpace
)
255 def getBBDepth(self
, worldSpace
):
256 return self
.getBBDiff(2, worldSpace
)
258 def bboxSurfaceArea(self
, worldSpace
):
259 leftBotBack_rgtTopFront
= self
.getBBox(worldSpace
)
260 w
= abs( leftBotBack_rgtTopFront
[1][0] - leftBotBack_rgtTopFront
[0][0] )
261 l
= abs( leftBotBack_rgtTopFront
[1][1] - leftBotBack_rgtTopFront
[0][1] )
262 d
= abs( leftBotBack_rgtTopFront
[1][2] - leftBotBack_rgtTopFront
[0][2] )
264 return 2 * (w
* l
+ w
* d
+ l
* d
)
267 return len(self
.segs
)
269 def getBezierPtsInfo(self
):
273 for j
, seg
in enumerate(self
.getSegs()):
276 handleRight
= seg
.ctrl1
280 handleLeft
= self
.getSeg(-1).ctrl2
284 handleLeft
= prevSeg
.ctrl2
286 bezierPtsInfo
.append([pt
, handleLeft
, handleRight
])
289 if(self
.toClose
== True):
290 bezierPtsInfo
[-1][2] = seg
.ctrl1
292 bezierPtsInfo
.append([prevSeg
.end
, prevSeg
.ctrl2
, prevSeg
.end
])
297 return str(self
.length
)
301 def __init__(self
, curve
, objData
= None, name
= None):
312 self
.parts
= [Part(self
, getSplineSegs(s
), s
.use_cyclic_u
) for s
in objData
.splines
]
314 def getPartCnt(self
):
315 return len(self
.parts
)
317 def getPartView(self
):
318 p
= Part(self
, [seg
for part
in self
.parts
for seg
in part
.getSegs()], None)
321 def getPartBoundaryIdxs(self
):
326 cumulCnt
+= p
.getSegCnt()
327 cumulCntList
.add(cumulCnt
)
331 def updatePartsList(self
, segCntsPerPart
, byPart
):
332 monolithicSegList
= [seg
for part
in self
.parts
for seg
in part
.getSegs()]
333 oldParts
= self
.parts
[:]
334 currPart
= oldParts
[0]
338 for i
in range(0, len(segCntsPerPart
)):
342 currIdx
= segCntsPerPart
[i
-1]
344 nextIdx
= segCntsPerPart
[i
]
347 if(vectCmpWithMargin(monolithicSegList
[currIdx
].start
, \
348 currPart
.getSegs()[0].start
) and \
349 vectCmpWithMargin(monolithicSegList
[nextIdx
-1].end
, \
350 currPart
.getSegs()[-1].end
)):
351 isClosed
= currPart
.isClosed
353 self
.parts
.append(Part(self
, \
354 monolithicSegList
[currIdx
:nextIdx
], isClosed
))
356 if(monolithicSegList
[nextIdx
-1] == currPart
.getSegs()[-1]):
358 if(partIdx
< len(oldParts
)):
359 currPart
= oldParts
[partIdx
]
361 def getBezierPtsBySpline(self
):
364 for i
, part
in enumerate(self
.parts
):
365 data
.append(part
.getBezierPtsInfo())
369 def getNewCurveData(self
):
371 newCurveData
= self
.curve
.data
.copy()
372 newCurveData
.splines
.clear()
374 splinesData
= self
.getBezierPtsBySpline()
376 for i
, newPoints
in enumerate(splinesData
):
378 spline
= newCurveData
.splines
.new('BEZIER')
379 spline
.bezier_points
.add(len(newPoints
)-1)
380 spline
.use_cyclic_u
= self
.parts
[i
].toClose
382 for j
in range(0, len(spline
.bezier_points
)):
383 newPoint
= newPoints
[j
]
384 spline
.bezier_points
[j
].co
= newPoint
[0]
385 spline
.bezier_points
[j
].handle_left
= newPoint
[1]
386 spline
.bezier_points
[j
].handle_right
= newPoint
[2]
387 spline
.bezier_points
[j
].handle_right_type
= 'FREE'
391 def updateCurve(self
):
392 curveData
= self
.curve
.data
393 #Remove existing shape keys first
394 if(curveData
.shape_keys
!= None):
395 keyblocks
= reversed(curveData
.shape_keys
.key_blocks
)
397 self
.curve
.shape_key_remove(sk
)
398 self
.curve
.data
= self
.getNewCurveData()
399 bpy
.data
.curves
.remove(curveData
)
401 def main(targetObj
, shapekeyObjs
, removeOriginal
, space
, matchParts
, \
402 matchCriteria
, alignBy
, alignValues
):
404 target
= Path(targetObj
)
406 shapekeys
= [Path(c
) for c
in shapekeyObjs
]
408 existingKeys
= getExistingShapeKeyPaths(target
)
409 shapekeys
= existingKeys
+ shapekeys
410 userSel
= [target
] + shapekeys
413 alignPath(path
, matchParts
, matchCriteria
, alignBy
, alignValues
)
415 addMissingSegs(userSel
, byPart
= (matchParts
!= "-None-"))
419 bIdxs
= bIdxs
.union(path
.getPartBoundaryIdxs())
422 path
.updatePartsList(sorted(list(bIdxs
)), byPart
= False)
424 #All will have the same part count by now
425 allToClose
= [all(path
.parts
[j
].isClosed
for path
in userSel
)
426 for j
in range(0, len(userSel
[0].parts
))]
428 #All paths will have the same no of splines with the same no of bezier points
430 for j
, part
in enumerate(path
.parts
):
431 part
.toClose
= allToClose
[j
]
435 if(len(existingKeys
) == 0):
436 target
.curve
.shape_key_add(name
= 'Basis')
438 addShapeKeys(target
.curve
, shapekeys
, space
)
442 if(path
.curve
!= target
.curve
):
443 safeRemoveObj(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
[:]
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 safeRemoveObj(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
.alignCos
782 alignVal1
= params
.alignVal1
783 alignVal2
= params
.alignVal2
784 alignVal3
= params
.alignVal3
786 targetObj
= bpy
.context
.active_object
787 shapekeyObjs
= [obj
for obj
in bpy
.context
.selected_objects
if isBezier(obj
) \
788 and obj
!= targetObj
]
790 if(targetObj
!= None and isBezier(targetObj
) and len(shapekeyObjs
) > 0):
791 main(targetObj
, shapekeyObjs
, removeOriginal
, space
, \
792 matchParts
, [matchCri1
, matchCri2
, matchCri3
], \
793 alignBy
, [alignVal1
, alignVal2
, alignVal3
])
798 class MarkerController
:
799 drawHandlerRef
= None
801 ptColor
= (0, .8, .8, 1)
803 def createSMMap(self
, context
):
804 objs
= context
.selected_objects
807 if(not isBezier(curve
)):
810 smMap
[curve
.name
] = {}
811 mw
= curve
.matrix_world
812 for splineIdx
, spline
in enumerate(curve
.data
.splines
):
813 if(not spline
.use_cyclic_u
):
816 #initialize to the curr start vert co and idx
817 smMap
[curve
.name
][splineIdx
] = \
818 [mw
@ curve
.data
.splines
[splineIdx
].bezier_points
[0].co
, 0]
820 for pt
in spline
.bezier_points
:
821 pt
.select_control_point
= False
823 if(len(smMap
[curve
.name
]) == 0):
824 del smMap
[curve
.name
]
828 def createBatch(self
, context
):
829 positions
= [s
[0] for cn
in self
.smMap
.values() for s
in cn
.values()]
830 colors
= [MarkerController
.ptColor
for i
in range(0, len(positions
))]
832 self
.batch
= batch_for_shader(self
.shader
, \
833 "POINTS", {"pos": positions
, "color": colors
})
836 context
.area
.tag_redraw()
838 def drawHandler(self
):
839 gpu
.state
.point_size_set(MarkerController
.defPointSize
)
840 self
.batch
.draw(self
.shader
)
842 def removeMarkers(self
, context
):
843 if(MarkerController
.drawHandlerRef
!= None):
844 bpy
.types
.SpaceView3D
.draw_handler_remove(MarkerController
.drawHandlerRef
, \
847 if(context
.area
and hasattr(context
.space_data
, 'region_3d')):
848 context
.area
.tag_redraw()
850 MarkerController
.drawHandlerRef
= None
854 def __init__(self
, context
):
855 self
.smMap
= self
.createSMMap(context
)
856 self
.shader
= gpu
.shader
.from_builtin('FLAT_COLOR')
859 MarkerController
.drawHandlerRef
= \
860 bpy
.types
.SpaceView3D
.draw_handler_add(self
.drawHandler
, \
861 (), "WINDOW", "POST_VIEW")
863 self
.createBatch(context
)
865 def saveStartVerts(self
):
866 for curveName
in self
.smMap
.keys():
867 curve
= bpy
.data
.objects
[curveName
]
868 splines
= curve
.data
.splines
869 spMap
= self
.smMap
[curveName
]
871 for splineIdx
in spMap
.keys():
872 markerInfo
= spMap
[splineIdx
]
873 if(markerInfo
[1] != 0):
874 pts
= splines
[splineIdx
].bezier_points
875 loc
, idx
= markerInfo
[0], markerInfo
[1]
878 ptCopy
= [[p
.co
.copy(), p
.handle_right
.copy(), \
879 p
.handle_left
.copy(), p
.handle_right_type
, \
880 p
.handle_left_type
] for p
in pts
]
882 for i
, pt
in enumerate(pts
):
883 srcIdx
= (idx
+ i
) % cnt
886 #Must set the types first
887 pt
.handle_right_type
= p
[3]
888 pt
.handle_left_type
= p
[4]
890 pt
.handle_right
= p
[1]
891 pt
.handle_left
= p
[2]
893 def updateSMMap(self
):
894 for curveName
in self
.smMap
.keys():
895 curve
= bpy
.data
.objects
[curveName
]
896 spMap
= self
.smMap
[curveName
]
897 mw
= curve
.matrix_world
899 for splineIdx
in spMap
.keys():
900 markerInfo
= spMap
[splineIdx
]
901 loc
, idx
= markerInfo
[0], markerInfo
[1]
902 pts
= curve
.data
.splines
[splineIdx
].bezier_points
904 selIdxs
= [x
for x
in range(0, len(pts
)) \
905 if pts
[x
].select_control_point
== True]
907 selIdx
= selIdxs
[0] if(len(selIdxs
) > 0 ) else idx
908 co
= mw
@ pts
[selIdx
].co
909 self
.smMap
[curveName
][splineIdx
] = [co
, selIdx
]
911 def deselectAll(self
):
912 for curveName
in self
.smMap
.keys():
913 curve
= bpy
.data
.objects
[curveName
]
914 for spline
in curve
.data
.splines
:
915 for pt
in spline
.bezier_points
:
916 pt
.select_control_point
= False
918 def getSpaces3D(context
):
919 areas3d
= [area
for area
in context
.window
.screen
.areas \
920 if area
.type == 'VIEW_3D']
922 return [s
for a
in areas3d
for s
in a
.spaces
if s
.type == 'VIEW_3D']
924 def hideHandles(context
):
926 spaces
= MarkerController
.getSpaces3D(context
)
928 if(hasattr(s
.overlay
, 'show_curve_handles')):
929 states
.append(s
.overlay
.show_curve_handles
)
930 s
.overlay
.show_curve_handles
= False
931 elif(hasattr(s
.overlay
, 'display_handle')): # 2.90
932 states
.append(s
.overlay
.display_handle
)
933 s
.overlay
.display_handle
= 'NONE'
936 def resetShowHandleState(context
, handleStates
):
937 spaces
= MarkerController
.getSpaces3D(context
)
938 for i
, s
in enumerate(spaces
):
939 if(hasattr(s
.overlay
, 'show_curve_handles')):
940 s
.overlay
.show_curve_handles
= handleStates
[i
]
941 elif(hasattr(s
.overlay
, 'display_handle')): # 2.90
942 s
.overlay
.display_handle
= handleStates
[i
]
945 class ModalMarkSegStartOp(Operator
):
946 bl_description
= "Mark Vertex"
947 bl_idname
= "wm.mark_vertex"
948 bl_label
= "Mark Start Vertex"
950 def cleanup(self
, context
):
951 wm
= context
.window_manager
952 wm
.event_timer_remove(self
._timer
)
953 self
.markerState
.removeMarkers(context
)
954 MarkerController
.resetShowHandleState(context
, self
.handleStates
)
955 context
.window_manager
.AssignShapeKeyParams
.markVertex
= False
957 def modal (self
, context
, event
):
958 params
= context
.window_manager
.AssignShapeKeyParams
960 if(context
.mode
== 'OBJECT' or event
.type == "ESC" or\
961 not context
.window_manager
.AssignShapeKeyParams
.markVertex
):
962 self
.cleanup(context
)
965 elif(event
.type == "RET"):
966 self
.markerState
.saveStartVerts()
967 self
.cleanup(context
)
970 if(event
.type == 'TIMER'):
971 self
.markerState
.updateSMMap()
972 self
.markerState
.createBatch(context
)
974 return {"PASS_THROUGH"}
976 def execute(self
, context
):
977 #TODO: Why such small step?
978 self
._timer
= context
.window_manager
.event_timer_add(time_step
= 0.0001, \
979 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 alignCos
: 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
= alignList
, default
= 'minX', description
='First align criterion')
1010 alignVal2
: EnumProperty(name
="Value 2",
1011 items
= alignList
, default
= 'maxY', description
='Second align criterion')
1013 alignVal3
: EnumProperty(name
="Value 3",
1014 items
= alignList
, 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(Panel
):
1038 bl_label
= "Curve Shape Keys"
1039 bl_idname
= "CURVE_PT_assign_shape_keys"
1040 bl_space_type
= 'VIEW_3D'
1041 bl_region_type
= 'UI'
1042 bl_category
= "Edit"
1043 bl_options
= {'DEFAULT_CLOSED'}
1046 def poll(cls
, context
):
1047 return context
.mode
in {'OBJECT', 'EDIT_CURVE'}
1049 def draw(self
, context
):
1051 layout
= self
.layout
1052 layout
.label(text
='Morph Curves:')
1053 col
= layout
.column()
1054 params
= context
.window_manager
.AssignShapeKeyParams
1056 if(context
.mode
== 'OBJECT'):
1058 row
.prop(params
, "removeOriginal")
1061 row
.prop(params
, "space")
1064 row
.prop(params
, "alignCos")
1066 if(params
.alignCos
== 'vertCo'):
1068 row
.prop(params
, "alignVal1")
1069 row
.prop(params
, "alignVal2")
1070 row
.prop(params
, "alignVal3")
1073 row
.prop(params
, "matchParts")
1075 if(params
.matchParts
== 'custom'):
1077 row
.prop(params
, "matchCri1")
1078 row
.prop(params
, "matchCri2")
1079 row
.prop(params
, "matchCri3")
1082 row
.operator("object.assign_shape_keys")
1084 col
.prop(params
, "markVertex", \
1088 def updatePanel(self
, context
):
1090 panel
= AssignShapeKeysPanel
1091 if "bl_rna" in panel
.__dict
__:
1092 bpy
.utils
.unregister_class(panel
)
1094 panel
.bl_category
= context
.preferences
.addons
[__name__
].preferences
.category
1095 bpy
.utils
.register_class(panel
)
1097 except Exception as e
:
1098 print("Assign Shape Keys: Updating Panel locations has failed", e
)
1100 class AssignShapeKeysPreferences(AddonPreferences
):
1101 bl_idname
= __name__
1103 category
: StringProperty(
1104 name
= "Tab Category",
1105 description
= "Choose a name for the category of the panel",
1107 update
= updatePanel
1110 def draw(self
, context
):
1111 layout
= self
.layout
1114 col
.label(text
="Tab Category:")
1115 col
.prop(self
, "category", text
="")
1117 # registering and menu integration
1119 bpy
.utils
.register_class(AssignShapeKeysPanel
)
1120 bpy
.utils
.register_class(AssignShapeKeysOp
)
1121 bpy
.utils
.register_class(AssignShapeKeyParams
)
1122 bpy
.types
.WindowManager
.AssignShapeKeyParams
= \
1123 bpy
.props
.PointerProperty(type=AssignShapeKeyParams
)
1124 bpy
.utils
.register_class(ModalMarkSegStartOp
)
1125 bpy
.utils
.register_class(AssignShapeKeysPreferences
)
1126 updatePanel(None, bpy
.context
)
1129 bpy
.utils
.unregister_class(AssignShapeKeysOp
)
1130 bpy
.utils
.unregister_class(AssignShapeKeysPanel
)
1131 del bpy
.types
.WindowManager
.AssignShapeKeyParams
1132 bpy
.utils
.unregister_class(AssignShapeKeyParams
)
1133 bpy
.utils
.unregister_class(ModalMarkSegStartOp
)
1134 bpy
.utils
.unregister_class(AssignShapeKeysPreferences
)
1136 if __name__
== "__main__":