diff --git a/curve_assign_shapekey.py b/curve_assign_shapekey.py
index 287873a042a3e12789d28601c065d4f61ba35aa1..58335d0f5b8976218e31aff25e79c035364e3733 100644
--- a/curve_assign_shapekey.py
+++ b/curve_assign_shapekey.py
@@ -1,1104 +1,1110 @@
-#
-#
-# 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
-
-#################### UI and Registration Stuff ####################
-
-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",
-    "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 markVertHandler(self, context):
-    if(self.markVertex):
-        bpy.ops.wm.mark_vertex()
-    
-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)
-
-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"}
-
-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()
-
-#################### Addon code starts ####################
-
-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 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)
-        
-        collections = self.curve.users_collection
-        for collection in collections:
-            collection.objects.link(obj)
-
-        if(self.curve.name in bpy.context.scene.collection.objects and \
-            obj.name not in bpy.context.scene.collection.objects):
-            bpy.context.scene.collection.objects.link(obj)
-
-        return obj
-
-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]
-
-    curve = target.addCurve()
-
-    curve.shape_key_add(name = 'Basis')
-
-    addShapeKey(curve, shapekeys, space)
-
-    if(removeOriginal):
-        for path in userSel:
-            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 addShapeKey(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
+#
+#
+# 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",
+    "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 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)
+
+        collections = self.curve.users_collection
+        for collection in collections:
+            collection.objects.link(obj)
+
+        if(self.curve.name in bpy.context.scene.collection.objects and \
+            obj.name not in bpy.context.scene.collection.objects):
+            bpy.context.scene.collection.objects.link(obj)
+
+        return obj
+
+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]
+
+    curve = target.addCurve()
+
+    curve.shape_key_add(name = 'Basis')
+
+    addShapeKey(curve, shapekeys, space)
+
+    if(removeOriginal):
+        for path in userSel:
+            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 addShapeKey(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()