Skip to content
Snippets Groups Projects
Commit 9aade9d9 authored by Shrinivas Kulkarni's avatar Shrinivas Kulkarni
Browse files

curve_assign_shapekey: 1) moved UI and registration to bottom of the script 2)...

curve_assign_shapekey: 1) moved UI and registration to bottom of the script 2) trimmed trailing whitespace 3) 2 lines between classes: T62799
parent 48b38ec3
Branches
Tags
No related merge requests found
...@@ -18,7 +18,6 @@ from mathutils import Vector ...@@ -18,7 +18,6 @@ from mathutils import Vector
from math import sqrt, floor from math import sqrt, floor
from functools import cmp_to_key from functools import cmp_to_key
#################### UI and Registration Stuff ####################
bl_info = { bl_info = {
"name": "Assign Shape Keys", "name": "Assign Shape Keys",
...@@ -42,1063 +41,1070 @@ matchList = [('vCnt', 'Vertex Count', 'Match by vertex count'), ...@@ -42,1063 +41,1070 @@ matchList = [('vCnt', 'Vertex Count', 'Match by vertex count'),
('minZ', 'Min Z', 'Match by bounding bon Min Z'), ('minZ', 'Min Z', 'Match by bounding bon Min Z'),
('maxZ', 'Max Z', 'Match by bounding bon Max Z')] ('maxZ', 'Max Z', 'Match by bounding bon Max Z')]
def markVertHandler(self, context): DEF_ERR_MARGIN = 0.0001
if(self.markVertex):
bpy.ops.wm.mark_vertex()
class AssignShapeKeyParams(bpy.types.PropertyGroup): def isBezier(obj):
return obj.type == 'CURVE' and len(obj.data.splines) > 0 \
and obj.data.splines[0].type == 'BEZIER'
removeOriginal : BoolProperty(name = "Remove Shape Key Objects", \ #Avoid errors due to floating point conversions/comparisons
description = "Remove shape key objects after assigning to target", \ #TODO: return -1, 0, 1
default = True) def floatCmpWithMargin(float1, float2, margin = DEF_ERR_MARGIN):
return abs(float1 - float2) < margin
space : EnumProperty(name = "Space", \ def vectCmpWithMargin(v1, v2, margin = DEF_ERR_MARGIN):
items = [('worldspace', 'World Space', 'worldspace'), return all(floatCmpWithMargin(v1[i], v2[i], margin) for i in range(0, len(v1)))
('localspace', 'Local Space', 'localspace')], \
description = 'Space that shape keys are evluated in')
alignList : EnumProperty(name="Vertex Alignment", items = \ class Segment():
[("-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", #pts[0] - start, pts[1] - ctrl1, pts[2] - ctrl2, , pts[3] - end
items = matchList, default = 'minX', description='First align criterion') 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])))
alignVal2 : EnumProperty(name="Value 2", def getSegLenRecurs(pts, start, end, t1 = 0, t2 = 1, error = DEF_ERR_MARGIN):
items = matchList, default = 'maxY', description='Second align criterion') 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
alignVal3 : EnumProperty(name="Value 3", def __init__(self, start, ctrl1, ctrl2, end):
items = matchList, default = 'minZ', description='Third align criterion') self.start = start
self.ctrl1 = ctrl1
self.ctrl2 = ctrl2
self.end = end
pts = [start, ctrl1, ctrl2, end]
self.length = Segment.getSegLenRecurs(pts, start, end)
matchParts : EnumProperty(name="Match Parts", items = \ #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
[("-None-", 'None', "Don't match parts"), \ def partialSeg(self, t0, t1):
('default', 'Default', 'Use part (spline) order as in curve'), \ pts = [self.start, self.ctrl1, self.ctrl2, self.end]
('custom', 'Custom', 'Use one of the custom criteria for part matching')], \
description='Match disconnected parts', default = 'default')
matchCri1 : EnumProperty(name="Value 1", if(t0 > t1):
items = matchList, default = 'minX', description='First match criterion') tt = t1
t1 = t0
t0 = tt
matchCri2 : EnumProperty(name="Value 2", #Let's make at least the line segments of predictable length :)
items = matchList, default = 'maxY', description='Second match criterion') 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)
matchCri3 : EnumProperty(name="Value 3", u0 = 1.0 - t0
items = matchList, default = 'minZ', description='Third match criterion') u1 = 1.0 - t1
markVertex : BoolProperty(name="Mark Starting Vertices", \ qa = [pts[0][i]*u0*u0 + pts[1][i]*2*t0*u0 + pts[2][i]*t0*t0 for i in range(0, 3)]
description='Mark first vertices in all splines of selected curves', \ qb = [pts[0][i]*u1*u1 + pts[1][i]*2*t1*u1 + pts[2][i]*t1*t1 for i in range(0, 3)]
default = False, update = markVertHandler) 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)]
class AssignShapeKeysPanel(bpy.types.Panel): 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)])
bl_label = "Assign Shape Keys" return Segment(pta, ptb, ptc, ptd)
bl_idname = "CURVE_PT_assign_shape_keys"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "Tool"
@classmethod #see https://stackoverflow.com/questions/24809978/calculating-the-bounding-box-of-cubic-bezier-curve
def poll(cls, context): #(3 D - 9 C + 9 B - 3 A) t^2 + (6 A - 12 B + 6 C) t + 3 (B - A)
return context.mode in {'OBJECT', 'EDIT_CURVE'} #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
def draw(self, context): A = self.start
B = self.ctrl1
C = self.ctrl2
D = self.end
layout = self.layout if(mw != None):
col = layout.column() A = mw @ A
params = context.window_manager.AssignShapeKeyParams B = mw @ B
C = mw @ C
D = mw @ D
if(context.mode == 'OBJECT'): MINXYZ = [min([A[i], D[i]]) for i in range(0, 3)]
row = col.row() MAXXYZ = [max([A[i], D[i]]) for i in range(0, 3)]
row.prop(params, "removeOriginal") leftBotBack_rgtTopFront = [MINXYZ, MAXXYZ]
row = col.row() a = [3 * D[i] - 9 * C[i] + 9 * B[i] - 3 * A[i] for i in range(0, 3)]
row.prop(params, "space") 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)]
row = col.row() solnsxyz = []
row.prop(params, "alignList") 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)
if(params.alignList == 'vertCo'): for i, soln in enumerate(solnsxyz):
row = col.row() for j, t in enumerate(soln):
row.prop(params, "alignVal1") if(t < 1 and t > 0):
row.prop(params, "alignVal2") co = evalBez(A[i], B[i], C[i], D[i], t)
row.prop(params, "alignVal3") if(co < leftBotBack_rgtTopFront[0][i]):
leftBotBack_rgtTopFront[0][i] = co
if(co > leftBotBack_rgtTopFront[1][i]):
leftBotBack_rgtTopFront[1][i] = co
row = col.row() return leftBotBack_rgtTopFront
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() class Part():
row.operator("object.assign_shape_keys") def __init__(self, parent, segs, isClosed):
else: self.parent = parent
col.prop(params, "markVertex", \ self.segs = segs
toggle = True)
class AssignShapeKeysOp(bpy.types.Operator): #use_cyclic_u
self.isClosed = isClosed
bl_idname = "object.assign_shape_keys" #Indicates if this should be closed based on its counterparts in other paths
bl_label = "Assign Shape Keys" self.toClose = isClosed
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context): self.length = sum(seg.length for seg in self.segs)
params = context.window_manager.AssignShapeKeyParams self.bbox = None
removeOriginal = params.removeOriginal self.bboxWorldSpace = None
space = params.space
matchParts = params.matchParts def getSeg(self, idx):
matchCri1 = params.matchCri1 return self.segs[idx]
matchCri2 = params.matchCri2
matchCri3 = params.matchCri3
alignBy = params.alignList def getSegs(self):
alignVal1 = params.alignVal1 return self.segs
alignVal2 = params.alignVal2
alignVal3 = params.alignVal3
createdObjsMap = main(removeOriginal, space, \ def getSegsCopy(self, start, end):
matchParts, [matchCri1, matchCri2, matchCri3], \ if(start == None):
alignBy, [alignVal1, alignVal2, alignVal3]) start = 0
if(end == None):
end = len(self.segs)
return self.segs[start:end]
return {'FINISHED'} 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
class MarkerController: if(worldSpace and self.bboxWorldSpace != None):
drawHandlerRef = None return self.bboxWorldSpace
defPointSize = 6
ptColor = (0, .8, .8, 1)
def createSMMap(self, context): leftBotBack_rgtTopFront = [[None]*3,[None]*3]
objs = context.selected_objects
smMap = {}
for curve in objs:
if(not isBezier(curve)):
continue
smMap[curve.name] = {} for seg in self.segs:
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 if(worldSpace):
smMap[curve.name][splineIdx] = \ bb = seg.bbox(self.parent.curve.matrix_world)
[mw @ curve.data.splines[splineIdx].bezier_points[0].co, 0] else:
bb = seg.bbox()
for pt in spline.bezier_points: for i in range(0, 3):
pt.select_control_point = False if (leftBotBack_rgtTopFront[0][i] == None or \
bb[0][i] < leftBotBack_rgtTopFront[0][i]):
leftBotBack_rgtTopFront[0][i] = bb[0][i]
if(len(smMap[curve.name]) == 0): for i in range(0, 3):
del smMap[curve.name] if (leftBotBack_rgtTopFront[1][i] == None or \
bb[1][i] > leftBotBack_rgtTopFront[1][i]):
leftBotBack_rgtTopFront[1][i] = bb[1][i]
return smMap if(worldSpace):
self.bboxWorldSpace = leftBotBack_rgtTopFront
else:
self.bbox = leftBotBack_rgtTopFront
def createBatch(self, context): return leftBotBack_rgtTopFront
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, \ #private
"POINTS", {"pos": positions, "color": colors}) def getBBDiff(self, axisIdx, worldSpace):
obj = self.parent.curve
bbox = self.getBBox(worldSpace)
diff = abs(bbox[1][axisIdx] - bbox[0][axisIdx])
return diff
if context.area: def getBBWidth(self, worldSpace):
context.area.tag_redraw() return self.getBBDiff(0, worldSpace)
def drawHandler(self): def getBBHeight(self, worldSpace):
bgl.glPointSize(MarkerController.defPointSize) return self.getBBDiff(1, worldSpace)
self.batch.draw(self.shader)
def removeMarkers(self, context): def getBBDepth(self, worldSpace):
if(MarkerController.drawHandlerRef != None): return self.getBBDiff(2, worldSpace)
bpy.types.SpaceView3D.draw_handler_remove(MarkerController.drawHandlerRef, \
"WINDOW")
if(context.area and hasattr(context.space_data, 'region_3d')): def bboxSurfaceArea(self, worldSpace):
context.area.tag_redraw() 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] )
MarkerController.drawHandlerRef = None return 2 * (w * l + w * d + l * d)
self.deselectAll() def getSegCnt(self):
return len(self.segs)
def __init__(self, context): def getBezierPtsInfo(self):
self.smMap = self.createSMMap(context) prevSeg = None
self.shader = gpu.shader.from_builtin('3D_FLAT_COLOR') bezierPtsInfo = []
self.shader.bind()
MarkerController.drawHandlerRef = \ for j, seg in enumerate(self.getSegs()):
bpy.types.SpaceView3D.draw_handler_add(self.drawHandler, \
(), "WINDOW", "POST_VIEW")
self.createBatch(context) pt = seg.start
handleRight = seg.ctrl1
def saveStartVerts(self): if(j == 0):
for curveName in self.smMap.keys(): if(self.toClose):
curve = bpy.data.objects[curveName] handleLeft = self.getSeg(-1).ctrl2
splines = curve.data.splines else:
spMap = self.smMap[curveName] handleLeft = pt
else:
handleLeft = prevSeg.ctrl2
for splineIdx in spMap.keys(): bezierPtsInfo.append([pt, handleLeft, handleRight])
markerInfo = spMap[splineIdx] prevSeg = seg
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(), \ if(self.toClose == True):
p.handle_left.copy(), p.handle_right_type, \ bezierPtsInfo[-1][2] = seg.ctrl1
p.handle_left_type] for p in pts] else:
bezierPtsInfo.append([prevSeg.end, prevSeg.ctrl2, prevSeg.end])
for i, pt in enumerate(pts): return bezierPtsInfo
srcIdx = (idx + i) % cnt
p = ptCopy[srcIdx]
#Must set the types first def __repr__(self):
pt.handle_right_type = p[3] return str(self.length)
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(): class Path:
markerInfo = spMap[splineIdx] def __init__(self, curve, objData = None, name = None):
loc, idx = markerInfo[0], markerInfo[1]
pts = curve.data.splines[splineIdx].bezier_points
selIdxs = [x for x in range(0, len(pts)) \ if(objData == None):
if pts[x].select_control_point == True] objData = curve.data
selIdx = selIdxs[0] if(len(selIdxs) > 0 ) else idx if(name == None):
co = mw @ pts[selIdx].co name = curve.name
self.smMap[curveName][splineIdx] = [co, selIdx]
def deselectAll(self): self.name = name
for curveName in self.smMap.keys(): self.curve = curve
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): self.parts = [Part(self, getSplineSegs(s), s.use_cyclic_u) for s in objData.splines]
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 getPartCnt(self):
return len(self.parts)
def hideHandles(context): def getPartView(self):
states = [] p = Part(self, [seg for part in self.parts for seg in part.getSegs()], None)
spaces = MarkerController.getSpaces3D(context) return p
for s in spaces:
states.append(s.overlay.show_curve_handles)
s.overlay.show_curve_handles = False
return states
def resetShowHandleState(context, handleStates): def getPartBoundaryIdxs(self):
spaces = MarkerController.getSpaces3D(context) cumulCntList = set()
for i, s in enumerate(spaces): cumulCnt = 0
s.overlay.show_curve_handles = handleStates[i]
class ModalMarkSegStartOp(bpy.types.Operator): for p in self.parts:
cumulCnt += p.getSegCnt()
cumulCntList.add(cumulCnt)
bl_description = "Mark Vertex" return cumulCntList
bl_idname = "wm.mark_vertex"
bl_label = "Mark Start Vertex"
def cleanup(self, context): def updatePartsList(self, segCntsPerPart, byPart):
wm = context.window_manager monolithicSegList = [seg for part in self.parts for seg in part.getSegs()]
wm.event_timer_remove(self._timer) oldParts = self.parts[:]
self.markerState.removeMarkers(context) currPart = oldParts[0]
MarkerController.resetShowHandleState(context, self.handleStates) partIdx = 0
context.window_manager.AssignShapeKeyParams.markVertex = False self.parts.clear()
def modal (self, context, event): for i in range(0, len(segCntsPerPart)):
params = context.window_manager.AssignShapeKeyParams if( i == 0):
currIdx = 0
else:
currIdx = segCntsPerPart[i-1]
if(context.mode == 'OBJECT' or event.type == "ESC" or\ nextIdx = segCntsPerPart[i]
not context.window_manager.AssignShapeKeyParams.markVertex): isClosed = False
self.cleanup(context)
return {'CANCELLED'}
elif(event.type == "RET"): if(vectCmpWithMargin(monolithicSegList[currIdx].start, \
self.markerState.saveStartVerts() currPart.getSegs()[0].start) and \
self.cleanup(context) vectCmpWithMargin(monolithicSegList[nextIdx-1].end, \
return {'FINISHED'} currPart.getSegs()[-1].end)):
isClosed = currPart.isClosed
if(event.type == 'TIMER'): self.parts.append(Part(self, \
self.markerState.updateSMMap() monolithicSegList[currIdx:nextIdx], isClosed))
self.markerState.createBatch(context)
elif(event.type in {'LEFT_CTRL', 'RIGHT_CTRL'}): if(monolithicSegList[nextIdx-1] == currPart.getSegs()[-1]):
self.ctrl = (event.value == 'PRESS') partIdx += 1
if(partIdx < len(oldParts)):
currPart = oldParts[partIdx]
elif(event.type in {'LEFT_SHIFT', 'RIGHT_SHIFT'}): def getBezierPtsBySpline(self):
self.shift = (event.value == 'PRESS') data = []
if(event.type not in {"MIDDLEMOUSE", "TAB", "LEFTMOUSE", \ for i, part in enumerate(self.parts):
"RIGHTMOUSE", 'WHEELDOWNMOUSE', 'WHEELUPMOUSE'} and \ data.append(part.getBezierPtsInfo())
not event.type.startswith("NUMPAD_")):
return {'RUNNING_MODAL'}
return {"PASS_THROUGH"} return data
def execute(self, context): def getNewCurveData(self):
#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) newCurveData = self.curve.data.copy()
self.markerState = MarkerController(context) newCurveData.splines.clear()
#Hide so that users don't accidentally select handles instead of points splinesData = self.getBezierPtsBySpline()
self.handleStates = MarkerController.hideHandles(context)
return {"RUNNING_MODAL"} for i, newPoints in enumerate(splinesData):
def register(): spline = newCurveData.splines.new('BEZIER')
bpy.utils.register_class(AssignShapeKeysPanel) spline.bezier_points.add(len(newPoints)-1)
bpy.utils.register_class(AssignShapeKeysOp) spline.use_cyclic_u = self.parts[i].toClose
bpy.utils.register_class(AssignShapeKeyParams)
bpy.types.WindowManager.AssignShapeKeyParams = \
bpy.props.PointerProperty(type=AssignShapeKeyParams)
bpy.utils.register_class(ModalMarkSegStartOp)
def unregister(): for j in range(0, len(spline.bezier_points)):
bpy.utils.unregister_class(AssignShapeKeysOp) newPoint = newPoints[j]
bpy.utils.unregister_class(AssignShapeKeysPanel) spline.bezier_points[j].co = newPoint[0]
del bpy.types.WindowManager.AssignShapeKeyParams spline.bezier_points[j].handle_left = newPoint[1]
bpy.utils.unregister_class(AssignShapeKeyParams) spline.bezier_points[j].handle_right = newPoint[2]
bpy.utils.unregister_class(ModalMarkSegStartOp) spline.bezier_points[j].handle_right_type = 'FREE'
if __name__ == "__main__": return newCurveData
register()
#################### Addon code starts #################### def addCurve(self):
curveData = self.getNewCurveData()
obj = self.curve.copy()
obj.data = curveData
if(obj.data.shape_keys != None):
keyblocks = reversed(obj.data.shape_keys.key_blocks)
for sk in keyblocks:
obj.shape_key_remove(sk)
DEF_ERR_MARGIN = 0.0001 collections = self.curve.users_collection
for collection in collections:
collection.objects.link(obj)
def isBezier(obj): if(self.curve.name in bpy.context.scene.collection.objects and \
return obj.type == 'CURVE' and len(obj.data.splines) > 0 \ obj.name not in bpy.context.scene.collection.objects):
and obj.data.splines[0].type == 'BEZIER' bpy.context.scene.collection.objects.link(obj)
#Avoid errors due to floating point conversions/comparisons return obj
#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): def main(removeOriginal, space, matchParts, matchCriteria, alignBy, alignValues):
return all(floatCmpWithMargin(v1[i], v2[i], margin) for i in range(0, len(v1))) targetObj = bpy.context.active_object
if(targetObj == None or not isBezier(targetObj)):
return
class Segment(): target = Path(targetObj)
#pts[0] - start, pts[1] - ctrl1, pts[2] - ctrl2, , pts[3] - end shapekeys = [Path(c) for c in bpy.context.selected_objects if isBezier(c) \
def pointAtT(pts, t): and c != bpy.context.active_object]
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): if(len(shapekeys) == 0):
t1_5 = (t1 + t2)/2 return
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): shapekeys = getExistingShapeKeyPaths(target) + shapekeys
self.start = start userSel = [target] + shapekeys
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 for path in userSel:
def partialSeg(self, t0, t1): alignPath(path, matchParts, matchCriteria, alignBy, alignValues)
pts = [self.start, self.ctrl1, self.ctrl2, self.end]
if(t0 > t1): addMissingSegs(userSel, byPart = (matchParts != "-None-"))
tt = t1
t1 = t0
t0 = tt
#Let's make at least the line segments of predictable length :) bIdxs = set()
if(pts[0] == pts[1] and pts[2] == pts[3]): for path in userSel:
pt0 = Vector([(1 - t0) * pts[0][i] + t0 * pts[2][i] for i in range(0, 3)]) bIdxs = bIdxs.union(path.getPartBoundaryIdxs())
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 for path in userSel:
u1 = 1.0 - t1 path.updatePartsList(sorted(list(bIdxs)), byPart = False)
qa = [pts[0][i]*u0*u0 + pts[1][i]*2*t0*u0 + pts[2][i]*t0*t0 for i in range(0, 3)] #All will have the same part count by now
qb = [pts[0][i]*u1*u1 + pts[1][i]*2*t1*u1 + pts[2][i]*t1*t1 for i in range(0, 3)] allToClose = [all(path.parts[j].isClosed for path in userSel)
qc = [pts[1][i]*u0*u0 + pts[2][i]*2*t0*u0 + pts[3][i]*t0*t0 for i in range(0, 3)] for j in range(0, len(userSel[0].parts))]
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)]) #All paths will have the same no of splines with the same no of bezier points
ptb = Vector([qa[i]*u1 + qc[i]*t1 for i in range(0, 3)]) for path in userSel:
ptc = Vector([qb[i]*u0 + qd[i]*t0 for i in range(0, 3)]) for j, part in enumerate(path.parts):
ptd = Vector([qb[i]*u1 + qd[i]*t1 for i in range(0, 3)]) part.toClose = allToClose[j]
return Segment(pta, ptb, ptc, ptd) curve = target.addCurve()
#see https://stackoverflow.com/questions/24809978/calculating-the-bounding-box-of-cubic-bezier-curve curve.shape_key_add(name = 'Basis')
#(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 addShapeKey(curve, shapekeys, space)
B = self.ctrl1
C = self.ctrl2
D = self.end
if(mw != None): if(removeOriginal):
A = mw @ A for path in userSel:
B = mw @ B safeRemoveCurveObj(path.curve)
C = mw @ C
D = mw @ D
MINXYZ = [min([A[i], D[i]]) for i in range(0, 3)] return {}
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)] def getSplineSegs(spline):
b = [6 * A[i] - 12 * B[i] + 6 * C[i] for i in range(0, 3)] p = spline.bezier_points
c = [3 * (B[i] - A[i]) for i in range(0, 3)] 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
solnsxyz = [] def subdivideSeg(origSeg, noSegs):
for i in range(0, 3): if(noSegs < 2):
solns = [] return [origSeg]
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): segs = []
for j, t in enumerate(soln): oldT = 0
if(t < 1 and t > 0): segLen = origSeg.length / noSegs
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 for i in range(0, noSegs-1):
t = float(i+1) / noSegs
seg = origSeg.partialSeg(oldT, t)
segs.append(seg)
oldT = t
class Part(): seg = origSeg.partialSeg(oldT, 1)
def __init__(self, parent, segs, isClosed): segs.append(seg)
self.parent = parent
self.segs = segs
#use_cyclic_u return segs
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) def getSubdivCntPerSeg(part, toAddCnt):
self.bbox = None
self.bboxWorldSpace = None
def getSeg(self, idx): class SegWrapper:
return self.segs[idx] def __init__(self, idx, seg):
self.idx = idx
self.seg = seg
self.length = seg.length
def getSegs(self): class PartWrapper:
return self.segs def __init__(self, part):
self.segList = []
self.segCnt = len(part.getSegs())
for idx, seg in enumerate(part.getSegs()):
self.segList.append(SegWrapper(idx, seg))
def getSegsCopy(self, start, end): partWrapper = PartWrapper(part)
if(start == None): partLen = part.length
start = 0 avgLen = partLen / (partWrapper.segCnt + toAddCnt)
if(end == None):
end = len(self.segs)
return self.segs[start:end]
def getBBox(self, worldSpace): segsToDivide = [sr for sr in partWrapper.segList if sr.seg.length >= avgLen]
#Avoid frequent calculations, as this will be called in compare method segToDivideCnt = len(segsToDivide)
if(not worldSpace and self.bbox != None): avgLen = sum(sr.seg.length for sr in segsToDivide) / (segToDivideCnt + toAddCnt)
return self.bbox
if(worldSpace and self.bboxWorldSpace != None): segsToDivide = sorted(segsToDivide, key=lambda x: x.length, reverse = True)
return self.bboxWorldSpace
leftBotBack_rgtTopFront = [[None]*3,[None]*3] cnts = [0] * partWrapper.segCnt
addedCnt = 0
for seg in self.segs:
if(worldSpace): for i in range(0, segToDivideCnt):
bb = seg.bbox(self.parent.curve.matrix_world) segLen = segsToDivide[i].seg.length
else:
bb = seg.bbox()
for i in range(0, 3): divideCnt = int(round(segLen/avgLen)) - 1
if (leftBotBack_rgtTopFront[0][i] == None or \ if(divideCnt == 0):
bb[0][i] < leftBotBack_rgtTopFront[0][i]): break
leftBotBack_rgtTopFront[0][i] = bb[0][i]
for i in range(0, 3): if((addedCnt + divideCnt) >= toAddCnt):
if (leftBotBack_rgtTopFront[1][i] == None or \ cnts[segsToDivide[i].idx] = toAddCnt - addedCnt
bb[1][i] > leftBotBack_rgtTopFront[1][i]): addedCnt = toAddCnt
leftBotBack_rgtTopFront[1][i] = bb[1][i] break
if(worldSpace): cnts[segsToDivide[i].idx] = divideCnt
self.bboxWorldSpace = leftBotBack_rgtTopFront
else:
self.bbox = leftBotBack_rgtTopFront
return leftBotBack_rgtTopFront addedCnt += divideCnt
#private #TODO: Verify if needed
def getBBDiff(self, axisIdx, worldSpace): while(toAddCnt > addedCnt):
obj = self.parent.curve for i in range(0, segToDivideCnt):
bbox = self.getBBox(worldSpace) cnts[segsToDivide[i].idx] += 1
diff = abs(bbox[1][axisIdx] - bbox[0][axisIdx]) addedCnt += 1
return diff if(toAddCnt == addedCnt):
break
def getBBWidth(self, worldSpace):
return self.getBBDiff(0, worldSpace)
def getBBHeight(self, worldSpace): return cnts
return self.getBBDiff(1, worldSpace)
def getBBDepth(self, worldSpace): #Just distribute equally; this is likely a rare condition. So why complicate?
return self.getBBDiff(2, worldSpace) def distributeCnt(maxSegCntsByPart, startIdx, extraCnt):
added = 0
elemCnt = len(maxSegCntsByPart) - startIdx
cntPerElem = floor(extraCnt / elemCnt)
remainder = extraCnt % elemCnt
def bboxSurfaceArea(self, worldSpace): for i in range(startIdx, len(maxSegCntsByPart)):
leftBotBack_rgtTopFront = self.getBBox(worldSpace) maxSegCntsByPart[i] += cntPerElem
w = abs( leftBotBack_rgtTopFront[1][0] - leftBotBack_rgtTopFront[0][0] ) if(i < remainder + startIdx):
l = abs( leftBotBack_rgtTopFront[1][1] - leftBotBack_rgtTopFront[0][1] ) maxSegCntsByPart[i] += 1
d = abs( leftBotBack_rgtTopFront[1][2] - leftBotBack_rgtTopFront[0][2] )
return 2 * (w * l + w * d + l * d) #Make all the paths to have the maximum number of segments in the set
#TODO: Refactor
def addMissingSegs(selPaths, byPart):
maxSegCntsByPart = []
maxSegCnt = 0
def getSegCnt(self): resSegCnt = []
return len(self.segs) sortedPaths = sorted(selPaths, key = lambda c: -len(c.parts))
def getBezierPtsInfo(self): for i, path in enumerate(sortedPaths):
prevSeg = None if(byPart == False):
bezierPtsInfo = [] 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)
for j, seg in enumerate(self.getSegs()): #First path
if(j == len(maxSegCntsByPart)):
maxSegCntsByPart.append(partSegCnt)
pt = seg.start #last part of this path, but other paths in set have more parts
handleRight = seg.ctrl1 elif((j == len(path.parts) - 1) and
len(maxSegCntsByPart) > len(path.parts)):
if(j == 0): remainingSegs = sum(maxSegCntsByPart[j:])
if(self.toClose): if(partSegCnt <= remainingSegs):
handleLeft = self.getSeg(-1).ctrl2 resSegCnt[i][j] = remainingSegs
else:
handleLeft = pt
else: else:
handleLeft = prevSeg.ctrl2 #This part has more segs than the sum of the remaining part segs
#So distribute the extra count
distributeCnt(maxSegCntsByPart, j, (partSegCnt - remainingSegs))
bezierPtsInfo.append([pt, handleLeft, handleRight]) #Also, adjust the seg count of the last part of the previous
prevSeg = seg #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
if(self.toClose == True): elif(partSegCnt > maxSegCntsByPart[j]):
bezierPtsInfo[-1][2] = seg.ctrl1 maxSegCntsByPart[j] = partSegCnt
else: for i, path in enumerate(sortedPaths):
bezierPtsInfo.append([prevSeg.end, prevSeg.ctrl2, prevSeg.end])
return bezierPtsInfo if(byPart == False):
partView = path.getPartView()
segCnt = partView.getSegCnt()
diff = maxSegCnt - segCnt
def __repr__(self): if(diff > 0):
return str(self.length) 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 = []
class Path: partSegCnt = part.getSegCnt()
def __init__(self, curve, objData = None, name = None):
if(objData == None): #TODO: Adding everything in the last part?
objData = curve.data if(j == (len(path.parts)-1) and
len(maxSegCntsByPart) > len(path.parts)):
diff = resSegCnt[i][j] - partSegCnt
else:
diff = maxSegCntsByPart[j] - partSegCnt
if(name == None): if(diff > 0):
name = curve.name cnts = getSubdivCntPerSeg(part, diff)
self.name = name for k, seg in enumerate(part.getSegs()):
self.curve = curve seg = part.getSeg(k)
subdivCnt = cnts[k] + 1 #1 for the existing one
newSegs += subdivideSeg(seg, subdivCnt)
self.parts = [Part(self, getSplineSegs(s), s.use_cyclic_u) for s in objData.splines] #isClosed won't be used, but let's update anyway
path.parts[j] = Part(path, newSegs, part.isClosed)
def getPartCnt(self): #TODO: Simplify (Not very readable)
return len(self.parts) def alignPath(path, matchParts, matchCriteria, alignBy, alignValues):
def getPartView(self): parts = path.parts[:]
p = Part(self, [seg for part in self.parts for seg in part.getSegs()], None)
return p
def getPartBoundaryIdxs(self): if(matchParts == 'custom'):
cumulCntList = set() fnMap = {'vCnt' : lambda part: -1 * part.getSegCnt(), \
cumulCnt = 0 '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')
for p in self.parts: fn = eval('lambda part: part.getBBox(worldSpace = True)[' + \
cumulCnt += p.getSegCnt() str(minmax) + '][' + str(axisIdx) + ']')
cumulCntList.add(cumulCnt)
return cumulCntList matchPartCmpFns.append(fn)
def updatePartsList(self, segCntsPerPart, byPart): def comparer(left, right):
monolithicSegList = [seg for part in self.parts for seg in part.getSegs()] for fn in matchPartCmpFns:
oldParts = self.parts[:] a = fn(left)
currPart = oldParts[0] b = fn(right)
partIdx = 0
self.parts.clear()
for i in range(0, len(segCntsPerPart)): if(floatCmpWithMargin(a, b)):
if( i == 0): continue
currIdx = 0
else: else:
currIdx = segCntsPerPart[i-1] return (a > b) - ( a < b) #No cmp in python3
nextIdx = segCntsPerPart[i]
isClosed = False
if(vectCmpWithMargin(monolithicSegList[currIdx].start, \ return 0
currPart.getSegs()[0].start) and \
vectCmpWithMargin(monolithicSegList[nextIdx-1].end, \
currPart.getSegs()[-1].end)):
isClosed = currPart.isClosed
self.parts.append(Part(self, \ parts = sorted(parts, key = cmp_to_key(comparer))
monolithicSegList[currIdx:nextIdx], isClosed))
if(monolithicSegList[nextIdx-1] == currPart.getSegs()[-1]): alignCmpFn = None
partIdx += 1 if(alignBy == 'vertCo'):
if(partIdx < len(oldParts)): def evalCmp(criteria, pt1, pt2):
currPart = oldParts[partIdx] if(len(criteria) == 0):
return True
def getBezierPtsBySpline(self): minmax = criteria[0][0]
data = [] axisIdx = criteria[0][1]
val1 = pt1[axisIdx]
val2 = pt2[axisIdx]
for i, part in enumerate(self.parts): if(floatCmpWithMargin(val1, val2)):
data.append(part.getBezierPtsInfo()) criteria = criteria[:]
criteria.pop(0)
return evalCmp(criteria, pt1, pt2)
return data return val1 < val2 if minmax == 'min' else val1 > val2
def getNewCurveData(self): 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))
newCurveData = self.curve.data.copy() startPt = None
newCurveData.splines.clear() startIdx = None
splinesData = self.getBezierPtsBySpline() 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
for i, newPoints in enumerate(splinesData): path.parts[i]= Part(path, parts[i].getSegsCopy(startIdx, None) + \
parts[i].getSegsCopy(None, startIdx), parts[i].isClosed)
else:
path.parts[i] = parts[i]
spline = newCurveData.splines.new('BEZIER') #TODO: Other shape key attributes like interpolation...?
spline.bezier_points.add(len(newPoints)-1) def getExistingShapeKeyPaths(path):
spline.use_cyclic_u = self.parts[i].toClose obj = path.curve
paths = []
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 addCurve(self):
curveData = self.getNewCurveData()
obj = self.curve.copy()
obj.data = curveData
if(obj.data.shape_keys != None): if(obj.data.shape_keys != None):
keyblocks = reversed(obj.data.shape_keys.key_blocks) keyblocks = obj.data.shape_keys.key_blocks[1:]#Skip basis
for sk in keyblocks: for key in keyblocks:
obj.shape_key_remove(sk) 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
collections = self.curve.users_collection def addShapeKey(curve, paths, space):
for collection in collections: for path in paths:
collection.objects.link(obj) 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]
if(self.curve.name in bpy.context.scene.collection.objects and \ #TODO: Remove try
obj.name not in bpy.context.scene.collection.objects): def safeRemoveCurveObj(obj):
bpy.context.scene.collection.objects.link(obj) try:
collections = obj.users_collection
return obj for c in collections:
c.objects.unlink(obj)
def main(removeOriginal, space, matchParts, matchCriteria, alignBy, alignValues): if(obj.name in bpy.context.scene.collection.objects):
targetObj = bpy.context.active_object bpy.context.scene.collection.objects.unlink(obj)
if(targetObj == None or not isBezier(targetObj)):
return
target = Path(targetObj) 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)
shapekeys = [Path(c) for c in bpy.context.selected_objects if isBezier(c) \ bpy.data.objects.remove(obj)
and c != bpy.context.active_object] except:
pass
if(len(shapekeys) == 0):
return
shapekeys = getExistingShapeKeyPaths(target) + shapekeys def markVertHandler(self, context):
userSel = [target] + shapekeys if(self.markVertex):
bpy.ops.wm.mark_vertex()
for path in userSel:
alignPath(path, matchParts, matchCriteria, alignBy, alignValues)
addMissingSegs(userSel, byPart = (matchParts != "-None-")) #################### UI and Registration ####################
bIdxs = set() class AssignShapeKeysOp(bpy.types.Operator):
for path in userSel: bl_idname = "object.assign_shape_keys"
bIdxs = bIdxs.union(path.getPartBoundaryIdxs()) bl_label = "Assign Shape Keys"
bl_options = {'REGISTER', 'UNDO'}
for path in userSel: def execute(self, context):
path.updatePartsList(sorted(list(bIdxs)), byPart = False) params = context.window_manager.AssignShapeKeyParams
removeOriginal = params.removeOriginal
space = params.space
#All will have the same part count by now matchParts = params.matchParts
allToClose = [all(path.parts[j].isClosed for path in userSel) matchCri1 = params.matchCri1
for j in range(0, len(userSel[0].parts))] matchCri2 = params.matchCri2
matchCri3 = params.matchCri3
#All paths will have the same no of splines with the same no of bezier points alignBy = params.alignList
for path in userSel: alignVal1 = params.alignVal1
for j, part in enumerate(path.parts): alignVal2 = params.alignVal2
part.toClose = allToClose[j] alignVal3 = params.alignVal3
curve = target.addCurve() createdObjsMap = main(removeOriginal, space, \
matchParts, [matchCri1, matchCri2, matchCri3], \
alignBy, [alignVal1, alignVal2, alignVal3])
curve.shape_key_add(name = 'Basis') return {'FINISHED'}
addShapeKey(curve, shapekeys, space)
if(removeOriginal): class MarkerController:
for path in userSel: drawHandlerRef = None
safeRemoveCurveObj(path.curve) defPointSize = 6
ptColor = (0, .8, .8, 1)
return {} def createSMMap(self, context):
objs = context.selected_objects
smMap = {}
for curve in objs:
if(not isBezier(curve)):
continue
def getSplineSegs(spline): smMap[curve.name] = {}
p = spline.bezier_points mw = curve.matrix_world
segs = [Segment(p[i-1].co, p[i-1].handle_right, p[i].handle_left, p[i].co) \ for splineIdx, spline in enumerate(curve.data.splines):
for i in range(1, len(p))] if(not spline.use_cyclic_u):
if(spline.use_cyclic_u): continue
segs.append(Segment(p[-1].co, p[-1].handle_right, p[0].handle_left, p[0].co))
return segs
def subdivideSeg(origSeg, noSegs): #initialize to the curr start vert co and idx
if(noSegs < 2): smMap[curve.name][splineIdx] = \
return [origSeg] [mw @ curve.data.splines[splineIdx].bezier_points[0].co, 0]
segs = [] for pt in spline.bezier_points:
oldT = 0 pt.select_control_point = False
segLen = origSeg.length / noSegs
for i in range(0, noSegs-1): if(len(smMap[curve.name]) == 0):
t = float(i+1) / noSegs del smMap[curve.name]
seg = origSeg.partialSeg(oldT, t)
segs.append(seg)
oldT = t
seg = origSeg.partialSeg(oldT, 1) return smMap
segs.append(seg)
return segs 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})
def getSubdivCntPerSeg(part, toAddCnt): if context.area:
context.area.tag_redraw()
class SegWrapper: def drawHandler(self):
def __init__(self, idx, seg): bgl.glPointSize(MarkerController.defPointSize)
self.idx = idx self.batch.draw(self.shader)
self.seg = seg
self.length = seg.length
class PartWrapper: def removeMarkers(self, context):
def __init__(self, part): if(MarkerController.drawHandlerRef != None):
self.segList = [] bpy.types.SpaceView3D.draw_handler_remove(MarkerController.drawHandlerRef, \
self.segCnt = len(part.getSegs()) "WINDOW")
for idx, seg in enumerate(part.getSegs()):
self.segList.append(SegWrapper(idx, seg))
partWrapper = PartWrapper(part) if(context.area and hasattr(context.space_data, 'region_3d')):
partLen = part.length context.area.tag_redraw()
avgLen = partLen / (partWrapper.segCnt + toAddCnt)
segsToDivide = [sr for sr in partWrapper.segList if sr.seg.length >= avgLen] MarkerController.drawHandlerRef = None
segToDivideCnt = len(segsToDivide)
avgLen = sum(sr.seg.length for sr in segsToDivide) / (segToDivideCnt + toAddCnt)
segsToDivide = sorted(segsToDivide, key=lambda x: x.length, reverse = True) self.deselectAll()
cnts = [0] * partWrapper.segCnt def __init__(self, context):
addedCnt = 0 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")
for i in range(0, segToDivideCnt): self.createBatch(context)
segLen = segsToDivide[i].seg.length
divideCnt = int(round(segLen/avgLen)) - 1 def saveStartVerts(self):
if(divideCnt == 0): for curveName in self.smMap.keys():
break curve = bpy.data.objects[curveName]
splines = curve.data.splines
spMap = self.smMap[curveName]
if((addedCnt + divideCnt) >= toAddCnt): for splineIdx in spMap.keys():
cnts[segsToDivide[i].idx] = toAddCnt - addedCnt markerInfo = spMap[splineIdx]
addedCnt = toAddCnt if(markerInfo[1] != 0):
break pts = splines[splineIdx].bezier_points
loc, idx = markerInfo[0], markerInfo[1]
cnt = len(pts)
cnts[segsToDivide[i].idx] = divideCnt ptCopy = [[p.co.copy(), p.handle_right.copy(), \
p.handle_left.copy(), p.handle_right_type, \
p.handle_left_type] for p in pts]
addedCnt += divideCnt for i, pt in enumerate(pts):
srcIdx = (idx + i) % cnt
p = ptCopy[srcIdx]
#TODO: Verify if needed #Must set the types first
while(toAddCnt > addedCnt): pt.handle_right_type = p[3]
for i in range(0, segToDivideCnt): pt.handle_left_type = p[4]
cnts[segsToDivide[i].idx] += 1 pt.co = p[0]
addedCnt += 1 pt.handle_right = p[1]
if(toAddCnt == addedCnt): pt.handle_left = p[2]
break
return cnts def updateSMMap(self):
for curveName in self.smMap.keys():
curve = bpy.data.objects[curveName]
spMap = self.smMap[curveName]
mw = curve.matrix_world
#Just distribute equally; this is likely a rare condition. So why complicate? for splineIdx in spMap.keys():
def distributeCnt(maxSegCntsByPart, startIdx, extraCnt): markerInfo = spMap[splineIdx]
added = 0 loc, idx = markerInfo[0], markerInfo[1]
elemCnt = len(maxSegCntsByPart) - startIdx pts = curve.data.splines[splineIdx].bezier_points
cntPerElem = floor(extraCnt / elemCnt)
remainder = extraCnt % elemCnt
for i in range(startIdx, len(maxSegCntsByPart)): selIdxs = [x for x in range(0, len(pts)) \
maxSegCntsByPart[i] += cntPerElem if pts[x].select_control_point == True]
if(i < remainder + startIdx):
maxSegCntsByPart[i] += 1
#Make all the paths to have the maximum number of segments in the set selIdx = selIdxs[0] if(len(selIdxs) > 0 ) else idx
#TODO: Refactor co = mw @ pts[selIdx].co
def addMissingSegs(selPaths, byPart): self.smMap[curveName][splineIdx] = [co, selIdx]
maxSegCntsByPart = []
maxSegCnt = 0
resSegCnt = [] def deselectAll(self):
sortedPaths = sorted(selPaths, key = lambda c: -len(c.parts)) 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
for i, path in enumerate(sortedPaths): def getSpaces3D(context):
if(byPart == False): areas3d = [area for area in context.window.screen.areas \
segCnt = path.getPartView().getSegCnt() if area.type == 'VIEW_3D']
if(segCnt > maxSegCnt):
maxSegCnt = segCnt
else:
resSegCnt.append([])
for j, part in enumerate(path.parts):
partSegCnt = part.getSegCnt()
resSegCnt[i].append(partSegCnt)
#First path return [s for a in areas3d for s in a.spaces if s.type == 'VIEW_3D']
if(j == len(maxSegCntsByPart)):
maxSegCntsByPart.append(partSegCnt)
#last part of this path, but other paths in set have more parts def hideHandles(context):
elif((j == len(path.parts) - 1) and states = []
len(maxSegCntsByPart) > len(path.parts)): spaces = MarkerController.getSpaces3D(context)
for s in spaces:
states.append(s.overlay.show_curve_handles)
s.overlay.show_curve_handles = False
return states
remainingSegs = sum(maxSegCntsByPart[j:]) def resetShowHandleState(context, handleStates):
if(partSegCnt <= remainingSegs): spaces = MarkerController.getSpaces3D(context)
resSegCnt[i][j] = remainingSegs for i, s in enumerate(spaces):
else: s.overlay.show_curve_handles = handleStates[i]
#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]): class ModalMarkSegStartOp(bpy.types.Operator):
maxSegCntsByPart[j] = partSegCnt bl_description = "Mark Vertex"
for i, path in enumerate(sortedPaths): bl_idname = "wm.mark_vertex"
bl_label = "Mark Start Vertex"
if(byPart == False): def cleanup(self, context):
partView = path.getPartView() wm = context.window_manager
segCnt = partView.getSegCnt() wm.event_timer_remove(self._timer)
diff = maxSegCnt - segCnt self.markerState.removeMarkers(context)
MarkerController.resetShowHandleState(context, self.handleStates)
context.window_manager.AssignShapeKeyParams.markVertex = False
if(diff > 0): def modal (self, context, event):
cnts = getSubdivCntPerSeg(partView, diff) params = context.window_manager.AssignShapeKeyParams
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) if(context.mode == 'OBJECT' or event.type == "ESC" or\
else: not context.window_manager.AssignShapeKeyParams.markVertex):
for j in range(0, len(path.parts)): self.cleanup(context)
part = path.parts[j] return {'CANCELLED'}
newSegs = []
partSegCnt = part.getSegCnt() elif(event.type == "RET"):
self.markerState.saveStartVerts()
self.cleanup(context)
return {'FINISHED'}
#TODO: Adding everything in the last part? if(event.type == 'TIMER'):
if(j == (len(path.parts)-1) and self.markerState.updateSMMap()
len(maxSegCntsByPart) > len(path.parts)): self.markerState.createBatch(context)
diff = resSegCnt[i][j] - partSegCnt
else:
diff = maxSegCntsByPart[j] - partSegCnt
if(diff > 0): elif(event.type in {'LEFT_CTRL', 'RIGHT_CTRL'}):
cnts = getSubdivCntPerSeg(part, diff) self.ctrl = (event.value == 'PRESS')
for k, seg in enumerate(part.getSegs()): elif(event.type in {'LEFT_SHIFT', 'RIGHT_SHIFT'}):
seg = part.getSeg(k) self.shift = (event.value == 'PRESS')
subdivCnt = cnts[k] + 1 #1 for the existing one
newSegs += subdivideSeg(seg, subdivCnt)
#isClosed won't be used, but let's update anyway if(event.type not in {"MIDDLEMOUSE", "TAB", "LEFTMOUSE", \
path.parts[j] = Part(path, newSegs, part.isClosed) "RIGHTMOUSE", 'WHEELDOWNMOUSE', 'WHEELUPMOUSE'} and \
not event.type.startswith("NUMPAD_")):
return {'RUNNING_MODAL'}
#TODO: Simplify (Not very readable) return {"PASS_THROUGH"}
def alignPath(path, matchParts, matchCriteria, alignBy, alignValues):
parts = path.parts[:] 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
if(matchParts == 'custom'): context.window_manager.modal_handler_add(self)
fnMap = {'vCnt' : lambda part: -1 * part.getSegCnt(), \ self.markerState = MarkerController(context)
'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)[' + \ #Hide so that users don't accidentally select handles instead of points
str(minmax) + '][' + str(axisIdx) + ']') self.handleStates = MarkerController.hideHandles(context)
matchPartCmpFns.append(fn) return {"RUNNING_MODAL"}
def comparer(left, right):
for fn in matchPartCmpFns:
a = fn(left)
b = fn(right)
if(floatCmpWithMargin(a, b)): class AssignShapeKeyParams(bpy.types.PropertyGroup):
continue
else:
return (a > b) - ( a < b) #No cmp in python3
return 0 removeOriginal : BoolProperty(name = "Remove Shape Key Objects", \
description = "Remove shape key objects after assigning to target", \
default = True)
parts = sorted(parts, key = cmp_to_key(comparer)) space : EnumProperty(name = "Space", \
items = [('worldspace', 'World Space', 'worldspace'),
('localspace', 'Local Space', 'localspace')], \
description = 'Space that shape keys are evluated in')
alignCmpFn = None alignList : EnumProperty(name="Vertex Alignment", items = \
if(alignBy == 'vertCo'): [("-None-", 'Manual Alignment', "Align curve segments based on starting vertex"), \
def evalCmp(criteria, pt1, pt2): ('vertCo', 'Vertex Coordinates', 'Align curve segments based on vertex coordinates')], \
if(len(criteria) == 0): description = 'Start aligning the vertices of target and shape keys from',
return True default = '-None-')
minmax = criteria[0][0] alignVal1 : EnumProperty(name="Value 1",
axisIdx = criteria[0][1] items = matchList, default = 'minX', description='First align criterion')
val1 = pt1[axisIdx]
val2 = pt2[axisIdx]
if(floatCmpWithMargin(val1, val2)): alignVal2 : EnumProperty(name="Value 2",
criteria = criteria[:] items = matchList, default = 'maxY', description='Second align criterion')
criteria.pop(0)
return evalCmp(criteria, pt1, pt2)
return val1 < val2 if minmax == 'min' else val1 > val2 alignVal3 : EnumProperty(name="Value 3",
items = matchList, default = 'minZ', description='Third align criterion')
alignCri = [[a[:3], ord(a[3:]) - ord('X')] for a in alignValues] matchParts : EnumProperty(name="Match Parts", items = \
alignCmpFn = lambda pt1, pt2, curve: (evalCmp(alignCri, \ [("-None-", 'None', "Don't match parts"), \
curve.matrix_world @ pt1, curve.matrix_world @ pt2)) ('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')
startPt = None matchCri1 : EnumProperty(name="Value 1",
startIdx = None items = matchList, default = 'minX', description='First match criterion')
for i in range(0, len(parts)): matchCri2 : EnumProperty(name="Value 2",
#Only truly closed parts items = matchList, default = 'maxY', description='Second match criterion')
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) + \ matchCri3 : EnumProperty(name="Value 3",
parts[i].getSegsCopy(None, startIdx), parts[i].isClosed) items = matchList, default = 'minZ', description='Third match criterion')
else:
path.parts[i] = parts[i]
#TODO: Other shape key attributes like interpolation...? markVertex : BoolProperty(name="Mark Starting Vertices", \
def getExistingShapeKeyPaths(path): description='Mark first vertices in all splines of selected curves', \
obj = path.curve default = False, update = markVertHandler)
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 addShapeKey(curve, paths, space): class AssignShapeKeysPanel(bpy.types.Panel):
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 bl_label = "Assign Shape Keys"
def safeRemoveCurveObj(obj): bl_idname = "CURVE_PT_assign_shape_keys"
try: bl_space_type = 'VIEW_3D'
collections = obj.users_collection bl_region_type = 'UI'
bl_category = "Tool"
for c in collections: @classmethod
c.objects.unlink(obj) def poll(cls, context):
return context.mode in {'OBJECT', 'EDIT_CURVE'}
if(obj.name in bpy.context.scene.collection.objects): def draw(self, context):
bpy.context.scene.collection.objects.unlink(obj)
if(obj.data.users == 1): layout = self.layout
if(obj.type == 'CURVE'): col = layout.column()
bpy.data.curves.remove(obj.data) #This also removes object? params = context.window_manager.AssignShapeKeyParams
elif(obj.type == 'MESH'):
bpy.data.meshes.remove(obj.data)
bpy.data.objects.remove(obj) if(context.mode == 'OBJECT'):
except: row = col.row()
pass 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()
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment