diff options
author | Campbell Barton <ideasman42@gmail.com> | 2019-09-05 08:18:40 +0300 |
---|---|---|
committer | Campbell Barton <ideasman42@gmail.com> | 2019-09-05 08:18:40 +0300 |
commit | 1881beb8b12028a37ef34e7e746555c3707a9150 (patch) | |
tree | b037596e4dc9b01ede704ebd89eaa223ccacf2f8 | |
parent | 2a98d83b4bcb37e3b8de5ccc72c283a5135708ed (diff) |
Cleanup: use unix line endings
-rw-r--r-- | curve_assign_shapekey.py | 2206 |
1 files changed, 1103 insertions, 1103 deletions
diff --git a/curve_assign_shapekey.py b/curve_assign_shapekey.py index 3fe88475..864b9643 100644 --- a/curve_assign_shapekey.py +++ b/curve_assign_shapekey.py @@ -1,1103 +1,1103 @@ -#
-#
-# This Blender add-on assigns one or more Bezier Curves as shape keys to another
-# Bezier Curve
-#
-# Supported Blender Version: 2.80 Beta
-#
-# Copyright (C) 2019 Shrinivas Kulkarni
-#
-# License: MIT (https://github.com/Shriinivas/assignshapekey/blob/master/LICENSE)
-#
-
-import bpy, bmesh, bgl, gpu
-from gpu_extras.batch import batch_for_shader
-from bpy.props import BoolProperty, EnumProperty
-from collections import OrderedDict
-from mathutils import Vector
-from math import sqrt, floor
-from functools import cmp_to_key
-
-
-bl_info = {
- "name": "Assign Shape Keys",
- "author": "Shrinivas Kulkarni",
- "version": (1, 0, 0),
- "location": "Properties > Active Tool and Workspace Settings > Assign Shape Keys",
- "description": "Assigns one or more Bezier curves as shape keys to another Bezier curve",
- "category": "Object",
- "wiki_url": "https://github.com/Shriinivas/assignshapekey/blob/master/README.md",
- "blender": (2, 80, 0),
-}
-
-matchList = [('vCnt', 'Vertex Count', 'Match by vertex count'),
- ('bbArea', 'Area', 'Match by surface area of the bounding box'), \
- ('bbHeight', 'Height', 'Match by bounding box height'), \
- ('bbWidth', 'Width', 'Match by bounding box width'),
- ('bbDepth', 'Depth', 'Match by bounding box depth'),
- ('minX', 'Min X', 'Match by bounding bon Min X'),
- ('maxX', 'Max X', 'Match by bounding bon Max X'),
- ('minY', 'Min Y', 'Match by bounding bon Min Y'),
- ('maxY', 'Max Y', 'Match by bounding bon Max Y'),
- ('minZ', 'Min Z', 'Match by bounding bon Min Z'),
- ('maxZ', 'Max Z', 'Match by bounding bon Max Z')]
-
-DEF_ERR_MARGIN = 0.0001
-
-def isBezier(obj):
- return obj.type == 'CURVE' and len(obj.data.splines) > 0 \
- and obj.data.splines[0].type == 'BEZIER'
-
-#Avoid errors due to floating point conversions/comparisons
-#TODO: return -1, 0, 1
-def floatCmpWithMargin(float1, float2, margin = DEF_ERR_MARGIN):
- return abs(float1 - float2) < margin
-
-def vectCmpWithMargin(v1, v2, margin = DEF_ERR_MARGIN):
- return all(floatCmpWithMargin(v1[i], v2[i], margin) for i in range(0, len(v1)))
-
-class Segment():
-
- #pts[0] - start, pts[1] - ctrl1, pts[2] - ctrl2, , pts[3] - end
- def pointAtT(pts, t):
- return pts[0] + t * (3 * (pts[1] - pts[0]) +
- t* (3 * (pts[0] + pts[2]) - 6 * pts[1] +
- t * (-pts[0] + 3 * (pts[1] - pts[2]) + pts[3])))
-
- def getSegLenRecurs(pts, start, end, t1 = 0, t2 = 1, error = DEF_ERR_MARGIN):
- t1_5 = (t1 + t2)/2
- mid = Segment.pointAtT(pts, t1_5)
- l = (end - start).length
- l2 = (mid - start).length + (end - mid).length
- if (l2 - l > error):
- return (Segment.getSegLenRecurs(pts, start, mid, t1, t1_5, error) +
- Segment.getSegLenRecurs(pts, mid, end, t1_5, t2, error))
- return l2
-
- def __init__(self, start, ctrl1, ctrl2, end):
- self.start = start
- self.ctrl1 = ctrl1
- self.ctrl2 = ctrl2
- self.end = end
- pts = [start, ctrl1, ctrl2, end]
- self.length = Segment.getSegLenRecurs(pts, start, end)
-
- #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
- def partialSeg(self, t0, t1):
- pts = [self.start, self.ctrl1, self.ctrl2, self.end]
-
- if(t0 > t1):
- tt = t1
- t1 = t0
- t0 = tt
-
- #Let's make at least the line segments of predictable length :)
- if(pts[0] == pts[1] and pts[2] == pts[3]):
- pt0 = Vector([(1 - t0) * pts[0][i] + t0 * pts[2][i] for i in range(0, 3)])
- pt1 = Vector([(1 - t1) * pts[0][i] + t1 * pts[2][i] for i in range(0, 3)])
- return Segment(pt0, pt0, pt1, pt1)
-
- u0 = 1.0 - t0
- u1 = 1.0 - t1
-
- qa = [pts[0][i]*u0*u0 + pts[1][i]*2*t0*u0 + pts[2][i]*t0*t0 for i in range(0, 3)]
- qb = [pts[0][i]*u1*u1 + pts[1][i]*2*t1*u1 + pts[2][i]*t1*t1 for i in range(0, 3)]
- qc = [pts[1][i]*u0*u0 + pts[2][i]*2*t0*u0 + pts[3][i]*t0*t0 for i in range(0, 3)]
- qd = [pts[1][i]*u1*u1 + pts[2][i]*2*t1*u1 + pts[3][i]*t1*t1 for i in range(0, 3)]
-
- pta = Vector([qa[i]*u0 + qc[i]*t0 for i in range(0, 3)])
- ptb = Vector([qa[i]*u1 + qc[i]*t1 for i in range(0, 3)])
- ptc = Vector([qb[i]*u0 + qd[i]*t0 for i in range(0, 3)])
- ptd = Vector([qb[i]*u1 + qd[i]*t1 for i in range(0, 3)])
-
- return Segment(pta, ptb, ptc, ptd)
-
- #see https://stackoverflow.com/questions/24809978/calculating-the-bounding-box-of-cubic-bezier-curve
- #(3 D - 9 C + 9 B - 3 A) t^2 + (6 A - 12 B + 6 C) t + 3 (B - A)
- #pts[0] - start, pts[1] - ctrl1, pts[2] - ctrl2, , pts[3] - end
- #TODO: Return Vectors to make world space calculations consistent
- def bbox(self, mw = None):
- def evalBez(AA, BB, CC, DD, t):
- return AA * (1 - t) * (1 - t) * (1 - t) + \
- 3 * BB * t * (1 - t) * (1 - t) + \
- 3 * CC * t * t * (1 - t) + \
- DD * t * t * t
-
- A = self.start
- B = self.ctrl1
- C = self.ctrl2
- D = self.end
-
- if(mw != None):
- A = mw @ A
- B = mw @ B
- C = mw @ C
- D = mw @ D
-
- MINXYZ = [min([A[i], D[i]]) for i in range(0, 3)]
- MAXXYZ = [max([A[i], D[i]]) for i in range(0, 3)]
- leftBotBack_rgtTopFront = [MINXYZ, MAXXYZ]
-
- a = [3 * D[i] - 9 * C[i] + 9 * B[i] - 3 * A[i] for i in range(0, 3)]
- b = [6 * A[i] - 12 * B[i] + 6 * C[i] for i in range(0, 3)]
- c = [3 * (B[i] - A[i]) for i in range(0, 3)]
-
- solnsxyz = []
- for i in range(0, 3):
- solns = []
- if(a[i] == 0):
- if(b[i] == 0):
- solns.append(0)#Independent of t so lets take the starting pt
- else:
- solns.append(c[i] / b[i])
- else:
- rootFact = b[i] * b[i] - 4 * a[i] * c[i]
- if(rootFact >=0 ):
- #Two solutions with + and - sqrt
- solns.append((-b[i] + sqrt(rootFact)) / (2 * a[i]))
- solns.append((-b[i] - sqrt(rootFact)) / (2 * a[i]))
- solnsxyz.append(solns)
-
- for i, soln in enumerate(solnsxyz):
- for j, t in enumerate(soln):
- if(t < 1 and t > 0):
- co = evalBez(A[i], B[i], C[i], D[i], t)
- if(co < leftBotBack_rgtTopFront[0][i]):
- leftBotBack_rgtTopFront[0][i] = co
- if(co > leftBotBack_rgtTopFront[1][i]):
- leftBotBack_rgtTopFront[1][i] = co
-
- return leftBotBack_rgtTopFront
-
-
-class Part():
- def __init__(self, parent, segs, isClosed):
- self.parent = parent
- self.segs = segs
-
- #use_cyclic_u
- self.isClosed = isClosed
-
- #Indicates if this should be closed based on its counterparts in other paths
- self.toClose = isClosed
-
- self.length = sum(seg.length for seg in self.segs)
- self.bbox = None
- self.bboxWorldSpace = None
-
- def getSeg(self, idx):
- return self.segs[idx]
-
- def getSegs(self):
- return self.segs
-
- def getSegsCopy(self, start, end):
- if(start == None):
- start = 0
- if(end == None):
- end = len(self.segs)
- return self.segs[start:end]
-
- def getBBox(self, worldSpace):
- #Avoid frequent calculations, as this will be called in compare method
- if(not worldSpace and self.bbox != None):
- return self.bbox
-
- if(worldSpace and self.bboxWorldSpace != None):
- return self.bboxWorldSpace
-
- leftBotBack_rgtTopFront = [[None]*3,[None]*3]
-
- for seg in self.segs:
-
- if(worldSpace):
- bb = seg.bbox(self.parent.curve.matrix_world)
- else:
- bb = seg.bbox()
-
- for i in range(0, 3):
- if (leftBotBack_rgtTopFront[0][i] == None or \
- bb[0][i] < leftBotBack_rgtTopFront[0][i]):
- leftBotBack_rgtTopFront[0][i] = bb[0][i]
-
- for i in range(0, 3):
- if (leftBotBack_rgtTopFront[1][i] == None or \
- bb[1][i] > leftBotBack_rgtTopFront[1][i]):
- leftBotBack_rgtTopFront[1][i] = bb[1][i]
-
- if(worldSpace):
- self.bboxWorldSpace = leftBotBack_rgtTopFront
- else:
- self.bbox = leftBotBack_rgtTopFront
-
- return leftBotBack_rgtTopFront
-
- #private
- def getBBDiff(self, axisIdx, worldSpace):
- obj = self.parent.curve
- bbox = self.getBBox(worldSpace)
- diff = abs(bbox[1][axisIdx] - bbox[0][axisIdx])
- return diff
-
- def getBBWidth(self, worldSpace):
- return self.getBBDiff(0, worldSpace)
-
- def getBBHeight(self, worldSpace):
- return self.getBBDiff(1, worldSpace)
-
- def getBBDepth(self, worldSpace):
- return self.getBBDiff(2, worldSpace)
-
- def bboxSurfaceArea(self, worldSpace):
- leftBotBack_rgtTopFront = self.getBBox(worldSpace)
- w = abs( leftBotBack_rgtTopFront[1][0] - leftBotBack_rgtTopFront[0][0] )
- l = abs( leftBotBack_rgtTopFront[1][1] - leftBotBack_rgtTopFront[0][1] )
- d = abs( leftBotBack_rgtTopFront[1][2] - leftBotBack_rgtTopFront[0][2] )
-
- return 2 * (w * l + w * d + l * d)
-
- def getSegCnt(self):
- return len(self.segs)
-
- def getBezierPtsInfo(self):
- prevSeg = None
- bezierPtsInfo = []
-
- for j, seg in enumerate(self.getSegs()):
-
- pt = seg.start
- handleRight = seg.ctrl1
-
- if(j == 0):
- if(self.toClose):
- handleLeft = self.getSeg(-1).ctrl2
- else:
- handleLeft = pt
- else:
- handleLeft = prevSeg.ctrl2
-
- bezierPtsInfo.append([pt, handleLeft, handleRight])
- prevSeg = seg
-
- if(self.toClose == True):
- bezierPtsInfo[-1][2] = seg.ctrl1
- else:
- bezierPtsInfo.append([prevSeg.end, prevSeg.ctrl2, prevSeg.end])
-
- return bezierPtsInfo
-
- def __repr__(self):
- return str(self.length)
-
-
-class Path:
- def __init__(self, curve, objData = None, name = None):
-
- if(objData == None):
- objData = curve.data
-
- if(name == None):
- name = curve.name
-
- self.name = name
- self.curve = curve
-
- self.parts = [Part(self, getSplineSegs(s), s.use_cyclic_u) for s in objData.splines]
-
- def getPartCnt(self):
- return len(self.parts)
-
- def getPartView(self):
- p = Part(self, [seg for part in self.parts for seg in part.getSegs()], None)
- return p
-
- def getPartBoundaryIdxs(self):
- cumulCntList = set()
- cumulCnt = 0
-
- for p in self.parts:
- cumulCnt += p.getSegCnt()
- cumulCntList.add(cumulCnt)
-
- return cumulCntList
-
- def updatePartsList(self, segCntsPerPart, byPart):
- monolithicSegList = [seg for part in self.parts for seg in part.getSegs()]
- oldParts = self.parts[:]
- currPart = oldParts[0]
- partIdx = 0
- self.parts.clear()
-
- for i in range(0, len(segCntsPerPart)):
- if( i == 0):
- currIdx = 0
- else:
- currIdx = segCntsPerPart[i-1]
-
- nextIdx = segCntsPerPart[i]
- isClosed = False
-
- if(vectCmpWithMargin(monolithicSegList[currIdx].start, \
- currPart.getSegs()[0].start) and \
- vectCmpWithMargin(monolithicSegList[nextIdx-1].end, \
- currPart.getSegs()[-1].end)):
- isClosed = currPart.isClosed
-
- self.parts.append(Part(self, \
- monolithicSegList[currIdx:nextIdx], isClosed))
-
- if(monolithicSegList[nextIdx-1] == currPart.getSegs()[-1]):
- partIdx += 1
- if(partIdx < len(oldParts)):
- currPart = oldParts[partIdx]
-
- def getBezierPtsBySpline(self):
- data = []
-
- for i, part in enumerate(self.parts):
- data.append(part.getBezierPtsInfo())
-
- return data
-
- def getNewCurveData(self):
-
- newCurveData = self.curve.data.copy()
- newCurveData.splines.clear()
-
- splinesData = self.getBezierPtsBySpline()
-
- for i, newPoints in enumerate(splinesData):
-
- spline = newCurveData.splines.new('BEZIER')
- spline.bezier_points.add(len(newPoints)-1)
- spline.use_cyclic_u = self.parts[i].toClose
-
- for j in range(0, len(spline.bezier_points)):
- newPoint = newPoints[j]
- spline.bezier_points[j].co = newPoint[0]
- spline.bezier_points[j].handle_left = newPoint[1]
- spline.bezier_points[j].handle_right = newPoint[2]
- spline.bezier_points[j].handle_right_type = 'FREE'
-
- return newCurveData
-
- def updateCurve(self):
- curveData = self.curve.data
- #Remove existing shape keys first
- if(curveData.shape_keys != None):
- keyblocks = reversed(curveData.shape_keys.key_blocks)
- for sk in keyblocks:
- self.curve.shape_key_remove(sk)
- self.curve.data = self.getNewCurveData()
- bpy.data.curves.remove(curveData)
-
-def main(removeOriginal, space, matchParts, matchCriteria, alignBy, alignValues):
- targetObj = bpy.context.active_object
- if(targetObj == None or not isBezier(targetObj)):
- return
-
- target = Path(targetObj)
-
- shapekeys = [Path(c) for c in bpy.context.selected_objects if isBezier(c) \
- and c != bpy.context.active_object]
-
- if(len(shapekeys) == 0):
- return
-
- shapekeys = getExistingShapeKeyPaths(target) + shapekeys
- userSel = [target] + shapekeys
-
- for path in userSel:
- alignPath(path, matchParts, matchCriteria, alignBy, alignValues)
-
- addMissingSegs(userSel, byPart = (matchParts != "-None-"))
-
- bIdxs = set()
- for path in userSel:
- bIdxs = bIdxs.union(path.getPartBoundaryIdxs())
-
- for path in userSel:
- path.updatePartsList(sorted(list(bIdxs)), byPart = False)
-
- #All will have the same part count by now
- allToClose = [all(path.parts[j].isClosed for path in userSel)
- for j in range(0, len(userSel[0].parts))]
-
- #All paths will have the same no of splines with the same no of bezier points
- for path in userSel:
- for j, part in enumerate(path.parts):
- part.toClose = allToClose[j]
-
- target.updateCurve()
-
- target.curve.shape_key_add(name = 'Basis')
-
- addShapeKeys(target.curve, shapekeys, space)
-
- if(removeOriginal):
- for path in userSel:
- if(path.curve != target.curve):
- safeRemoveCurveObj(path.curve)
-
- return {}
-
-def getSplineSegs(spline):
- p = spline.bezier_points
- segs = [Segment(p[i-1].co, p[i-1].handle_right, p[i].handle_left, p[i].co) \
- for i in range(1, len(p))]
- if(spline.use_cyclic_u):
- segs.append(Segment(p[-1].co, p[-1].handle_right, p[0].handle_left, p[0].co))
- return segs
-
-def subdivideSeg(origSeg, noSegs):
- if(noSegs < 2):
- return [origSeg]
-
- segs = []
- oldT = 0
- segLen = origSeg.length / noSegs
-
- for i in range(0, noSegs-1):
- t = float(i+1) / noSegs
- seg = origSeg.partialSeg(oldT, t)
- segs.append(seg)
- oldT = t
-
- seg = origSeg.partialSeg(oldT, 1)
- segs.append(seg)
-
- return segs
-
-
-def getSubdivCntPerSeg(part, toAddCnt):
-
- class SegWrapper:
- def __init__(self, idx, seg):
- self.idx = idx
- self.seg = seg
- self.length = seg.length
-
- class PartWrapper:
- def __init__(self, part):
- self.segList = []
- self.segCnt = len(part.getSegs())
- for idx, seg in enumerate(part.getSegs()):
- self.segList.append(SegWrapper(idx, seg))
-
- partWrapper = PartWrapper(part)
- partLen = part.length
- avgLen = partLen / (partWrapper.segCnt + toAddCnt)
-
- segsToDivide = [sr for sr in partWrapper.segList if sr.seg.length >= avgLen]
- segToDivideCnt = len(segsToDivide)
- avgLen = sum(sr.seg.length for sr in segsToDivide) / (segToDivideCnt + toAddCnt)
-
- segsToDivide = sorted(segsToDivide, key=lambda x: x.length, reverse = True)
-
- cnts = [0] * partWrapper.segCnt
- addedCnt = 0
-
-
- for i in range(0, segToDivideCnt):
- segLen = segsToDivide[i].seg.length
-
- divideCnt = int(round(segLen/avgLen)) - 1
- if(divideCnt == 0):
- break
-
- if((addedCnt + divideCnt) >= toAddCnt):
- cnts[segsToDivide[i].idx] = toAddCnt - addedCnt
- addedCnt = toAddCnt
- break
-
- cnts[segsToDivide[i].idx] = divideCnt
-
- addedCnt += divideCnt
-
- #TODO: Verify if needed
- while(toAddCnt > addedCnt):
- for i in range(0, segToDivideCnt):
- cnts[segsToDivide[i].idx] += 1
- addedCnt += 1
- if(toAddCnt == addedCnt):
- break
-
- return cnts
-
-#Just distribute equally; this is likely a rare condition. So why complicate?
-def distributeCnt(maxSegCntsByPart, startIdx, extraCnt):
- added = 0
- elemCnt = len(maxSegCntsByPart) - startIdx
- cntPerElem = floor(extraCnt / elemCnt)
- remainder = extraCnt % elemCnt
-
- for i in range(startIdx, len(maxSegCntsByPart)):
- maxSegCntsByPart[i] += cntPerElem
- if(i < remainder + startIdx):
- maxSegCntsByPart[i] += 1
-
-#Make all the paths to have the maximum number of segments in the set
-#TODO: Refactor
-def addMissingSegs(selPaths, byPart):
- maxSegCntsByPart = []
- maxSegCnt = 0
-
- resSegCnt = []
- sortedPaths = sorted(selPaths, key = lambda c: -len(c.parts))
-
- for i, path in enumerate(sortedPaths):
- if(byPart == False):
- segCnt = path.getPartView().getSegCnt()
- if(segCnt > maxSegCnt):
- maxSegCnt = segCnt
- else:
- resSegCnt.append([])
- for j, part in enumerate(path.parts):
- partSegCnt = part.getSegCnt()
- resSegCnt[i].append(partSegCnt)
-
- #First path
- if(j == len(maxSegCntsByPart)):
- maxSegCntsByPart.append(partSegCnt)
-
- #last part of this path, but other paths in set have more parts
- elif((j == len(path.parts) - 1) and
- len(maxSegCntsByPart) > len(path.parts)):
-
- remainingSegs = sum(maxSegCntsByPart[j:])
- if(partSegCnt <= remainingSegs):
- resSegCnt[i][j] = remainingSegs
- else:
- #This part has more segs than the sum of the remaining part segs
- #So distribute the extra count
- distributeCnt(maxSegCntsByPart, j, (partSegCnt - remainingSegs))
-
- #Also, adjust the seg count of the last part of the previous
- #segments that had fewer than max number of parts
- for k in range(0, i):
- if(len(sortedPaths[k].parts) < len(maxSegCntsByPart)):
- totalSegs = sum(maxSegCntsByPart)
- existingSegs = sum(maxSegCntsByPart[:len(sortedPaths[k].parts)-1])
- resSegCnt[k][-1] = totalSegs - existingSegs
-
- elif(partSegCnt > maxSegCntsByPart[j]):
- maxSegCntsByPart[j] = partSegCnt
- for i, path in enumerate(sortedPaths):
-
- if(byPart == False):
- partView = path.getPartView()
- segCnt = partView.getSegCnt()
- diff = maxSegCnt - segCnt
-
- if(diff > 0):
- cnts = getSubdivCntPerSeg(partView, diff)
- cumulSegIdx = 0
- for j in range(0, len(path.parts)):
- part = path.parts[j]
- newSegs = []
- for k, seg in enumerate(part.getSegs()):
- numSubdivs = cnts[cumulSegIdx] + 1
- newSegs += subdivideSeg(seg, numSubdivs)
- cumulSegIdx += 1
-
- path.parts[j] = Part(path, newSegs, part.isClosed)
- else:
- for j in range(0, len(path.parts)):
- part = path.parts[j]
- newSegs = []
-
- partSegCnt = part.getSegCnt()
-
- #TODO: Adding everything in the last part?
- if(j == (len(path.parts)-1) and
- len(maxSegCntsByPart) > len(path.parts)):
- diff = resSegCnt[i][j] - partSegCnt
- else:
- diff = maxSegCntsByPart[j] - partSegCnt
-
- if(diff > 0):
- cnts = getSubdivCntPerSeg(part, diff)
-
- for k, seg in enumerate(part.getSegs()):
- seg = part.getSeg(k)
- subdivCnt = cnts[k] + 1 #1 for the existing one
- newSegs += subdivideSeg(seg, subdivCnt)
-
- #isClosed won't be used, but let's update anyway
- path.parts[j] = Part(path, newSegs, part.isClosed)
-
-#TODO: Simplify (Not very readable)
-def alignPath(path, matchParts, matchCriteria, alignBy, alignValues):
-
- parts = path.parts[:]
-
- if(matchParts == 'custom'):
- fnMap = {'vCnt' : lambda part: -1 * part.getSegCnt(), \
- 'bbArea': lambda part: -1 * part.bboxSurfaceArea(worldSpace = True), \
- 'bbHeight' : lambda part: -1 * part.getBBHeight(worldSpace = True), \
- 'bbWidth' : lambda part: -1 * part.getBBWidth(worldSpace = True), \
- 'bbDepth' : lambda part: -1 * part.getBBDepth(worldSpace = True)
- }
- matchPartCmpFns = []
- for criterion in matchCriteria:
- fn = fnMap.get(criterion)
- if(fn == None):
- minmax = criterion[:3] == 'max' #0 if min; 1 if max
- axisIdx = ord(criterion[3:]) - ord('X')
-
- fn = eval('lambda part: part.getBBox(worldSpace = True)[' + \
- str(minmax) + '][' + str(axisIdx) + ']')
-
- matchPartCmpFns.append(fn)
-
- def comparer(left, right):
- for fn in matchPartCmpFns:
- a = fn(left)
- b = fn(right)
-
- if(floatCmpWithMargin(a, b)):
- continue
- else:
- return (a > b) - ( a < b) #No cmp in python3
-
- return 0
-
- parts = sorted(parts, key = cmp_to_key(comparer))
-
- alignCmpFn = None
- if(alignBy == 'vertCo'):
- def evalCmp(criteria, pt1, pt2):
- if(len(criteria) == 0):
- return True
-
- minmax = criteria[0][0]
- axisIdx = criteria[0][1]
- val1 = pt1[axisIdx]
- val2 = pt2[axisIdx]
-
- if(floatCmpWithMargin(val1, val2)):
- criteria = criteria[:]
- criteria.pop(0)
- return evalCmp(criteria, pt1, pt2)
-
- return val1 < val2 if minmax == 'min' else val1 > val2
-
- alignCri = [[a[:3], ord(a[3:]) - ord('X')] for a in alignValues]
- alignCmpFn = lambda pt1, pt2, curve: (evalCmp(alignCri, \
- curve.matrix_world @ pt1, curve.matrix_world @ pt2))
-
- startPt = None
- startIdx = None
-
- for i in range(0, len(parts)):
- #Only truly closed parts
- if(alignCmpFn != None and parts[i].isClosed):
- for j in range(0, parts[i].getSegCnt()):
- seg = parts[i].getSeg(j)
- if(j == 0 or alignCmpFn(seg.start, startPt, path.curve)):
- startPt = seg.start
- startIdx = j
-
- path.parts[i]= Part(path, parts[i].getSegsCopy(startIdx, None) + \
- parts[i].getSegsCopy(None, startIdx), parts[i].isClosed)
- else:
- path.parts[i] = parts[i]
-
-#TODO: Other shape key attributes like interpolation...?
-def getExistingShapeKeyPaths(path):
- obj = path.curve
- paths = []
-
- if(obj.data.shape_keys != None):
- keyblocks = obj.data.shape_keys.key_blocks[1:]#Skip basis
- for key in keyblocks:
- datacopy = obj.data.copy()
- i = 0
- for spline in datacopy.splines:
- for pt in spline.bezier_points:
- pt.co = key.data[i].co
- pt.handle_left = key.data[i].handle_left
- pt.handle_right = key.data[i].handle_right
- i += 1
- paths.append(Path(obj, datacopy, key.name))
- return paths
-
-def addShapeKeys(curve, paths, space):
- for path in paths:
- key = curve.shape_key_add(name = path.name)
- pts = [pt for pset in path.getBezierPtsBySpline() for pt in pset]
- for i, pt in enumerate(pts):
- if(space == 'worldspace'):
- pt = [curve.matrix_world.inverted() @ (path.curve.matrix_world @ p) for p in pt]
- key.data[i].co = pt[0]
- key.data[i].handle_left = pt[1]
- key.data[i].handle_right = pt[2]
-
-#TODO: Remove try
-def safeRemoveCurveObj(obj):
- try:
- collections = obj.users_collection
-
- for c in collections:
- c.objects.unlink(obj)
-
- if(obj.name in bpy.context.scene.collection.objects):
- bpy.context.scene.collection.objects.unlink(obj)
-
- if(obj.data.users == 1):
- if(obj.type == 'CURVE'):
- bpy.data.curves.remove(obj.data) #This also removes object?
- elif(obj.type == 'MESH'):
- bpy.data.meshes.remove(obj.data)
-
- bpy.data.objects.remove(obj)
- except:
- pass
-
-
-def markVertHandler(self, context):
- if(self.markVertex):
- bpy.ops.wm.mark_vertex()
-
-
-#################### UI and Registration ####################
-
-class AssignShapeKeysOp(bpy.types.Operator):
- bl_idname = "object.assign_shape_keys"
- bl_label = "Assign Shape Keys"
- bl_options = {'REGISTER', 'UNDO'}
-
- def execute(self, context):
- params = context.window_manager.AssignShapeKeyParams
- removeOriginal = params.removeOriginal
- space = params.space
-
- matchParts = params.matchParts
- matchCri1 = params.matchCri1
- matchCri2 = params.matchCri2
- matchCri3 = params.matchCri3
-
- alignBy = params.alignList
- alignVal1 = params.alignVal1
- alignVal2 = params.alignVal2
- alignVal3 = params.alignVal3
-
- createdObjsMap = main(removeOriginal, space, \
- matchParts, [matchCri1, matchCri2, matchCri3], \
- alignBy, [alignVal1, alignVal2, alignVal3])
-
- return {'FINISHED'}
-
-
-class MarkerController:
- drawHandlerRef = None
- defPointSize = 6
- ptColor = (0, .8, .8, 1)
-
- def createSMMap(self, context):
- objs = context.selected_objects
- smMap = {}
- for curve in objs:
- if(not isBezier(curve)):
- continue
-
- smMap[curve.name] = {}
- mw = curve.matrix_world
- for splineIdx, spline in enumerate(curve.data.splines):
- if(not spline.use_cyclic_u):
- continue
-
- #initialize to the curr start vert co and idx
- smMap[curve.name][splineIdx] = \
- [mw @ curve.data.splines[splineIdx].bezier_points[0].co, 0]
-
- for pt in spline.bezier_points:
- pt.select_control_point = False
-
- if(len(smMap[curve.name]) == 0):
- del smMap[curve.name]
-
- return smMap
-
- def createBatch(self, context):
- positions = [s[0] for cn in self.smMap.values() for s in cn.values()]
- colors = [MarkerController.ptColor for i in range(0, len(positions))]
-
- self.batch = batch_for_shader(self.shader, \
- "POINTS", {"pos": positions, "color": colors})
-
- if context.area:
- context.area.tag_redraw()
-
- def drawHandler(self):
- bgl.glPointSize(MarkerController.defPointSize)
- self.batch.draw(self.shader)
-
- def removeMarkers(self, context):
- if(MarkerController.drawHandlerRef != None):
- bpy.types.SpaceView3D.draw_handler_remove(MarkerController.drawHandlerRef, \
- "WINDOW")
-
- if(context.area and hasattr(context.space_data, 'region_3d')):
- context.area.tag_redraw()
-
- MarkerController.drawHandlerRef = None
-
- self.deselectAll()
-
- def __init__(self, context):
- self.smMap = self.createSMMap(context)
- self.shader = gpu.shader.from_builtin('3D_FLAT_COLOR')
- self.shader.bind()
-
- MarkerController.drawHandlerRef = \
- bpy.types.SpaceView3D.draw_handler_add(self.drawHandler, \
- (), "WINDOW", "POST_VIEW")
-
- self.createBatch(context)
-
- def saveStartVerts(self):
- for curveName in self.smMap.keys():
- curve = bpy.data.objects[curveName]
- splines = curve.data.splines
- spMap = self.smMap[curveName]
-
- for splineIdx in spMap.keys():
- markerInfo = spMap[splineIdx]
- if(markerInfo[1] != 0):
- pts = splines[splineIdx].bezier_points
- loc, idx = markerInfo[0], markerInfo[1]
- cnt = len(pts)
-
- ptCopy = [[p.co.copy(), p.handle_right.copy(), \
- p.handle_left.copy(), p.handle_right_type, \
- p.handle_left_type] for p in pts]
-
- for i, pt in enumerate(pts):
- srcIdx = (idx + i) % cnt
- p = ptCopy[srcIdx]
-
- #Must set the types first
- pt.handle_right_type = p[3]
- pt.handle_left_type = p[4]
- pt.co = p[0]
- pt.handle_right = p[1]
- pt.handle_left = p[2]
-
- def updateSMMap(self):
- for curveName in self.smMap.keys():
- curve = bpy.data.objects[curveName]
- spMap = self.smMap[curveName]
- mw = curve.matrix_world
-
- for splineIdx in spMap.keys():
- markerInfo = spMap[splineIdx]
- loc, idx = markerInfo[0], markerInfo[1]
- pts = curve.data.splines[splineIdx].bezier_points
-
- selIdxs = [x for x in range(0, len(pts)) \
- if pts[x].select_control_point == True]
-
- selIdx = selIdxs[0] if(len(selIdxs) > 0 ) else idx
- co = mw @ pts[selIdx].co
- self.smMap[curveName][splineIdx] = [co, selIdx]
-
- def deselectAll(self):
- for curveName in self.smMap.keys():
- curve = bpy.data.objects[curveName]
- for spline in curve.data.splines:
- for pt in spline.bezier_points:
- pt.select_control_point = False
-
- def getSpaces3D(context):
- areas3d = [area for area in context.window.screen.areas \
- if area.type == 'VIEW_3D']
-
- return [s for a in areas3d for s in a.spaces if s.type == 'VIEW_3D']
-
- def hideHandles(context):
- states = []
- spaces = MarkerController.getSpaces3D(context)
- for s in spaces:
- states.append(s.overlay.show_curve_handles)
- s.overlay.show_curve_handles = False
- return states
-
- def resetShowHandleState(context, handleStates):
- spaces = MarkerController.getSpaces3D(context)
- for i, s in enumerate(spaces):
- s.overlay.show_curve_handles = handleStates[i]
-
-
-class ModalMarkSegStartOp(bpy.types.Operator):
- bl_description = "Mark Vertex"
- bl_idname = "wm.mark_vertex"
- bl_label = "Mark Start Vertex"
-
- def cleanup(self, context):
- wm = context.window_manager
- wm.event_timer_remove(self._timer)
- self.markerState.removeMarkers(context)
- MarkerController.resetShowHandleState(context, self.handleStates)
- context.window_manager.AssignShapeKeyParams.markVertex = False
-
- def modal (self, context, event):
- params = context.window_manager.AssignShapeKeyParams
-
- if(context.mode == 'OBJECT' or event.type == "ESC" or\
- not context.window_manager.AssignShapeKeyParams.markVertex):
- self.cleanup(context)
- return {'CANCELLED'}
-
- elif(event.type == "RET"):
- self.markerState.saveStartVerts()
- self.cleanup(context)
- return {'FINISHED'}
-
- if(event.type == 'TIMER'):
- self.markerState.updateSMMap()
- self.markerState.createBatch(context)
-
- elif(event.type in {'LEFT_CTRL', 'RIGHT_CTRL'}):
- self.ctrl = (event.value == 'PRESS')
-
- elif(event.type in {'LEFT_SHIFT', 'RIGHT_SHIFT'}):
- self.shift = (event.value == 'PRESS')
-
- if(event.type not in {"MIDDLEMOUSE", "TAB", "LEFTMOUSE", \
- "RIGHTMOUSE", 'WHEELDOWNMOUSE', 'WHEELUPMOUSE'} and \
- not event.type.startswith("NUMPAD_")):
- return {'RUNNING_MODAL'}
-
- return {"PASS_THROUGH"}
-
- def execute(self, context):
- #TODO: Why such small step?
- self._timer = context.window_manager.event_timer_add(time_step = 0.0001, \
- window = context.window)
- self.ctrl = False
- self.shift = False
-
- context.window_manager.modal_handler_add(self)
- self.markerState = MarkerController(context)
-
- #Hide so that users don't accidentally select handles instead of points
- self.handleStates = MarkerController.hideHandles(context)
-
- return {"RUNNING_MODAL"}
-
-
-class AssignShapeKeyParams(bpy.types.PropertyGroup):
-
- removeOriginal : BoolProperty(name = "Remove Shape Key Objects", \
- description = "Remove shape key objects after assigning to target", \
- default = True)
-
- space : EnumProperty(name = "Space", \
- items = [('worldspace', 'World Space', 'worldspace'),
- ('localspace', 'Local Space', 'localspace')], \
- description = 'Space that shape keys are evluated in')
-
- alignList : EnumProperty(name="Vertex Alignment", items = \
- [("-None-", 'Manual Alignment', "Align curve segments based on starting vertex"), \
- ('vertCo', 'Vertex Coordinates', 'Align curve segments based on vertex coordinates')], \
- description = 'Start aligning the vertices of target and shape keys from',
- default = '-None-')
-
- alignVal1 : EnumProperty(name="Value 1",
- items = matchList, default = 'minX', description='First align criterion')
-
- alignVal2 : EnumProperty(name="Value 2",
- items = matchList, default = 'maxY', description='Second align criterion')
-
- alignVal3 : EnumProperty(name="Value 3",
- items = matchList, default = 'minZ', description='Third align criterion')
-
- matchParts : EnumProperty(name="Match Parts", items = \
- [("-None-", 'None', "Don't match parts"), \
- ('default', 'Default', 'Use part (spline) order as in curve'), \
- ('custom', 'Custom', 'Use one of the custom criteria for part matching')], \
- description='Match disconnected parts', default = 'default')
-
- matchCri1 : EnumProperty(name="Value 1",
- items = matchList, default = 'minX', description='First match criterion')
-
- matchCri2 : EnumProperty(name="Value 2",
- items = matchList, default = 'maxY', description='Second match criterion')
-
- matchCri3 : EnumProperty(name="Value 3",
- items = matchList, default = 'minZ', description='Third match criterion')
-
- markVertex : BoolProperty(name="Mark Starting Vertices", \
- description='Mark first vertices in all splines of selected curves', \
- default = False, update = markVertHandler)
-
-
-class AssignShapeKeysPanel(bpy.types.Panel):
-
- bl_label = "Assign Shape Keys"
- bl_idname = "CURVE_PT_assign_shape_keys"
- bl_space_type = 'VIEW_3D'
- bl_region_type = 'UI'
- bl_category = "Tool"
-
- @classmethod
- def poll(cls, context):
- return context.mode in {'OBJECT', 'EDIT_CURVE'}
-
- def draw(self, context):
-
- layout = self.layout
- col = layout.column()
- params = context.window_manager.AssignShapeKeyParams
-
- if(context.mode == 'OBJECT'):
- row = col.row()
- row.prop(params, "removeOriginal")
-
- row = col.row()
- row.prop(params, "space")
-
- row = col.row()
- row.prop(params, "alignList")
-
- if(params.alignList == 'vertCo'):
- row = col.row()
- row.prop(params, "alignVal1")
- row.prop(params, "alignVal2")
- row.prop(params, "alignVal3")
-
- row = col.row()
- row.prop(params, "matchParts")
-
- if(params.matchParts == 'custom'):
- row = col.row()
- row.prop(params, "matchCri1")
- row.prop(params, "matchCri2")
- row.prop(params, "matchCri3")
-
- row = col.row()
- row.operator("object.assign_shape_keys")
- else:
- col.prop(params, "markVertex", \
- toggle = True)
-
-
-# registering and menu integration
-def register():
- bpy.utils.register_class(AssignShapeKeysPanel)
- bpy.utils.register_class(AssignShapeKeysOp)
- bpy.utils.register_class(AssignShapeKeyParams)
- bpy.types.WindowManager.AssignShapeKeyParams = \
- bpy.props.PointerProperty(type=AssignShapeKeyParams)
- bpy.utils.register_class(ModalMarkSegStartOp)
-
-def unregister():
- bpy.utils.unregister_class(AssignShapeKeysOp)
- bpy.utils.unregister_class(AssignShapeKeysPanel)
- del bpy.types.WindowManager.AssignShapeKeyParams
- bpy.utils.unregister_class(AssignShapeKeyParams)
- bpy.utils.unregister_class(ModalMarkSegStartOp)
-
-if __name__ == "__main__":
- register()
+# +# +# This Blender add-on assigns one or more Bezier Curves as shape keys to another +# Bezier Curve +# +# Supported Blender Version: 2.80 Beta +# +# Copyright (C) 2019 Shrinivas Kulkarni +# +# License: MIT (https://github.com/Shriinivas/assignshapekey/blob/master/LICENSE) +# + +import bpy, bmesh, bgl, gpu +from gpu_extras.batch import batch_for_shader +from bpy.props import BoolProperty, EnumProperty +from collections import OrderedDict +from mathutils import Vector +from math import sqrt, floor +from functools import cmp_to_key + + +bl_info = { + "name": "Assign Shape Keys", + "author": "Shrinivas Kulkarni", + "version": (1, 0, 0), + "location": "Properties > Active Tool and Workspace Settings > Assign Shape Keys", + "description": "Assigns one or more Bezier curves as shape keys to another Bezier curve", + "category": "Object", + "wiki_url": "https://github.com/Shriinivas/assignshapekey/blob/master/README.md", + "blender": (2, 80, 0), +} + +matchList = [('vCnt', 'Vertex Count', 'Match by vertex count'), + ('bbArea', 'Area', 'Match by surface area of the bounding box'), \ + ('bbHeight', 'Height', 'Match by bounding box height'), \ + ('bbWidth', 'Width', 'Match by bounding box width'), + ('bbDepth', 'Depth', 'Match by bounding box depth'), + ('minX', 'Min X', 'Match by bounding bon Min X'), + ('maxX', 'Max X', 'Match by bounding bon Max X'), + ('minY', 'Min Y', 'Match by bounding bon Min Y'), + ('maxY', 'Max Y', 'Match by bounding bon Max Y'), + ('minZ', 'Min Z', 'Match by bounding bon Min Z'), + ('maxZ', 'Max Z', 'Match by bounding bon Max Z')] + +DEF_ERR_MARGIN = 0.0001 + +def isBezier(obj): + return obj.type == 'CURVE' and len(obj.data.splines) > 0 \ + and obj.data.splines[0].type == 'BEZIER' + +#Avoid errors due to floating point conversions/comparisons +#TODO: return -1, 0, 1 +def floatCmpWithMargin(float1, float2, margin = DEF_ERR_MARGIN): + return abs(float1 - float2) < margin + +def vectCmpWithMargin(v1, v2, margin = DEF_ERR_MARGIN): + return all(floatCmpWithMargin(v1[i], v2[i], margin) for i in range(0, len(v1))) + +class Segment(): + + #pts[0] - start, pts[1] - ctrl1, pts[2] - ctrl2, , pts[3] - end + def pointAtT(pts, t): + return pts[0] + t * (3 * (pts[1] - pts[0]) + + t* (3 * (pts[0] + pts[2]) - 6 * pts[1] + + t * (-pts[0] + 3 * (pts[1] - pts[2]) + pts[3]))) + + def getSegLenRecurs(pts, start, end, t1 = 0, t2 = 1, error = DEF_ERR_MARGIN): + t1_5 = (t1 + t2)/2 + mid = Segment.pointAtT(pts, t1_5) + l = (end - start).length + l2 = (mid - start).length + (end - mid).length + if (l2 - l > error): + return (Segment.getSegLenRecurs(pts, start, mid, t1, t1_5, error) + + Segment.getSegLenRecurs(pts, mid, end, t1_5, t2, error)) + return l2 + + def __init__(self, start, ctrl1, ctrl2, end): + self.start = start + self.ctrl1 = ctrl1 + self.ctrl2 = ctrl2 + self.end = end + pts = [start, ctrl1, ctrl2, end] + self.length = Segment.getSegLenRecurs(pts, start, end) + + #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 + def partialSeg(self, t0, t1): + pts = [self.start, self.ctrl1, self.ctrl2, self.end] + + if(t0 > t1): + tt = t1 + t1 = t0 + t0 = tt + + #Let's make at least the line segments of predictable length :) + if(pts[0] == pts[1] and pts[2] == pts[3]): + pt0 = Vector([(1 - t0) * pts[0][i] + t0 * pts[2][i] for i in range(0, 3)]) + pt1 = Vector([(1 - t1) * pts[0][i] + t1 * pts[2][i] for i in range(0, 3)]) + return Segment(pt0, pt0, pt1, pt1) + + u0 = 1.0 - t0 + u1 = 1.0 - t1 + + qa = [pts[0][i]*u0*u0 + pts[1][i]*2*t0*u0 + pts[2][i]*t0*t0 for i in range(0, 3)] + qb = [pts[0][i]*u1*u1 + pts[1][i]*2*t1*u1 + pts[2][i]*t1*t1 for i in range(0, 3)] + qc = [pts[1][i]*u0*u0 + pts[2][i]*2*t0*u0 + pts[3][i]*t0*t0 for i in range(0, 3)] + qd = [pts[1][i]*u1*u1 + pts[2][i]*2*t1*u1 + pts[3][i]*t1*t1 for i in range(0, 3)] + + pta = Vector([qa[i]*u0 + qc[i]*t0 for i in range(0, 3)]) + ptb = Vector([qa[i]*u1 + qc[i]*t1 for i in range(0, 3)]) + ptc = Vector([qb[i]*u0 + qd[i]*t0 for i in range(0, 3)]) + ptd = Vector([qb[i]*u1 + qd[i]*t1 for i in range(0, 3)]) + + return Segment(pta, ptb, ptc, ptd) + + #see https://stackoverflow.com/questions/24809978/calculating-the-bounding-box-of-cubic-bezier-curve + #(3 D - 9 C + 9 B - 3 A) t^2 + (6 A - 12 B + 6 C) t + 3 (B - A) + #pts[0] - start, pts[1] - ctrl1, pts[2] - ctrl2, , pts[3] - end + #TODO: Return Vectors to make world space calculations consistent + def bbox(self, mw = None): + def evalBez(AA, BB, CC, DD, t): + return AA * (1 - t) * (1 - t) * (1 - t) + \ + 3 * BB * t * (1 - t) * (1 - t) + \ + 3 * CC * t * t * (1 - t) + \ + DD * t * t * t + + A = self.start + B = self.ctrl1 + C = self.ctrl2 + D = self.end + + if(mw != None): + A = mw @ A + B = mw @ B + C = mw @ C + D = mw @ D + + MINXYZ = [min([A[i], D[i]]) for i in range(0, 3)] + MAXXYZ = [max([A[i], D[i]]) for i in range(0, 3)] + leftBotBack_rgtTopFront = [MINXYZ, MAXXYZ] + + a = [3 * D[i] - 9 * C[i] + 9 * B[i] - 3 * A[i] for i in range(0, 3)] + b = [6 * A[i] - 12 * B[i] + 6 * C[i] for i in range(0, 3)] + c = [3 * (B[i] - A[i]) for i in range(0, 3)] + + solnsxyz = [] + for i in range(0, 3): + solns = [] + if(a[i] == 0): + if(b[i] == 0): + solns.append(0)#Independent of t so lets take the starting pt + else: + solns.append(c[i] / b[i]) + else: + rootFact = b[i] * b[i] - 4 * a[i] * c[i] + if(rootFact >=0 ): + #Two solutions with + and - sqrt + solns.append((-b[i] + sqrt(rootFact)) / (2 * a[i])) + solns.append((-b[i] - sqrt(rootFact)) / (2 * a[i])) + solnsxyz.append(solns) + + for i, soln in enumerate(solnsxyz): + for j, t in enumerate(soln): + if(t < 1 and t > 0): + co = evalBez(A[i], B[i], C[i], D[i], t) + if(co < leftBotBack_rgtTopFront[0][i]): + leftBotBack_rgtTopFront[0][i] = co + if(co > leftBotBack_rgtTopFront[1][i]): + leftBotBack_rgtTopFront[1][i] = co + + return leftBotBack_rgtTopFront + + +class Part(): + def __init__(self, parent, segs, isClosed): + self.parent = parent + self.segs = segs + + #use_cyclic_u + self.isClosed = isClosed + + #Indicates if this should be closed based on its counterparts in other paths + self.toClose = isClosed + + self.length = sum(seg.length for seg in self.segs) + self.bbox = None + self.bboxWorldSpace = None + + def getSeg(self, idx): + return self.segs[idx] + + def getSegs(self): + return self.segs + + def getSegsCopy(self, start, end): + if(start == None): + start = 0 + if(end == None): + end = len(self.segs) + return self.segs[start:end] + + def getBBox(self, worldSpace): + #Avoid frequent calculations, as this will be called in compare method + if(not worldSpace and self.bbox != None): + return self.bbox + + if(worldSpace and self.bboxWorldSpace != None): + return self.bboxWorldSpace + + leftBotBack_rgtTopFront = [[None]*3,[None]*3] + + for seg in self.segs: + + if(worldSpace): + bb = seg.bbox(self.parent.curve.matrix_world) + else: + bb = seg.bbox() + + for i in range(0, 3): + if (leftBotBack_rgtTopFront[0][i] == None or \ + bb[0][i] < leftBotBack_rgtTopFront[0][i]): + leftBotBack_rgtTopFront[0][i] = bb[0][i] + + for i in range(0, 3): + if (leftBotBack_rgtTopFront[1][i] == None or \ + bb[1][i] > leftBotBack_rgtTopFront[1][i]): + leftBotBack_rgtTopFront[1][i] = bb[1][i] + + if(worldSpace): + self.bboxWorldSpace = leftBotBack_rgtTopFront + else: + self.bbox = leftBotBack_rgtTopFront + + return leftBotBack_rgtTopFront + + #private + def getBBDiff(self, axisIdx, worldSpace): + obj = self.parent.curve + bbox = self.getBBox(worldSpace) + diff = abs(bbox[1][axisIdx] - bbox[0][axisIdx]) + return diff + + def getBBWidth(self, worldSpace): + return self.getBBDiff(0, worldSpace) + + def getBBHeight(self, worldSpace): + return self.getBBDiff(1, worldSpace) + + def getBBDepth(self, worldSpace): + return self.getBBDiff(2, worldSpace) + + def bboxSurfaceArea(self, worldSpace): + leftBotBack_rgtTopFront = self.getBBox(worldSpace) + w = abs( leftBotBack_rgtTopFront[1][0] - leftBotBack_rgtTopFront[0][0] ) + l = abs( leftBotBack_rgtTopFront[1][1] - leftBotBack_rgtTopFront[0][1] ) + d = abs( leftBotBack_rgtTopFront[1][2] - leftBotBack_rgtTopFront[0][2] ) + + return 2 * (w * l + w * d + l * d) + + def getSegCnt(self): + return len(self.segs) + + def getBezierPtsInfo(self): + prevSeg = None + bezierPtsInfo = [] + + for j, seg in enumerate(self.getSegs()): + + pt = seg.start + handleRight = seg.ctrl1 + + if(j == 0): + if(self.toClose): + handleLeft = self.getSeg(-1).ctrl2 + else: + handleLeft = pt + else: + handleLeft = prevSeg.ctrl2 + + bezierPtsInfo.append([pt, handleLeft, handleRight]) + prevSeg = seg + + if(self.toClose == True): + bezierPtsInfo[-1][2] = seg.ctrl1 + else: + bezierPtsInfo.append([prevSeg.end, prevSeg.ctrl2, prevSeg.end]) + + return bezierPtsInfo + + def __repr__(self): + return str(self.length) + + +class Path: + def __init__(self, curve, objData = None, name = None): + + if(objData == None): + objData = curve.data + + if(name == None): + name = curve.name + + self.name = name + self.curve = curve + + self.parts = [Part(self, getSplineSegs(s), s.use_cyclic_u) for s in objData.splines] + + def getPartCnt(self): + return len(self.parts) + + def getPartView(self): + p = Part(self, [seg for part in self.parts for seg in part.getSegs()], None) + return p + + def getPartBoundaryIdxs(self): + cumulCntList = set() + cumulCnt = 0 + + for p in self.parts: + cumulCnt += p.getSegCnt() + cumulCntList.add(cumulCnt) + + return cumulCntList + + def updatePartsList(self, segCntsPerPart, byPart): + monolithicSegList = [seg for part in self.parts for seg in part.getSegs()] + oldParts = self.parts[:] + currPart = oldParts[0] + partIdx = 0 + self.parts.clear() + + for i in range(0, len(segCntsPerPart)): + if( i == 0): + currIdx = 0 + else: + currIdx = segCntsPerPart[i-1] + + nextIdx = segCntsPerPart[i] + isClosed = False + + if(vectCmpWithMargin(monolithicSegList[currIdx].start, \ + currPart.getSegs()[0].start) and \ + vectCmpWithMargin(monolithicSegList[nextIdx-1].end, \ + currPart.getSegs()[-1].end)): + isClosed = currPart.isClosed + + self.parts.append(Part(self, \ + monolithicSegList[currIdx:nextIdx], isClosed)) + + if(monolithicSegList[nextIdx-1] == currPart.getSegs()[-1]): + partIdx += 1 + if(partIdx < len(oldParts)): + currPart = oldParts[partIdx] + + def getBezierPtsBySpline(self): + data = [] + + for i, part in enumerate(self.parts): + data.append(part.getBezierPtsInfo()) + + return data + + def getNewCurveData(self): + + newCurveData = self.curve.data.copy() + newCurveData.splines.clear() + + splinesData = self.getBezierPtsBySpline() + + for i, newPoints in enumerate(splinesData): + + spline = newCurveData.splines.new('BEZIER') + spline.bezier_points.add(len(newPoints)-1) + spline.use_cyclic_u = self.parts[i].toClose + + for j in range(0, len(spline.bezier_points)): + newPoint = newPoints[j] + spline.bezier_points[j].co = newPoint[0] + spline.bezier_points[j].handle_left = newPoint[1] + spline.bezier_points[j].handle_right = newPoint[2] + spline.bezier_points[j].handle_right_type = 'FREE' + + return newCurveData + + def updateCurve(self): + curveData = self.curve.data + #Remove existing shape keys first + if(curveData.shape_keys != None): + keyblocks = reversed(curveData.shape_keys.key_blocks) + for sk in keyblocks: + self.curve.shape_key_remove(sk) + self.curve.data = self.getNewCurveData() + bpy.data.curves.remove(curveData) + +def main(removeOriginal, space, matchParts, matchCriteria, alignBy, alignValues): + targetObj = bpy.context.active_object + if(targetObj == None or not isBezier(targetObj)): + return + + target = Path(targetObj) + + shapekeys = [Path(c) for c in bpy.context.selected_objects if isBezier(c) \ + and c != bpy.context.active_object] + + if(len(shapekeys) == 0): + return + + shapekeys = getExistingShapeKeyPaths(target) + shapekeys + userSel = [target] + shapekeys + + for path in userSel: + alignPath(path, matchParts, matchCriteria, alignBy, alignValues) + + addMissingSegs(userSel, byPart = (matchParts != "-None-")) + + bIdxs = set() + for path in userSel: + bIdxs = bIdxs.union(path.getPartBoundaryIdxs()) + + for path in userSel: + path.updatePartsList(sorted(list(bIdxs)), byPart = False) + + #All will have the same part count by now + allToClose = [all(path.parts[j].isClosed for path in userSel) + for j in range(0, len(userSel[0].parts))] + + #All paths will have the same no of splines with the same no of bezier points + for path in userSel: + for j, part in enumerate(path.parts): + part.toClose = allToClose[j] + + target.updateCurve() + + target.curve.shape_key_add(name = 'Basis') + + addShapeKeys(target.curve, shapekeys, space) + + if(removeOriginal): + for path in userSel: + if(path.curve != target.curve): + safeRemoveCurveObj(path.curve) + + return {} + +def getSplineSegs(spline): + p = spline.bezier_points + segs = [Segment(p[i-1].co, p[i-1].handle_right, p[i].handle_left, p[i].co) \ + for i in range(1, len(p))] + if(spline.use_cyclic_u): + segs.append(Segment(p[-1].co, p[-1].handle_right, p[0].handle_left, p[0].co)) + return segs + +def subdivideSeg(origSeg, noSegs): + if(noSegs < 2): + return [origSeg] + + segs = [] + oldT = 0 + segLen = origSeg.length / noSegs + + for i in range(0, noSegs-1): + t = float(i+1) / noSegs + seg = origSeg.partialSeg(oldT, t) + segs.append(seg) + oldT = t + + seg = origSeg.partialSeg(oldT, 1) + segs.append(seg) + + return segs + + +def getSubdivCntPerSeg(part, toAddCnt): + + class SegWrapper: + def __init__(self, idx, seg): + self.idx = idx + self.seg = seg + self.length = seg.length + + class PartWrapper: + def __init__(self, part): + self.segList = [] + self.segCnt = len(part.getSegs()) + for idx, seg in enumerate(part.getSegs()): + self.segList.append(SegWrapper(idx, seg)) + + partWrapper = PartWrapper(part) + partLen = part.length + avgLen = partLen / (partWrapper.segCnt + toAddCnt) + + segsToDivide = [sr for sr in partWrapper.segList if sr.seg.length >= avgLen] + segToDivideCnt = len(segsToDivide) + avgLen = sum(sr.seg.length for sr in segsToDivide) / (segToDivideCnt + toAddCnt) + + segsToDivide = sorted(segsToDivide, key=lambda x: x.length, reverse = True) + + cnts = [0] * partWrapper.segCnt + addedCnt = 0 + + + for i in range(0, segToDivideCnt): + segLen = segsToDivide[i].seg.length + + divideCnt = int(round(segLen/avgLen)) - 1 + if(divideCnt == 0): + break + + if((addedCnt + divideCnt) >= toAddCnt): + cnts[segsToDivide[i].idx] = toAddCnt - addedCnt + addedCnt = toAddCnt + break + + cnts[segsToDivide[i].idx] = divideCnt + + addedCnt += divideCnt + + #TODO: Verify if needed + while(toAddCnt > addedCnt): + for i in range(0, segToDivideCnt): + cnts[segsToDivide[i].idx] += 1 + addedCnt += 1 + if(toAddCnt == addedCnt): + break + + return cnts + +#Just distribute equally; this is likely a rare condition. So why complicate? +def distributeCnt(maxSegCntsByPart, startIdx, extraCnt): + added = 0 + elemCnt = len(maxSegCntsByPart) - startIdx + cntPerElem = floor(extraCnt / elemCnt) + remainder = extraCnt % elemCnt + + for i in range(startIdx, len(maxSegCntsByPart)): + maxSegCntsByPart[i] += cntPerElem + if(i < remainder + startIdx): + maxSegCntsByPart[i] += 1 + +#Make all the paths to have the maximum number of segments in the set +#TODO: Refactor +def addMissingSegs(selPaths, byPart): + maxSegCntsByPart = [] + maxSegCnt = 0 + + resSegCnt = [] + sortedPaths = sorted(selPaths, key = lambda c: -len(c.parts)) + + for i, path in enumerate(sortedPaths): + if(byPart == False): + segCnt = path.getPartView().getSegCnt() + if(segCnt > maxSegCnt): + maxSegCnt = segCnt + else: + resSegCnt.append([]) + for j, part in enumerate(path.parts): + partSegCnt = part.getSegCnt() + resSegCnt[i].append(partSegCnt) + + #First path + if(j == len(maxSegCntsByPart)): + maxSegCntsByPart.append(partSegCnt) + + #last part of this path, but other paths in set have more parts + elif((j == len(path.parts) - 1) and + len(maxSegCntsByPart) > len(path.parts)): + + remainingSegs = sum(maxSegCntsByPart[j:]) + if(partSegCnt <= remainingSegs): + resSegCnt[i][j] = remainingSegs + else: + #This part has more segs than the sum of the remaining part segs + #So distribute the extra count + distributeCnt(maxSegCntsByPart, j, (partSegCnt - remainingSegs)) + + #Also, adjust the seg count of the last part of the previous + #segments that had fewer than max number of parts + for k in range(0, i): + if(len(sortedPaths[k].parts) < len(maxSegCntsByPart)): + totalSegs = sum(maxSegCntsByPart) + existingSegs = sum(maxSegCntsByPart[:len(sortedPaths[k].parts)-1]) + resSegCnt[k][-1] = totalSegs - existingSegs + + elif(partSegCnt > maxSegCntsByPart[j]): + maxSegCntsByPart[j] = partSegCnt + for i, path in enumerate(sortedPaths): + + if(byPart == False): + partView = path.getPartView() + segCnt = partView.getSegCnt() + diff = maxSegCnt - segCnt + + if(diff > 0): + cnts = getSubdivCntPerSeg(partView, diff) + cumulSegIdx = 0 + for j in range(0, len(path.parts)): + part = path.parts[j] + newSegs = [] + for k, seg in enumerate(part.getSegs()): + numSubdivs = cnts[cumulSegIdx] + 1 + newSegs += subdivideSeg(seg, numSubdivs) + cumulSegIdx += 1 + + path.parts[j] = Part(path, newSegs, part.isClosed) + else: + for j in range(0, len(path.parts)): + part = path.parts[j] + newSegs = [] + + partSegCnt = part.getSegCnt() + + #TODO: Adding everything in the last part? + if(j == (len(path.parts)-1) and + len(maxSegCntsByPart) > len(path.parts)): + diff = resSegCnt[i][j] - partSegCnt + else: + diff = maxSegCntsByPart[j] - partSegCnt + + if(diff > 0): + cnts = getSubdivCntPerSeg(part, diff) + + for k, seg in enumerate(part.getSegs()): + seg = part.getSeg(k) + subdivCnt = cnts[k] + 1 #1 for the existing one + newSegs += subdivideSeg(seg, subdivCnt) + + #isClosed won't be used, but let's update anyway + path.parts[j] = Part(path, newSegs, part.isClosed) + +#TODO: Simplify (Not very readable) +def alignPath(path, matchParts, matchCriteria, alignBy, alignValues): + + parts = path.parts[:] + + if(matchParts == 'custom'): + fnMap = {'vCnt' : lambda part: -1 * part.getSegCnt(), \ + 'bbArea': lambda part: -1 * part.bboxSurfaceArea(worldSpace = True), \ + 'bbHeight' : lambda part: -1 * part.getBBHeight(worldSpace = True), \ + 'bbWidth' : lambda part: -1 * part.getBBWidth(worldSpace = True), \ + 'bbDepth' : lambda part: -1 * part.getBBDepth(worldSpace = True) + } + matchPartCmpFns = [] + for criterion in matchCriteria: + fn = fnMap.get(criterion) + if(fn == None): + minmax = criterion[:3] == 'max' #0 if min; 1 if max + axisIdx = ord(criterion[3:]) - ord('X') + + fn = eval('lambda part: part.getBBox(worldSpace = True)[' + \ + str(minmax) + '][' + str(axisIdx) + ']') + + matchPartCmpFns.append(fn) + + def comparer(left, right): + for fn in matchPartCmpFns: + a = fn(left) + b = fn(right) + + if(floatCmpWithMargin(a, b)): + continue + else: + return (a > b) - ( a < b) #No cmp in python3 + + return 0 + + parts = sorted(parts, key = cmp_to_key(comparer)) + + alignCmpFn = None + if(alignBy == 'vertCo'): + def evalCmp(criteria, pt1, pt2): + if(len(criteria) == 0): + return True + + minmax = criteria[0][0] + axisIdx = criteria[0][1] + val1 = pt1[axisIdx] + val2 = pt2[axisIdx] + + if(floatCmpWithMargin(val1, val2)): + criteria = criteria[:] + criteria.pop(0) + return evalCmp(criteria, pt1, pt2) + + return val1 < val2 if minmax == 'min' else val1 > val2 + + alignCri = [[a[:3], ord(a[3:]) - ord('X')] for a in alignValues] + alignCmpFn = lambda pt1, pt2, curve: (evalCmp(alignCri, \ + curve.matrix_world @ pt1, curve.matrix_world @ pt2)) + + startPt = None + startIdx = None + + for i in range(0, len(parts)): + #Only truly closed parts + if(alignCmpFn != None and parts[i].isClosed): + for j in range(0, parts[i].getSegCnt()): + seg = parts[i].getSeg(j) + if(j == 0 or alignCmpFn(seg.start, startPt, path.curve)): + startPt = seg.start + startIdx = j + + path.parts[i]= Part(path, parts[i].getSegsCopy(startIdx, None) + \ + parts[i].getSegsCopy(None, startIdx), parts[i].isClosed) + else: + path.parts[i] = parts[i] + +#TODO: Other shape key attributes like interpolation...? +def getExistingShapeKeyPaths(path): + obj = path.curve + paths = [] + + if(obj.data.shape_keys != None): + keyblocks = obj.data.shape_keys.key_blocks[1:]#Skip basis + for key in keyblocks: + datacopy = obj.data.copy() + i = 0 + for spline in datacopy.splines: + for pt in spline.bezier_points: + pt.co = key.data[i].co + pt.handle_left = key.data[i].handle_left + pt.handle_right = key.data[i].handle_right + i += 1 + paths.append(Path(obj, datacopy, key.name)) + return paths + +def addShapeKeys(curve, paths, space): + for path in paths: + key = curve.shape_key_add(name = path.name) + pts = [pt for pset in path.getBezierPtsBySpline() for pt in pset] + for i, pt in enumerate(pts): + if(space == 'worldspace'): + pt = [curve.matrix_world.inverted() @ (path.curve.matrix_world @ p) for p in pt] + key.data[i].co = pt[0] + key.data[i].handle_left = pt[1] + key.data[i].handle_right = pt[2] + +#TODO: Remove try +def safeRemoveCurveObj(obj): + try: + collections = obj.users_collection + + for c in collections: + c.objects.unlink(obj) + + if(obj.name in bpy.context.scene.collection.objects): + bpy.context.scene.collection.objects.unlink(obj) + + if(obj.data.users == 1): + if(obj.type == 'CURVE'): + bpy.data.curves.remove(obj.data) #This also removes object? + elif(obj.type == 'MESH'): + bpy.data.meshes.remove(obj.data) + + bpy.data.objects.remove(obj) + except: + pass + + +def markVertHandler(self, context): + if(self.markVertex): + bpy.ops.wm.mark_vertex() + + +#################### UI and Registration #################### + +class AssignShapeKeysOp(bpy.types.Operator): + bl_idname = "object.assign_shape_keys" + bl_label = "Assign Shape Keys" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + params = context.window_manager.AssignShapeKeyParams + removeOriginal = params.removeOriginal + space = params.space + + matchParts = params.matchParts + matchCri1 = params.matchCri1 + matchCri2 = params.matchCri2 + matchCri3 = params.matchCri3 + + alignBy = params.alignList + alignVal1 = params.alignVal1 + alignVal2 = params.alignVal2 + alignVal3 = params.alignVal3 + + createdObjsMap = main(removeOriginal, space, \ + matchParts, [matchCri1, matchCri2, matchCri3], \ + alignBy, [alignVal1, alignVal2, alignVal3]) + + return {'FINISHED'} + + +class MarkerController: + drawHandlerRef = None + defPointSize = 6 + ptColor = (0, .8, .8, 1) + + def createSMMap(self, context): + objs = context.selected_objects + smMap = {} + for curve in objs: + if(not isBezier(curve)): + continue + + smMap[curve.name] = {} + mw = curve.matrix_world + for splineIdx, spline in enumerate(curve.data.splines): + if(not spline.use_cyclic_u): + continue + + #initialize to the curr start vert co and idx + smMap[curve.name][splineIdx] = \ + [mw @ curve.data.splines[splineIdx].bezier_points[0].co, 0] + + for pt in spline.bezier_points: + pt.select_control_point = False + + if(len(smMap[curve.name]) == 0): + del smMap[curve.name] + + return smMap + + def createBatch(self, context): + positions = [s[0] for cn in self.smMap.values() for s in cn.values()] + colors = [MarkerController.ptColor for i in range(0, len(positions))] + + self.batch = batch_for_shader(self.shader, \ + "POINTS", {"pos": positions, "color": colors}) + + if context.area: + context.area.tag_redraw() + + def drawHandler(self): + bgl.glPointSize(MarkerController.defPointSize) + self.batch.draw(self.shader) + + def removeMarkers(self, context): + if(MarkerController.drawHandlerRef != None): + bpy.types.SpaceView3D.draw_handler_remove(MarkerController.drawHandlerRef, \ + "WINDOW") + + if(context.area and hasattr(context.space_data, 'region_3d')): + context.area.tag_redraw() + + MarkerController.drawHandlerRef = None + + self.deselectAll() + + def __init__(self, context): + self.smMap = self.createSMMap(context) + self.shader = gpu.shader.from_builtin('3D_FLAT_COLOR') + self.shader.bind() + + MarkerController.drawHandlerRef = \ + bpy.types.SpaceView3D.draw_handler_add(self.drawHandler, \ + (), "WINDOW", "POST_VIEW") + + self.createBatch(context) + + def saveStartVerts(self): + for curveName in self.smMap.keys(): + curve = bpy.data.objects[curveName] + splines = curve.data.splines + spMap = self.smMap[curveName] + + for splineIdx in spMap.keys(): + markerInfo = spMap[splineIdx] + if(markerInfo[1] != 0): + pts = splines[splineIdx].bezier_points + loc, idx = markerInfo[0], markerInfo[1] + cnt = len(pts) + + ptCopy = [[p.co.copy(), p.handle_right.copy(), \ + p.handle_left.copy(), p.handle_right_type, \ + p.handle_left_type] for p in pts] + + for i, pt in enumerate(pts): + srcIdx = (idx + i) % cnt + p = ptCopy[srcIdx] + + #Must set the types first + pt.handle_right_type = p[3] + pt.handle_left_type = p[4] + pt.co = p[0] + pt.handle_right = p[1] + pt.handle_left = p[2] + + def updateSMMap(self): + for curveName in self.smMap.keys(): + curve = bpy.data.objects[curveName] + spMap = self.smMap[curveName] + mw = curve.matrix_world + + for splineIdx in spMap.keys(): + markerInfo = spMap[splineIdx] + loc, idx = markerInfo[0], markerInfo[1] + pts = curve.data.splines[splineIdx].bezier_points + + selIdxs = [x for x in range(0, len(pts)) \ + if pts[x].select_control_point == True] + + selIdx = selIdxs[0] if(len(selIdxs) > 0 ) else idx + co = mw @ pts[selIdx].co + self.smMap[curveName][splineIdx] = [co, selIdx] + + def deselectAll(self): + for curveName in self.smMap.keys(): + curve = bpy.data.objects[curveName] + for spline in curve.data.splines: + for pt in spline.bezier_points: + pt.select_control_point = False + + def getSpaces3D(context): + areas3d = [area for area in context.window.screen.areas \ + if area.type == 'VIEW_3D'] + + return [s for a in areas3d for s in a.spaces if s.type == 'VIEW_3D'] + + def hideHandles(context): + states = [] + spaces = MarkerController.getSpaces3D(context) + for s in spaces: + states.append(s.overlay.show_curve_handles) + s.overlay.show_curve_handles = False + return states + + def resetShowHandleState(context, handleStates): + spaces = MarkerController.getSpaces3D(context) + for i, s in enumerate(spaces): + s.overlay.show_curve_handles = handleStates[i] + + +class ModalMarkSegStartOp(bpy.types.Operator): + bl_description = "Mark Vertex" + bl_idname = "wm.mark_vertex" + bl_label = "Mark Start Vertex" + + def cleanup(self, context): + wm = context.window_manager + wm.event_timer_remove(self._timer) + self.markerState.removeMarkers(context) + MarkerController.resetShowHandleState(context, self.handleStates) + context.window_manager.AssignShapeKeyParams.markVertex = False + + def modal (self, context, event): + params = context.window_manager.AssignShapeKeyParams + + if(context.mode == 'OBJECT' or event.type == "ESC" or\ + not context.window_manager.AssignShapeKeyParams.markVertex): + self.cleanup(context) + return {'CANCELLED'} + + elif(event.type == "RET"): + self.markerState.saveStartVerts() + self.cleanup(context) + return {'FINISHED'} + + if(event.type == 'TIMER'): + self.markerState.updateSMMap() + self.markerState.createBatch(context) + + elif(event.type in {'LEFT_CTRL', 'RIGHT_CTRL'}): + self.ctrl = (event.value == 'PRESS') + + elif(event.type in {'LEFT_SHIFT', 'RIGHT_SHIFT'}): + self.shift = (event.value == 'PRESS') + + if(event.type not in {"MIDDLEMOUSE", "TAB", "LEFTMOUSE", \ + "RIGHTMOUSE", 'WHEELDOWNMOUSE', 'WHEELUPMOUSE'} and \ + not event.type.startswith("NUMPAD_")): + return {'RUNNING_MODAL'} + + return {"PASS_THROUGH"} + + def execute(self, context): + #TODO: Why such small step? + self._timer = context.window_manager.event_timer_add(time_step = 0.0001, \ + window = context.window) + self.ctrl = False + self.shift = False + + context.window_manager.modal_handler_add(self) + self.markerState = MarkerController(context) + + #Hide so that users don't accidentally select handles instead of points + self.handleStates = MarkerController.hideHandles(context) + + return {"RUNNING_MODAL"} + + +class AssignShapeKeyParams(bpy.types.PropertyGroup): + + removeOriginal : BoolProperty(name = "Remove Shape Key Objects", \ + description = "Remove shape key objects after assigning to target", \ + default = True) + + space : EnumProperty(name = "Space", \ + items = [('worldspace', 'World Space', 'worldspace'), + ('localspace', 'Local Space', 'localspace')], \ + description = 'Space that shape keys are evluated in') + + alignList : EnumProperty(name="Vertex Alignment", items = \ + [("-None-", 'Manual Alignment', "Align curve segments based on starting vertex"), \ + ('vertCo', 'Vertex Coordinates', 'Align curve segments based on vertex coordinates')], \ + description = 'Start aligning the vertices of target and shape keys from', + default = '-None-') + + alignVal1 : EnumProperty(name="Value 1", + items = matchList, default = 'minX', description='First align criterion') + + alignVal2 : EnumProperty(name="Value 2", + items = matchList, default = 'maxY', description='Second align criterion') + + alignVal3 : EnumProperty(name="Value 3", + items = matchList, default = 'minZ', description='Third align criterion') + + matchParts : EnumProperty(name="Match Parts", items = \ + [("-None-", 'None', "Don't match parts"), \ + ('default', 'Default', 'Use part (spline) order as in curve'), \ + ('custom', 'Custom', 'Use one of the custom criteria for part matching')], \ + description='Match disconnected parts', default = 'default') + + matchCri1 : EnumProperty(name="Value 1", + items = matchList, default = 'minX', description='First match criterion') + + matchCri2 : EnumProperty(name="Value 2", + items = matchList, default = 'maxY', description='Second match criterion') + + matchCri3 : EnumProperty(name="Value 3", + items = matchList, default = 'minZ', description='Third match criterion') + + markVertex : BoolProperty(name="Mark Starting Vertices", \ + description='Mark first vertices in all splines of selected curves', \ + default = False, update = markVertHandler) + + +class AssignShapeKeysPanel(bpy.types.Panel): + + bl_label = "Assign Shape Keys" + bl_idname = "CURVE_PT_assign_shape_keys" + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + bl_category = "Tool" + + @classmethod + def poll(cls, context): + return context.mode in {'OBJECT', 'EDIT_CURVE'} + + def draw(self, context): + + layout = self.layout + col = layout.column() + params = context.window_manager.AssignShapeKeyParams + + if(context.mode == 'OBJECT'): + row = col.row() + row.prop(params, "removeOriginal") + + row = col.row() + row.prop(params, "space") + + row = col.row() + row.prop(params, "alignList") + + if(params.alignList == 'vertCo'): + row = col.row() + row.prop(params, "alignVal1") + row.prop(params, "alignVal2") + row.prop(params, "alignVal3") + + row = col.row() + row.prop(params, "matchParts") + + if(params.matchParts == 'custom'): + row = col.row() + row.prop(params, "matchCri1") + row.prop(params, "matchCri2") + row.prop(params, "matchCri3") + + row = col.row() + row.operator("object.assign_shape_keys") + else: + col.prop(params, "markVertex", \ + toggle = True) + + +# registering and menu integration +def register(): + bpy.utils.register_class(AssignShapeKeysPanel) + bpy.utils.register_class(AssignShapeKeysOp) + bpy.utils.register_class(AssignShapeKeyParams) + bpy.types.WindowManager.AssignShapeKeyParams = \ + bpy.props.PointerProperty(type=AssignShapeKeyParams) + bpy.utils.register_class(ModalMarkSegStartOp) + +def unregister(): + bpy.utils.unregister_class(AssignShapeKeysOp) + bpy.utils.unregister_class(AssignShapeKeysPanel) + del bpy.types.WindowManager.AssignShapeKeyParams + bpy.utils.unregister_class(AssignShapeKeyParams) + bpy.utils.unregister_class(ModalMarkSegStartOp) + +if __name__ == "__main__": + register() |