From 04f482c5aa48a1b04a429dafba3e09f7287b9110 Mon Sep 17 00:00:00 2001
From: "Spivak Vladimir (cwolf3d)" <cwolf3d@gmail.com>
Date: Fri, 11 Oct 2019 02:52:59 +0300
Subject: [PATCH] Addon: Curve Tools: Refactoring. Added context menu.

---
 curve_tools/Curves.py                         |  16 +-
 curve_tools/Operators.py                      | 124 +++++++++------
 curve_tools/Properties.py                     |  15 ++
 curve_tools/Surfaces.py                       |  24 +--
 curve_tools/__init__.py                       | 132 ++++++++--------
 curve_tools/auto_loft.py                      |  17 +--
 curve_tools/cad.py                            |  77 +++++++---
 curve_tools/curve_outline.py                  | 112 --------------
 curve_tools/curve_remove_doubles.py           | 110 --------------
 curve_tools/exports.py                        |  11 ++
 curve_tools/fillet.py                         | 143 ++++++++++++++++++
 curve_tools/internal.py                       |  19 +++
 ...CurveIntersections.py => intersections.py} |  54 +++----
 curve_tools/{Math.py => mathematics.py}       |   0
 curve_tools/outline.py                        | 118 +++++++++++++++
 curve_tools/{PathFinder.py => path_finder.py} |  28 ++--
 curve_tools/remove_doubles.py                 | 135 +++++++++++++++++
 ...wCurveResolution.py => show_resolution.py} |  19 ++-
 ...SplinesSequence.py => splines_sequence.py} |  17 ++-
 curve_tools/toolpath.py                       |  23 ++-
 20 files changed, 760 insertions(+), 434 deletions(-)
 delete mode 100644 curve_tools/curve_outline.py
 delete mode 100644 curve_tools/curve_remove_doubles.py
 create mode 100644 curve_tools/fillet.py
 rename curve_tools/{CurveIntersections.py => intersections.py} (94%)
 rename curve_tools/{Math.py => mathematics.py} (100%)
 create mode 100644 curve_tools/outline.py
 rename curve_tools/{PathFinder.py => path_finder.py} (95%)
 create mode 100644 curve_tools/remove_doubles.py
 rename curve_tools/{ShowCurveResolution.py => show_resolution.py} (92%)
 rename curve_tools/{SplinesSequence.py => splines_sequence.py} (96%)

diff --git a/curve_tools/Curves.py b/curve_tools/Curves.py
index d5608e3c3..da0b13984 100644
--- a/curve_tools/Curves.py
+++ b/curve_tools/Curves.py
@@ -1,4 +1,4 @@
-from . import Math
+from . import mathematics
 
 import bpy
 
@@ -390,7 +390,7 @@ class BezierSpline:
         self.segments.append(BezierSegment(self.segments[-1].bezierPoint2, spline2.segments[0].bezierPoint1))
         for seg2 in spline2.segments: self.segments.append(seg2)
 
-        self.resolution += spline2.resolution    # extra segment will usually be short -- impact on resolution negligible
+        self.resolution += spline2.resolution    # extra segment will usually be short -- impact on resolution negligable
 
         self.isCyclic = False    # is this ok?
 
@@ -559,11 +559,11 @@ class Curve:
 
                     currEndPoint = currentSpline.segments[-1].bezierPoint2.co
                     nextStartPoint = nextSpline.segments[0].bezierPoint1.co
-                    if Math.IsSamePoint(currEndPoint, nextStartPoint, threshold): return [currentSpline, nextSpline]
+                    if mathematics.IsSamePoint(currEndPoint, nextStartPoint, threshold): return [currentSpline, nextSpline]
 
                     nextEndPoint = nextSpline.segments[-1].bezierPoint2.co
                     currStartPoint = currentSpline.segments[0].bezierPoint1.co
-                    if Math.IsSamePoint(nextEndPoint, currStartPoint, threshold): return [nextSpline, currentSpline]
+                    if mathematics.IsSamePoint(nextEndPoint, currStartPoint, threshold): return [nextSpline, currentSpline]
 
             return None
         else:
@@ -575,18 +575,18 @@ class Curve:
 
                     currEndPoint = currentSpline.segments[-1].bezierPoint2.co
                     nextStartPoint = nextSpline.segments[0].bezierPoint1.co
-                    if Math.IsSamePoint(currEndPoint, nextStartPoint, threshold): return [currentSpline, nextSpline]
+                    if mathematics.IsSamePoint(currEndPoint, nextStartPoint, threshold): return [currentSpline, nextSpline]
 
                     nextEndPoint = nextSpline.segments[-1].bezierPoint2.co
                     currStartPoint = currentSpline.segments[0].bezierPoint1.co
-                    if Math.IsSamePoint(nextEndPoint, currStartPoint, threshold): return [nextSpline, currentSpline]
+                    if mathematics.IsSamePoint(nextEndPoint, currStartPoint, threshold): return [nextSpline, currentSpline]
 
-                    if Math.IsSamePoint(currEndPoint, nextEndPoint, threshold):
+                    if mathematics.IsSamePoint(currEndPoint, nextEndPoint, threshold):
                         nextSpline.Reverse()
                         #print("## ", "nextSpline.Reverse()")
                         return [currentSpline, nextSpline]
 
-                    if Math.IsSamePoint(currStartPoint, nextStartPoint, threshold):
+                    if mathematics.IsSamePoint(currStartPoint, nextStartPoint, threshold):
                         currentSpline.Reverse()
                         #print("## ", "currentSpline.Reverse()")
                         return [currentSpline, nextSpline]
diff --git a/curve_tools/Operators.py b/curve_tools/Operators.py
index edf16ab13..e4fe24dd6 100644
--- a/curve_tools/Operators.py
+++ b/curve_tools/Operators.py
@@ -7,12 +7,12 @@ from bpy_extras import object_utils, view3d_utils
 from mathutils import  *
 from math import  *
 
-from . import Properties
-from . import Curves
-from . import CurveIntersections
-from . import Util
-from . import Surfaces
-from . import Math
+from . import properties
+from . import curves
+from . import intersections
+from . import util
+from . import surfaces
+from . import mathematics
 
 # 1 CURVE SELECTED
 # ################
@@ -24,11 +24,11 @@ class OperatorCurveInfo(bpy.types.Operator):
 
     @classmethod
     def poll(cls, context):
-        return Util.Selected1Curve()
+        return util.Selected1Curve()
 
 
     def execute(self, context):
-        curve = Curves.Curve(context.active_object)
+        curve = curves.Curve(context.active_object)
 
         nrSplines = len(curve.splines)
         nrSegments = 0
@@ -52,11 +52,11 @@ class OperatorCurveLength(bpy.types.Operator):
 
     @classmethod
     def poll(cls, context):
-        return Util.Selected1Curve()
+        return util.Selected1Curve()
 
 
     def execute(self, context):
-        curve = Curves.Curve(context.active_object)
+        curve = curves.Curve(context.active_object)
 
         context.scene.curvetools.CurveLength = curve.length
 
@@ -72,11 +72,11 @@ class OperatorSplinesInfo(bpy.types.Operator):
 
     @classmethod
     def poll(cls, context):
-        return Util.Selected1Curve()
+        return util.Selected1Curve()
 
 
     def execute(self, context):
-        curve = Curves.Curve(context.active_object)
+        curve = curves.Curve(context.active_object)
         nrSplines = len(curve.splines)
 
         print("")
@@ -105,11 +105,11 @@ class OperatorSegmentsInfo(bpy.types.Operator):
 
     @classmethod
     def poll(cls, context):
-        return Util.Selected1Curve()
+        return util.Selected1Curve()
 
 
     def execute(self, context):
-        curve = Curves.Curve(context.active_object)
+        curve = curves.Curve(context.active_object)
         nrSplines = len(curve.splines)
         nrSegments = 0
 
@@ -146,7 +146,7 @@ class OperatorOriginToSpline0Start(bpy.types.Operator):
 
     @classmethod
     def poll(cls, context):
-        return Util.Selected1Curve()
+        return util.Selected1Curve()
 
 
     def execute(self, context):
@@ -183,11 +183,11 @@ class OperatorIntersectCurves(bpy.types.Operator):
 
     @classmethod
     def poll(cls, context):
-        return Util.Selected2OrMoreCurves()
+        return util.Selected2OrMoreCurves()
 
 
     def execute(self, context):
-        print("### TODO: OperatorIntersectCurves.execute()")
+        print("### TODO: OperatorIntersectcurves.execute()")
 
         algo = context.scene.curvetools.IntersectCurvesAlgorithm
         print("-- algo:", algo)
@@ -213,7 +213,7 @@ class OperatorIntersectCurves(bpy.types.Operator):
                     selected_objects[j].select_set(True)
         
                     if selected_objects[i].type == 'CURVE' and selected_objects[j].type == 'CURVE':
-                        curveIntersector = CurveIntersections.CurvesIntersector.FromSelection()
+                        curveIntersector = intersections.CurvesIntersector.FromSelection()
                         rvIntersectionNrs = curveIntersector.CalcAndApplyIntersections()
 
                         self.report({'INFO'}, "Active curve points: %d; other curve points: %d" % (rvIntersectionNrs[0], rvIntersectionNrs[1]))
@@ -234,16 +234,16 @@ class OperatorLoftCurves(bpy.types.Operator):
 
     @classmethod
     def poll(cls, context):
-        return Util.Selected2Curves()
+        return util.Selected2Curves()
 
 
     def execute(self, context):
-        #print("### TODO: OperatorLoftCurves.execute()")
+        #print("### TODO: OperatorLoftcurves.execute()")
 
-        loftedSurface = Surfaces.LoftedSurface.FromSelection()
+        loftedSurface = surfaces.LoftedSurface.FromSelection()
         loftedSurface.AddToScene()
 
-        self.report({'INFO'}, "OperatorLoftCurves.execute()")
+        self.report({'INFO'}, "OperatorLoftcurves.execute()")
 
         return {'FINISHED'}
 
@@ -259,16 +259,16 @@ class OperatorSweepCurves(bpy.types.Operator):
 
     @classmethod
     def poll(cls, context):
-        return Util.Selected2Curves()
+        return util.Selected2Curves()
 
 
     def execute(self, context):
-        #print("### TODO: OperatorSweepCurves.execute()")
+        #print("### TODO: OperatorSweepcurves.execute()")
 
-        sweptSurface = Surfaces.SweptSurface.FromSelection()
+        sweptSurface = surfaces.SweptSurface.FromSelection()
         sweptSurface.AddToScene()
 
-        self.report({'INFO'}, "OperatorSweepCurves.execute()")
+        self.report({'INFO'}, "OperatorSweepcurves.execute()")
 
         return {'FINISHED'}
 
@@ -284,11 +284,11 @@ class OperatorBirail(bpy.types.Operator):
 
     @classmethod
     def poll(cls, context):
-        return Util.Selected3Curves()
+        return util.Selected3Curves()
 
 
     def execute(self, context):
-        birailedSurface = Surfaces.BirailedSurface.FromSelection()
+        birailedSurface = surfaces.BirailedSurface.FromSelection()
         birailedSurface.AddToScene()
 
         self.report({'INFO'}, "OperatorBirail.execute()")
@@ -307,12 +307,12 @@ class OperatorSplinesSetResolution(bpy.types.Operator):
 
     @classmethod
     def poll(cls, context):
-        return Util.Selected1OrMoreCurves()
+        return util.Selected1OrMoreCurves()
 
 
     def execute(self, context):
         splRes = context.scene.curvetools.SplineResolution
-        selCurves = Util.GetSelectedCurves()
+        selCurves = util.GetSelectedCurves()
 
         for blCurve in selCurves:
             for spline in blCurve.data.splines:
@@ -331,14 +331,14 @@ class OperatorSplinesRemoveZeroSegment(bpy.types.Operator):
 
     @classmethod
     def poll(cls, context):
-        return Util.Selected1OrMoreCurves()
+        return util.Selected1OrMoreCurves()
 
 
     def execute(self, context):
-        selCurves = Util.GetSelectedCurves()
+        selCurves = util.GetSelectedCurves()
 
         for blCurve in selCurves:
-            curve = Curves.Curve(blCurve)
+            curve = curves.Curve(blCurve)
             nrSplines = curve.nrSplines
 
             splinesToRemove = []
@@ -365,15 +365,15 @@ class OperatorSplinesRemoveShort(bpy.types.Operator):
 
     @classmethod
     def poll(cls, context):
-        return Util.Selected1OrMoreCurves()
+        return util.Selected1OrMoreCurves()
 
 
     def execute(self, context):
         threshold = context.scene.curvetools.SplineRemoveLength
-        selCurves = Util.GetSelectedCurves()
+        selCurves = util.GetSelectedCurves()
 
         for blCurve in selCurves:
-            curve = Curves.Curve(blCurve)
+            curve = curves.Curve(blCurve)
             nrSplines = curve.nrSplines
 
             nrRemovedSplines = curve.RemoveShortSplines(threshold)
@@ -394,14 +394,14 @@ class OperatorSplinesJoinNeighbouring(bpy.types.Operator):
 
     @classmethod
     def poll(cls, context):
-        return Util.Selected1OrMoreCurves()
+        return util.Selected1OrMoreCurves()
 
 
     def execute(self, context):
-        selCurves = Util.GetSelectedCurves()
+        selCurves = util.GetSelectedCurves()
 
         for blCurve in selCurves:
-            curve = Curves.Curve(blCurve)
+            curve = curves.Curve(blCurve)
             nrSplines = curve.nrSplines
 
             threshold = context.scene.curvetools.SplineJoinDistance
@@ -423,7 +423,7 @@ def SurfaceFromBezier(surfacedata, points, center):
     len_points = len(points) - 1
     
     if len_points % 2 == 0:
-        h = Math.subdivide_cubic_bezier(
+        h = mathematics.subdivide_cubic_bezier(
                         points[len_points].co, points[len_points].handle_right,
                         points[0].handle_left, points[0].co, 0.5
                         )
@@ -550,7 +550,7 @@ class ConvertSelectedFacesToBezier(bpy.types.Operator):
 
     @classmethod
     def poll(cls, context):
-        return Util.Selected1Mesh()
+        return util.Selected1Mesh()
 
     def execute(self, context):
         # main function
@@ -620,7 +620,7 @@ class ConvertBezierToSurface(bpy.types.Operator):
     
     @classmethod
     def poll(cls, context):
-        return Util.Selected1OrMoreCurves()
+        return util.Selected1OrMoreCurves()
 
     def execute(self, context):
         # main function
@@ -686,7 +686,7 @@ class BezierPointsFillet(bpy.types.Operator):
 
     @classmethod
     def poll(cls, context):
-        return Util.Selected1OrMoreCurves()
+        return util.Selected1OrMoreCurves()
 
     def execute(self, context):
         # main function
@@ -798,7 +798,7 @@ class BezierDivide(bpy.types.Operator):
 
     @classmethod
     def poll(cls, context):
-        return Util.Selected1OrMoreCurves()
+        return util.Selected1OrMoreCurves()
 
     def execute(self, context):
         # main function
@@ -834,7 +834,7 @@ class BezierDivide(bpy.types.Operator):
                     if (j in ii) and (j + 1 in ii):
                         bezier_points[j + jn].select_control_point = True
                         bezier_points[j + 1 + jn].select_control_point = True
-                        h = Math.subdivide_cubic_bezier(
+                        h = mathematics.subdivide_cubic_bezier(
                             bezier_points[j + jn].co, bezier_points[j + jn].handle_right,
                             bezier_points[j + 1 + jn].handle_left, bezier_points[j + 1 + jn].co, self.Bezier_t / 100
                             )
@@ -853,7 +853,7 @@ class BezierDivide(bpy.types.Operator):
                     if j == n - 1 and (0 in ii) and spline.use_cyclic_u:
                         bezier_points[j + jn].select_control_point = True
                         bezier_points[0].select_control_point = True
-                        h = Math.subdivide_cubic_bezier(
+                        h = mathematics.subdivide_cubic_bezier(
                             bezier_points[j + jn].co, bezier_points[j + jn].handle_right,
                             bezier_points[0].handle_left, bezier_points[0].co, self.Bezier_t / 100
                             )
@@ -922,10 +922,10 @@ class Split(bpy.types.Operator):
 
     @classmethod
     def poll(cls, context):
-        return Util.Selected1OrMoreCurves()
+        return util.Selected1OrMoreCurves()
 
     def execute(self, context):
-        selected_Curves = Util.GetSelectedCurves()
+        selected_Curves = util.GetSelectedCurves()
         
         for curve in selected_Curves:
             spline_points = []
@@ -1004,6 +1004,33 @@ class Split(bpy.types.Operator):
                         num=i
    
         return {'FINISHED'}
+        
+class SeparateOutline(bpy.types.Operator):
+    bl_idname = "curvetools.sep_outline"
+    bl_label = "Separate Outline"
+    bl_options = {'REGISTER', 'UNDO'}
+    bl_description = "Makes 'Outline' separate mesh"
+
+    @classmethod
+    def poll(cls, context):
+        return util.Selected1OrMoreCurves()
+
+    def execute(self, context):
+        bpy.ops.object.mode_set(mode = 'EDIT')
+        bpy.ops.curve.separate()
+
+        return {'FINISHED'}
+
+def register():
+    for cls in classes:
+        bpy.utils.register_class(operators)
+
+def unregister():
+    for cls in classes:
+        bpy.utils.unregister_class(operators)
+
+if __name__ == "__main__":
+    register()
 
 operators = [
     OperatorCurveInfo,
@@ -1025,4 +1052,5 @@ operators = [
     BezierDivide,
     CurveScaleReset,
     Split,
+    SeparateOutline,
     ]
diff --git a/curve_tools/Properties.py b/curve_tools/Properties.py
index 415b3c8b2..d6fe9e0f6 100644
--- a/curve_tools/Properties.py
+++ b/curve_tools/Properties.py
@@ -87,3 +87,18 @@ class curvetoolsSelectedObject(bpy.types.PropertyGroup):
         for blObject in blenderSelectedObjects: rvNames.append(blObject.name)
 
         return rvNames
+
+def register():
+    for cls in classes:
+        bpy.utils.register_class(operators)
+
+def unregister():
+    for cls in classes:
+        bpy.utils.unregister_class(operators)
+
+if __name__ == "__main__":
+    register()
+
+operators = [
+    curvetoolsSelectedObject,
+    ]
diff --git a/curve_tools/Surfaces.py b/curve_tools/Surfaces.py
index 22b5119cf..cfae70402 100644
--- a/curve_tools/Surfaces.py
+++ b/curve_tools/Surfaces.py
@@ -1,8 +1,8 @@
 import bpy
 import bmesh
 
-from . import Math
-from . import Curves
+from . import mathematics
+from . import curves
 
 
 
@@ -62,8 +62,8 @@ class LoftedSurface:
         blenderOtherCurve = selObjects[0]
         if blenderActiveCurve == blenderOtherCurve: blenderOtherCurve = selObjects[1]
 
-        aCurve = Curves.Curve(blenderActiveCurve)
-        oCurve = Curves.Curve(blenderOtherCurve)
+        aCurve = curves.Curve(blenderActiveCurve)
+        oCurve = curves.Curve(blenderOtherCurve)
 
         name = "TODO: autoname"
 
@@ -162,7 +162,7 @@ class SweptSplineSurface:
         prevDerivativeO = localDerivativesO[0]
         for iO in range(self.resolutionO):
             currDerivativeO = localDerivativesO[iO]
-            localRotMatO = Math.CalcRotationMatrix(prevDerivativeO, currDerivativeO)
+            localRotMatO = mathematics.CalcRotationMatrix(prevDerivativeO, currDerivativeO)
 
             currLocalAToLocalO = worldMatrixOInv @ currWorldMatrixA
             worldPointsA = []
@@ -210,8 +210,8 @@ class SweptSurface:
         blenderOtherCurve = selObjects[0]
         if blenderActiveCurve == blenderOtherCurve: blenderOtherCurve = selObjects[1]
 
-        aCurve = Curves.Curve(blenderActiveCurve)
-        oCurve = Curves.Curve(blenderOtherCurve)
+        aCurve = curves.Curve(blenderActiveCurve)
+        oCurve = curves.Curve(blenderOtherCurve)
 
         name = "TODO: autoname"
 
@@ -315,7 +315,7 @@ class BirailedSplineSurface:
         prevDerivativeRail1 = localDerivativesRail1[0]
         for iRail in range(self.resolutionRails):
             currDerivativeRail1 = localDerivativesRail1[iRail]
-            localRotMatRail1 = Math.CalcRotationMatrix(prevDerivativeRail1, currDerivativeRail1)
+            localRotMatRail1 = mathematics.CalcRotationMatrix(prevDerivativeRail1, currDerivativeRail1)
 
             currLocalProfileToLocalRail1 = worldMatrixRail1Inv @ currWorldMatrixProfile
             worldPointsProfileRail1 = []
@@ -336,7 +336,7 @@ class BirailedSplineSurface:
                 scaleFactorRail2 = v3To.magnitude / v3From.magnitude
             else:
                 scaleFactorRail2 = 1
-            rotMatRail2 = Math.CalcRotationMatrix(v3From, v3To)
+            rotMatRail2 = mathematics.CalcRotationMatrix(v3From, v3To)
 
             worldOffsetsProfileRail2 = []
             for iProfile in range(self.resolutionProfile):
@@ -397,9 +397,9 @@ class BirailedSurface:
         if profileBlenderCurve is None: raise Exception("profileBlenderCurve is None")
 
 
-        rail1Curve = Curves.Curve(rail1BlenderCurve)
-        rail2Curve = Curves.Curve(rail2BlenderCurve)
-        profileCurve = Curves.Curve(profileBlenderCurve)
+        rail1Curve = curves.Curve(rail1BlenderCurve)
+        rail2Curve = curves.Curve(rail2BlenderCurve)
+        profileCurve = curves.Curve(profileBlenderCurve)
 
         name = "TODO: autoname"
 
diff --git a/curve_tools/__init__.py b/curve_tools/__init__.py
index e38f854e9..736e60fb3 100644
--- a/curve_tools/__init__.py
+++ b/curve_tools/__init__.py
@@ -25,13 +25,12 @@ bl_info = {
     "name": "Curve Tools",
     "description": "Adds some functionality for bezier/nurbs curve/surface modeling",
     "author": "Mackraken",
-    "version": (0, 3, 3),
+    "version": (0, 4, 0),
     "blender": (2, 80, 0),
     "location": "View3D > Tool Shelf > Edit Tab",
     "warning": "WIP",
-    "wiki_url": "https://wiki.blender.org/index.php/Extensions:2.6/Py/"
-                "Scripts/Curve/Curve_Tools",
-    "tracker_url": "https://developer.blender.org/maniphest/task/edit/form/2/",
+    "wiki_url": "",
+    "tracker_url": "",
     "category": "Add Curve"}
 
 
@@ -50,22 +49,25 @@ from bpy.props import (
         StringProperty,
         FloatVectorProperty,
         )
-from . import Properties
-from . import Operators
-from . import auto_loft
-from . import curve_outline
-from . import PathFinder
-from . import ShowCurveResolution
-from . import SplinesSequence
+from . import properties, operators, auto_loft, outline, remove_doubles
+from . import path_finder, show_resolution, splines_sequence, fillet
 from . import internal, cad, toolpath, exports
 
-if 'internal' in locals():
+if 'bpy' in locals():
+    importlib.reload(properties)
+    importlib.reload(operators)
+    importlib.reload(auto_loft)
+    importlib.reload(outline)
+    importlib.reload(remove_doubles)
+    importlib.reload(path_finder)
+    importlib.reload(show_resolution)
+    importlib.reload(splines_sequence)
+    importlib.reload(fillet)
     importlib.reload(internal)
     importlib.reload(cad)
     importlib.reload(toolpath)
     importlib.reload(exports)
 
-
 from bpy.types import (
         AddonPreferences,
         )
@@ -81,28 +83,10 @@ def UpdateDummy(object, context):
     UTILSDROP = scene.UTUtilsDrop
 
 
-class SeparateOutline(Operator):
-    bl_idname = "object.sep_outline"
-    bl_label = "Separate Outline"
-    bl_options = {'REGISTER', 'UNDO'}
-    bl_description = "Makes 'Outline' separate mesh"
-
-    @classmethod
-    def poll(cls, context):
-        return (context.object is not None and
-                context.object.type == 'CURVE')
-
-    def execute(self, context):
-        bpy.ops.object.mode_set(mode = 'EDIT')
-        bpy.ops.curve.separate()
-
-        return {'FINISHED'}
-
-
 class curvetoolsSettings(PropertyGroup):
     # selection
     SelectedObjects: CollectionProperty(
-            type=Properties.curvetoolsSelectedObject
+            type=properties.curvetoolsSelectedObject
             )
     NrSelectedObjects: IntProperty(
             name="NrSelectedObjects",
@@ -308,7 +292,6 @@ class VIEW3D_PT_CurvePanel(Panel):
 
             row = col.row(align=True)
             row.prop(context.scene.curvetools, "LimitDistance", text="LimitDistance")
-            # row.active = (context.scene.curvetools.IntersectCurvesAlgorithm == '3D')
 
             row = col.row(align=True)
             row.prop(context.scene.curvetools, "IntersectCurvesAlgorithm", text="Algorithm")
@@ -346,12 +329,14 @@ class VIEW3D_PT_CurvePanel(Panel):
         if ADVANCEDDROP:
             # C. 3 curves
             row = col.row(align=True)
-            row.operator("object._curve_outline", text="Curve Outline")
+            row.operator("curvetools.outline", text="Curve Outline")
             row = col.row(align=True)
-            row.operator("object.sep_outline", text="Separate Outline or selected")
+            row.operator("curvetools.sep_outline", text="Separate Outline or selected")
             row = col.row(align=True)
             row.operator("curvetools.bezier_points_fillet", text='Fillet')
             row = col.row(align=True)
+            row.operator("curvetools.bezier_cad_handle_projection", text='Handle Projection')
+            row = col.row(align=True)
             row.operator("curvetools.bezier_spline_divide", text='Divide')
             row = col.row(align=True)
             row.operator("curvetools.scale_reset", text='Scale Reset')
@@ -369,17 +354,19 @@ class VIEW3D_PT_CurvePanel(Panel):
         row.prop(scene, "UTExtendedDrop", icon="TRIA_DOWN")
         if EXTENDEDDROP:
             row = col.row(align=True)
-            row.operator("curve.add_toolpath_offset_curve", text="Offset Curve")
+            row.operator("curvetools.add_toolpath_offset_curve", text="Offset Curve")
             row = col.row(align=True)
-            row.operator("curve.bezier_cad_boolean", text="Boolean 2 selected spline")
+            row.operator("curvetools.bezier_cad_boolean", text="Boolean 2 selected spline")
             row = col.row(align=True)
-            row.operator("curve.bezier_cad_subdivide", text="Multi Subdivide")
+            row.operator("curvetools.bezier_cad_subdivide", text="Multi Subdivide")
             row = col.row(align=True)
             row.operator("curvetools.split", text='Split by selected points')            
             row = col.row(align=True)
-            row.operator("curve.add_toolpath_discretize_curve", text="Discretize Curve")
+            row.operator("curvetools.remove_doubles", text='Remove Doubles')            
+            row = col.row(align=True)
+            row.operator("curvetools.add_toolpath_discretize_curve", text="Discretize Curve")
             row = col.row(align=True)
-            row.operator("curve.bezier_cad_array", text="Array selected spline")
+            row.operator("curvetools.bezier_cad_array", text="Array selected spline")
 
         # Utils Curve options
         box1 = self.layout.box()
@@ -396,7 +383,7 @@ class VIEW3D_PT_CurvePanel(Panel):
             row = col.row(align=True)
             row.prop(context.scene.curvetools, "curve_vertcolor", text="")
             row = col.row(align=True)
-            row.operator("curve.show_resolution", text="Run [ESC]")
+            row.operator("curvetools.show_resolution", text="Run [ESC]")
             
             # D.1 set spline sequence
             row = col.row(align=True)
@@ -406,12 +393,12 @@ class VIEW3D_PT_CurvePanel(Panel):
             row.prop(context.scene.curvetools, "font_thickness", text="")
             row.prop(context.scene.curvetools, "font_size", text="")
             row = col.row(align=True)
-            oper = row.operator("curve.rearrange_spline", text="<")
+            oper = row.operator("curvetools.rearrange_spline", text="<")
             oper.command = 'PREV'
-            oper = row.operator("curve.rearrange_spline", text=">")
+            oper = row.operator("curvetools.rearrange_spline", text=">")
             oper.command = 'NEXT'
             row = col.row(align=True)
-            row.operator("curve.show_splines_sequence", text="Run [ESC]")
+            row.operator("curvetools.show_splines_sequence", text="Run [ESC]")
 
             # D.2 remove splines
             row = col.row(align=True)
@@ -455,7 +442,6 @@ class VIEW3D_PT_CurvePanel(Panel):
             row = col.row(align=True)
             row.label(text="A - deselect all")
             
-
 # Add-ons Preferences Update Panel
 
 # Define Panel classes for updating
@@ -500,6 +486,27 @@ class CurveAddonPreferences(AddonPreferences):
         col.label(text="Tab Category:")
         col.prop(self, "category", text="")
 
+# Context MENU
+def curve_tools_context_menu(self, context):
+    bl_label = 'Curve tools'
+   
+    self.layout.operator("curvetools.bezier_points_fillet", text="Fillet")
+    self.layout.operator("curvetools.bezier_cad_handle_projection", text='Handle Projection')
+    self.layout.operator("curvetools.bezier_spline_divide", text="Divide")
+    self.layout.operator("curvetools.add_toolpath_offset_curve", text="Offset Curve")
+    self.layout.operator("curvetools.remove_doubles", text='Remove Doubles')
+    self.layout.separator()
+    
+def curve_tools_object_context_menu(self, context):
+    bl_label = 'Curve tools'
+   
+    if context.active_object.type == "CURVE":
+        self.layout.operator("curvetools.scale_reset", text="Scale Reset")
+        self.layout.operator("curvetools.add_toolpath_offset_curve", text="Offset Curve")
+        self.layout.operator("curvetools.remove_doubles", text='Remove Doubles')
+        self.layout.separator()
+
+# Import-export 2d svg
 def menu_file_export(self, context):
     for operator in exports.operators:
         self.layout.operator(operator.bl_idname)
@@ -509,16 +516,21 @@ def menu_file_import(self, context):
         self.layout.operator(operator.bl_idname)
 
 # REGISTER
-classes = cad.operators + toolpath.operators + exports.operators + Operators.operators + [
-    Properties.curvetoolsSelectedObject,
-    CurveAddonPreferences,
-    curvetoolsSettings,
-    SeparateOutline,
-    PathFinder.PathFinder,
-    ShowCurveResolution.ShowCurveResolution,
-    SplinesSequence.ShowSplinesSequence,
-    SplinesSequence.RearrangeSpline,
-    ]
+classes = cad.operators + \
+        toolpath.operators + \
+        exports.operators + \
+        operators.operators + \
+        properties.operators + \
+        path_finder.operators + \
+        show_resolution.operators + \
+        splines_sequence.operators + \
+        outline.operators + \
+        fillet.operators + \
+        remove_doubles.operators + \
+        [
+            CurveAddonPreferences,
+            curvetoolsSettings,
+        ]
 
 def register():
     bpy.types.Scene.UTSingleDrop = BoolProperty(
@@ -560,13 +572,14 @@ def register():
     
     auto_loft.register()
     
-    curve_outline.register()
-    
     bpy.types.TOPBAR_MT_file_export.append(menu_file_export)
     
     bpy.types.Scene.curvetools = bpy.props.PointerProperty(type=curvetoolsSettings)
     
     update_panel(None, bpy.context)
+    
+    bpy.types.VIEW3D_MT_edit_curve_context_menu.prepend(curve_tools_context_menu)
+    bpy.types.VIEW3D_MT_object_context_menu.prepend(curve_tools_object_context_menu)
 
 
 def unregister():
@@ -579,10 +592,11 @@ def unregister():
     
     auto_loft.unregister()
     
-    curve_outline.unregister()
-    
     bpy.types.TOPBAR_MT_file_export.remove(menu_file_export)
     
+    bpy.types.VIEW3D_MT_edit_curve_context_menu.remove(curve_tools_context_menu)
+    bpy.types.VIEW3D_MT_object_context_menu.remove(curve_tools_object_context_menu)
+    
     for panel in panels:
         bpy.utils.unregister_class(panel)
     
diff --git a/curve_tools/auto_loft.py b/curve_tools/auto_loft.py
index 6675aaf1d..c0711196a 100644
--- a/curve_tools/auto_loft.py
+++ b/curve_tools/auto_loft.py
@@ -1,9 +1,9 @@
 import bpy
 from bpy.props import BoolProperty
 from bpy.types import Operator, Panel
-from curve_tools.Surfaces import LoftedSurface
-from curve_tools.Curves import Curve
-from curve_tools import Util
+from . import surfaces
+from . import curves
+from . import util
 
 
 class OperatorAutoLoftCurves(Operator):
@@ -13,16 +13,16 @@ class OperatorAutoLoftCurves(Operator):
 
     @classmethod
     def poll(cls, context):
-        return Util.Selected2Curves()
+        return util.Selected2Curves()
 
     def execute(self, context):
-        #print("### TODO: OperatorLoftCurves.execute()")
+        #print("### TODO: OperatorLoftcurves.execute()")
         mesh = bpy.data.meshes.new("LoftMesh")
 
         curve0 = context.selected_objects[0]
         curve1 = context.selected_objects[1]
 
-        ls = LoftedSurface(Curve(curve0), Curve(curve1), "AutoLoft")
+        ls = surfaces.LoftedSurface(curves.Curve(curve0), curves.Curve(curve1), "AutoLoft")
 
         ls.bMesh.to_mesh(mesh)
 
@@ -37,8 +37,6 @@ class OperatorAutoLoftCurves(Operator):
                            "description": "Auto loft from %s to %s" % (curve0.name, curve1.name),
                            "curve0": curve0.name,
                            "curve1": curve1.name}
-        #print(loftobj['_RNA_UI'].to_dict())
-        #self.report({'INFO'}, "OperatorAutoLoftCurves.execute()")
 
         return {'FINISHED'}
 
@@ -61,12 +59,11 @@ class AutoLoftModalOperator(Operator):
         #print("TIMER", lofters)
 
         for loftmesh in lofters:
-            #loftmesh.hide_select = True
             rna = loftmesh['_RNA_UI']["autoloft"].to_dict()
             curve0 = scene.objects.get(rna["curve0"])
             curve1 = scene.objects.get(rna["curve1"])
             if curve0 and curve1:
-                ls = LoftedSurface(Curve(curve0), Curve(curve1), loftmesh.name)
+                ls = surfaces.LoftedSurface(curves.Curve(curve0), curves.Curve(curve1), loftmesh.name)
                 ls.bMesh.to_mesh(loftmesh.data)
         return {'FINISHED'}
 
diff --git a/curve_tools/cad.py b/curve_tools/cad.py
index 288f91497..49ccf171b 100644
--- a/curve_tools/cad.py
+++ b/curve_tools/cad.py
@@ -28,20 +28,20 @@ bl_info = {
 
 import bpy
 from . import internal
-from . import Util
+from . import util
 
 class Fillet(bpy.types.Operator):
-    bl_idname = 'curve.bezier_cad_fillet'
+    bl_idname = 'curvetools.bezier_cad_fillet'
     bl_description = bl_label = 'Fillet'
     bl_options = {'REGISTER', 'UNDO'}
 
     radius: bpy.props.FloatProperty(name='Radius', description='Radius of the rounded corners', unit='LENGTH', min=0.0, default=0.1)
     chamfer_mode: bpy.props.BoolProperty(name='Chamfer', description='Cut off sharp without rounding', default=False)
-    limit_half_way: bpy.props.BoolProperty(name='Limit Half Way', description='Limits the segments to half their length in order to prevent collisions', default=False)
+    limit_half_way: bpy.props.BoolProperty(name='Limit Half Way', description='Limits the segements to half their length in order to prevent collisions', default=False)
 
     @classmethod
     def poll(cls, context):
-        return Util.Selected1OrMoreCurves()
+        return util.Selected1OrMoreCurves()
 
     def execute(self, context):
         splines = internal.getSelectedSplines(True, True, True)
@@ -54,7 +54,7 @@ class Fillet(bpy.types.Operator):
         return {'FINISHED'}
 
 class Boolean(bpy.types.Operator):
-    bl_idname = 'curve.bezier_cad_boolean'
+    bl_idname = 'curvetools.bezier_cad_boolean'
     bl_description = bl_label = 'Boolean'
     bl_options = {'REGISTER', 'UNDO'}
 
@@ -66,7 +66,7 @@ class Boolean(bpy.types.Operator):
 
     @classmethod
     def poll(cls, context):
-        return Util.Selected1OrMoreCurves()
+        return util.Selected1OrMoreCurves()
 
     def execute(self, context):
         current_mode = bpy.context.object.mode
@@ -80,7 +80,7 @@ class Boolean(bpy.types.Operator):
         if len(splines) != 2:
             self.report({'WARNING'}, 'Invalid selection. Only work to selected two spline.')
             return {'CANCELLED'}
-        bpy.ops.curve.spline_type_set(type='BEZIER')
+        bpy.ops.curvetools.spline_type_set(type='BEZIER')
         splineA = bpy.context.object.data.splines.active
         splineB = splines[0] if (splines[1] == splineA) else splines[1]
         if not internal.bezierBooleanGeometry(splineA, splineB, self.operation):
@@ -92,13 +92,13 @@ class Boolean(bpy.types.Operator):
         return {'FINISHED'}
 
 class Intersection(bpy.types.Operator):
-    bl_idname = 'curve.bezier_cad_intersection'
+    bl_idname = 'curvetools.bezier_cad_intersection'
     bl_description = bl_label = 'Intersection'
     bl_options = {'REGISTER', 'UNDO'}
 
     @classmethod
     def poll(cls, context):
-        return Util.Selected1OrMoreCurves()
+        return util.Selected1OrMoreCurves()
 
     def execute(self, context):
         segments = internal.bezierSegments(bpy.context.object.data.splines, True)
@@ -108,15 +108,33 @@ class Intersection(bpy.types.Operator):
 
         internal.bezierMultiIntersection(segments)
         return {'FINISHED'}
+        
+class HandleProjection(bpy.types.Operator):
+    bl_idname = 'curvetools.bezier_cad_handle_projection'
+    bl_description = bl_label = 'Handle Projection'
+    bl_options = {'REGISTER', 'UNDO'}
+
+    @classmethod
+    def poll(cls, context):
+        return internal.curveObject()
+
+    def execute(self, context):
+        segments = internal.bezierSegments(bpy.context.object.data.splines, True)
+        if len(segments) < 1:
+            self.report({'WARNING'}, 'Nothing selected')
+            return {'CANCELLED'}
+
+        internal.bezierProjectHandles(segments)
+        return {'FINISHED'}
 
 class MergeEnds(bpy.types.Operator):
-    bl_idname = 'curve.bezier_cad_merge_ends'
+    bl_idname = 'curvetools.bezier_cad_merge_ends'
     bl_description = bl_label = 'Merge Ends'
     bl_options = {'REGISTER', 'UNDO'}
 
     @classmethod
     def poll(cls, context):
-        return Util.Selected1OrMoreCurves()
+        return util.Selected1OrMoreCurves()
 
     def execute(self, context):
         points = []
@@ -151,17 +169,17 @@ class MergeEnds(bpy.types.Operator):
             points[0].handle_left = handle
         points[0].co = new_co
 
-        bpy.ops.curve.select_all(action='DESELECT')
+        bpy.ops.curvetools.select_all(action='DESELECT')
         points[1].select_control_point = True
-        bpy.ops.curve.delete()
+        bpy.ops.curvetools.delete()
         selected_splines[0].bezier_points[-1 if is_last_point[0] else 0].select_control_point = True
         selected_splines[1].bezier_points[-1 if is_last_point[1] else 0].select_control_point = True
-        bpy.ops.curve.make_segment()
-        bpy.ops.curve.select_all(action='DESELECT')
+        bpy.ops.curvetools.make_segment()
+        bpy.ops.curvetools.select_all(action='DESELECT')
         return {'FINISHED'}
 
 class Subdivide(bpy.types.Operator):
-    bl_idname = 'curve.bezier_cad_subdivide'
+    bl_idname = 'curvetools.bezier_cad_subdivide'
     bl_description = bl_label = 'Subdivide'
     bl_options = {'REGISTER', 'UNDO'}
 
@@ -169,7 +187,7 @@ class Subdivide(bpy.types.Operator):
 
     @classmethod
     def poll(cls, context):
-        return Util.Selected1OrMoreCurves()
+        return util.Selected1OrMoreCurves()
 
     def execute(self, context):
         current_mode = bpy.context.object.mode
@@ -193,7 +211,7 @@ class Subdivide(bpy.types.Operator):
         return {'FINISHED'}
 
 class Array(bpy.types.Operator):
-    bl_idname = 'curve.bezier_cad_array'
+    bl_idname = 'curvetools.bezier_cad_array'
     bl_description = bl_label = 'Array'
     bl_options = {'REGISTER', 'UNDO'}
 
@@ -204,7 +222,7 @@ class Array(bpy.types.Operator):
 
     @classmethod
     def poll(cls, context):
-        return Util.Selected1OrMoreCurves()
+        return util.Selected1OrMoreCurves()
 
     def execute(self, context):
         splines = internal.getSelectedSplines(True, True)
@@ -215,13 +233,13 @@ class Array(bpy.types.Operator):
         return {'FINISHED'}
 
 class Circle(bpy.types.Operator):
-    bl_idname = 'curve.bezier_cad_circle'
+    bl_idname = 'curvetools.bezier_cad_circle'
     bl_description = bl_label = 'Circle'
     bl_options = {'REGISTER', 'UNDO'}
 
     @classmethod
     def poll(cls, context):
-        return Util.Selected1OrMoreCurves()
+        return util.Selected1OrMoreCurves()
 
     def execute(self, context):
         segments = internal.bezierSegments(bpy.context.object.data.splines, True)
@@ -240,12 +258,12 @@ class Circle(bpy.types.Operator):
         return {'FINISHED'}
 
 class Length(bpy.types.Operator):
-    bl_idname = 'curve.bezier_cad_length'
+    bl_idname = 'curvetools.bezier_cad_length'
     bl_description = bl_label = 'Length'
 
     @classmethod
     def poll(cls, context):
-        return Util.Selected1OrMoreCurves()
+        return util.Selected1OrMoreCurves()
 
     def execute(self, context):
         segments = internal.bezierSegments(bpy.context.object.data.splines, True)
@@ -259,4 +277,15 @@ class Length(bpy.types.Operator):
         self.report({'INFO'}, bpy.utils.units.to_string(bpy.context.scene.unit_settings.system, 'LENGTH', length))
         return {'FINISHED'}
 
-operators = [Fillet, Boolean, Intersection, MergeEnds, Subdivide, Array, Circle, Length]
+def register():
+    for cls in classes:
+        bpy.utils.register_class(operators)
+
+def unregister():
+    for cls in classes:
+        bpy.utils.unregister_class(operators)
+
+if __name__ == "__main__":
+    register()
+
+operators = [Fillet, Boolean, Intersection, HandleProjection, MergeEnds, Subdivide, Array, Circle, Length]
diff --git a/curve_tools/curve_outline.py b/curve_tools/curve_outline.py
deleted file mode 100644
index 6847e3c56..000000000
--- a/curve_tools/curve_outline.py
+++ /dev/null
@@ -1,112 +0,0 @@
-'''
-by Yann Bertrand, january 2014.
-
-BEGIN GPL LICENSE BLOCK
-
-This program is free software; you can redistribute it and/or
-modify it under the terms of the GNU General Public License
-as published by the Free Software Foundation; either version 2
-of the License, or (at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program; if not, write to the Free Software Foundation,
-Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
-
-END GPL LICENCE BLOCK
-'''
-
-bl_info = {
-    "name": "Curve Outline",
-    "description": "creates an Outline",
-    "category": "Object",
-    "author": "Yann Bertrand (jimflim)",
-    "version": (0, 4),
-    "blender": (2, 69, 0),
-}
-
-import bpy
-from mathutils import Vector
-from mathutils.geometry import intersect_line_line
-
-
-def createOutline(curve, outline):
-
-    for spline in curve.data.splines[:]:
-        p = spline.bezier_points
-        out = []
-
-        n = ((p[0].handle_right-p[0].co).normalized()-(p[0].handle_left-p[0].co).normalized()).normalized()
-        n = Vector((-n[1], n[0], n[2]))
-        o = p[0].co+outline*n
-        out.append(o)
-
-        for i in range(1,len(p)):
-            n = ((p[i].handle_right-p[i].co).normalized()-(p[i].handle_left-p[i].co).normalized()).normalized()
-            n = Vector((-n[1], n[0], n[2]))
-            o = intersect_line_line(out[-1], (out[-1]+p[i].co-p[i-1].co), p[i].co, p[i].co+n)[0]
-            out.append(o)
-
-        curve.data.splines.new('BEZIER')
-        if spline.use_cyclic_u:
-            curve.data.splines[-1].use_cyclic_u = True
-        p_out = curve.data.splines[-1].bezier_points
-        p_out.add(len(out)-1)
-
-        for i in range(len(out)):
-            p_out[i].handle_left_type = 'FREE'
-            p_out[i].handle_right_type = 'FREE'
-
-            p_out[i].co = out[i]
-
-            if i<len(out)-1:
-                l = (p[i+1].co-p[i].co).length
-                l2 = (out[i]-out[i+1]).length
-
-            if i==0:
-                p_out[i].handle_left = out[i] + ((p[i].handle_left-p[i].co)*l2/l)
-            if i<len(out)-1:
-                p_out[i+1].handle_left = out[i+1] + ((p[i+1].handle_left-p[i+1].co)*l2/l)
-            p_out[i].handle_right = out[i] + ((p[i].handle_right-p[i].co)*l2/l)
-
-        for i in range(len(p)):
-            p_out[i].handle_left_type = p[i].handle_left_type
-            p_out[i].handle_right_type = p[i].handle_right_type
-
-    return
-
-
-class CurveOutline(bpy.types.Operator):
-    """Curve Outliner"""
-    bl_idname = "object._curve_outline"
-    bl_label = "Create Outline"
-    bl_options = {'REGISTER', 'UNDO'}
-    outline: bpy.props.FloatProperty(name="Amount", default=0.1)
-
-    @classmethod
-    def poll(cls, context):
-        return (context.object is not None and
-                context.object.type == 'CURVE')
-
-    def execute(self, context):
-        createOutline(context.object, self.outline)
-        return {'FINISHED'}
-
-    def invoke(self, context, event):
-        return context.window_manager.invoke_props_popup(self, event)
-
-def menu_func(self, context):
-    self.layout.operator(CurveOutline.bl_idname)
-
-def register():
-    bpy.utils.register_class(CurveOutline)
-
-def unregister():
-    bpy.utils.unregister_class(CurveOutline)
-
-if __name__ == "__main__":
-    register()
diff --git a/curve_tools/curve_remove_doubles.py b/curve_tools/curve_remove_doubles.py
deleted file mode 100644
index 373b69c94..000000000
--- a/curve_tools/curve_remove_doubles.py
+++ /dev/null
@@ -1,110 +0,0 @@
-import bpy, mathutils
-
-
-bl_info = {
-    'name': 'Curve Remove Doubles',
-    'author': 'Michael Soluyanov',
-    'version': (1, 1),
-    'blender': (2, 80, 0),
-    'location': 'View3D > Context menu (W/RMB) > Remove Doubles',
-    'description': 'Adds command "Remove Doubles" for curves',
-    'category': 'Object'
-}
-
-def main(context, distance = 0.01):
-    
-    obj = context.active_object
-    dellist = []
-    
-    if bpy.ops.object.mode_set.poll():
-        bpy.ops.object.mode_set(mode='EDIT')
-    
-    for spline in obj.data.splines: 
-        if len(spline.bezier_points) > 1:
-            for i in range(0, len(spline.bezier_points)): 
-                
-                if i == 0:
-                    ii = len(spline.bezier_points) - 1
-                else:        
-                    ii = i - 1
-                    
-                dot = spline.bezier_points[i];
-                dot1 = spline.bezier_points[ii];   
-                    
-                while dot1 in dellist and i != ii:
-                    ii -= 1
-                    if ii < 0: 
-                        ii = len(spline.bezier_points)-1
-                    dot1 = spline.bezier_points[ii]
-                    
-                if dot.select_control_point and dot1.select_control_point and (i!=0 or spline.use_cyclic_u):   
-                    
-                    if (dot.co-dot1.co).length < distance:
-                        # remove points and recreate hangles
-                        dot1.handle_right_type = "FREE"
-                        dot1.handle_right = dot.handle_right
-                        dot1.co = (dot.co + dot1.co) / 2
-                        dellist.append(dot)
-                        
-                    else:
-                        # Handles that are on main point position converts to vector,
-                        # if next handle are also vector
-                        if dot.handle_left_type == 'VECTOR' and (dot1.handle_right - dot1.co).length < distance:
-                            dot1.handle_right_type = "VECTOR"
-                        if dot1.handle_right_type == 'VECTOR' and (dot.handle_left - dot.co).length < distance:
-                            dot.handle_left_type = "VECTOR"  
-                      
-                            
-    
-    bpy.ops.curve.select_all(action = 'DESELECT')
-
-    for dot in dellist:
-        dot.select_control_point = True
-        
-    count = len(dellist)
-    
-    bpy.ops.curve.delete(type = 'VERT')
-    
-    bpy.ops.curve.select_all(action = 'SELECT')
-    
-    return count
-    
-
-
-class CurveRemvDbs(bpy.types.Operator):
-    """Merge consecutive points that are near to each other"""
-    bl_idname = 'curve.remove_doubles'
-    bl_label = 'Remove Doubles'
-    bl_options = {'REGISTER', 'UNDO'}
-
-    distance: bpy.props.FloatProperty(name = 'Distance', default = 0.01)
-
-    @classmethod
-    def poll(cls, context):
-        obj = context.active_object
-        return (obj and obj.type == 'CURVE')
-
-    def execute(self, context):
-        removed=main(context, self.distance)
-        self.report({'INFO'}, "Removed %d bezier points" % removed)
-        return {'FINISHED'}
-
-
-
-
-def menu_func(self, context):
-    self.layout.operator(CurveRemvDbs.bl_idname, text='Remove Doubles')
-
-def register():
-    bpy.utils.register_class(CurveRemvDbs)
-    bpy.types.VIEW3D_MT_edit_curve_context_menu.append(menu_func)
-
-def unregister():
-    bpy.utils.unregister_class(CurveRemvDbs)
-    bpy.types.VIEW3D_MT_edit_curve_context_menu.remove(menu_func)
-
-if __name__ == "__main__":
-    register()
-
-
-
diff --git a/curve_tools/exports.py b/curve_tools/exports.py
index b3b7d9004..130fb2a74 100644
--- a/curve_tools/exports.py
+++ b/curve_tools/exports.py
@@ -224,4 +224,15 @@ class GCodeExport(bpy.types.Operator, ExportHelper):
                                 f.write(speed_code+' X{:.3f} Y{:.3f} Z{:.3f}\n'.format(position[0], position[1], position[2]))
         return {'FINISHED'}
 
+def register():
+    for cls in classes:
+        bpy.utils.register_class(operators)
+
+def unregister():
+    for cls in classes:
+        bpy.utils.unregister_class(operators)
+
+if __name__ == "__main__":
+    register()
+
 operators = [SvgExport, GCodeExport]
diff --git a/curve_tools/fillet.py b/curve_tools/fillet.py
new file mode 100644
index 000000000..3bb3e492c
--- /dev/null
+++ b/curve_tools/fillet.py
@@ -0,0 +1,143 @@
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+#  This program is free software; you can redistribute it and / or
+#  modify it under the terms of the GNU General Public License
+#  as published by the Free Software Foundation; either version 2
+#  of the License, or (at your option) any later version.
+#
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program; if not, write to the Free Software Foundation,
+#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+bl_info = {
+    'name': 'Curve Fillet',
+    'author': 'Spivak Vladimir (cwolf3d)',
+    'version': (0, 0, 1),
+    'blender': (2, 80, 0),
+    'location': 'Curve Tools addon. (N) Panel',
+    'description': 'Various types of fillet (chamfering)',
+    'warning': '', # used for warning icon and text in addons panel
+    'wiki_url': '',
+    'tracker_url': '',
+    'category': 'Curve'}
+
+
+import bpy
+from bpy.props import *
+from bpy_extras import object_utils, view3d_utils
+from mathutils import  *
+from math import  *
+
+def click(self, context, event):
+    bpy.ops.object.mode_set(mode = 'EDIT')
+    bpy.context.view_layer.update()
+
+
+def remove_handler(handlers):
+    for handler in handlers:
+        try:
+            bpy.types.SpaceView3D.draw_handler_remove(handler, 'WINDOW')
+        except:
+            pass
+    for handler in handlers:
+        handlers.remove(handler)                        
+
+
+class Fillet(bpy.types.Operator):
+    bl_idname = "curvetools.fillet"
+    bl_label = "Curve Fillet"
+    bl_description = "Curve Fillet"
+    bl_options = {'REGISTER', 'UNDO'}
+    
+    x: IntProperty(name="x", description="x")
+    y: IntProperty(name="y", description="y")
+    location3D: FloatVectorProperty(name = "",
+                description = "Start location",
+                default = (0.0, 0.0, 0.0),
+                subtype = 'XYZ')
+                
+    handlers = []
+        
+    def execute(self, context):
+        self.report({'INFO'}, "ESC or TAB - cancel")
+        bpy.ops.object.mode_set(mode = 'EDIT')
+        
+        # color change in the panel
+        self.path_color = bpy.context.scene.curvetools.path_color
+        self.path_thickness = bpy.context.scene.curvetools.path_thickness
+
+    def modal(self, context, event):
+        context.area.tag_redraw()
+        
+        if event.type in {'ESC', 'TAB'}:  # Cancel
+            remove_handler(self.handlers)
+            return {'CANCELLED'}
+            
+        if event.type in {'X', 'DEL'}:  # Cancel
+            remove_handler(self.handlers)
+            bpy.ops.curve.delete(type='VERT') 
+            return {'RUNNING_MODAL'}         
+        
+        elif event.alt and event.shift and event.type == 'LEFTMOUSE':
+            click(self, context, event)
+        
+        elif event.alt and not event.shift and event.type == 'LEFTMOUSE':
+            remove_handler(self.handlers)
+            bpy.ops.curve.select_all(action='DESELECT')
+            click(self, context, event)
+            
+        elif event.alt and event.type == 'RIGHTMOUSE':
+           remove_handler(self.handlers)
+           bpy.ops.curve.select_all(action='DESELECT')
+           click(self, context, event)
+
+        elif event.alt and not event.shift and event.shift and event.type == 'RIGHTMOUSE':
+            click(self, context, event)
+            
+        elif event.type == 'A':
+            remove_handler(self.handlers)
+            bpy.ops.curve.select_all(action='DESELECT')
+            
+        elif event.type == 'MOUSEMOVE':  # 
+            self.x = event.mouse_x
+            self.y = event.mouse_y
+            region = bpy.context.region
+            rv3d = bpy.context.space_data.region_3d
+            self.location3D = view3d_utils.region_2d_to_location_3d(
+                region,
+                rv3d,
+                (event.mouse_region_x, event.mouse_region_y),
+                (0.0, 0.0, 0.0)
+                )       
+
+        return {'PASS_THROUGH'}
+
+    def invoke(self, context, event):
+        self.execute(context)
+        context.window_manager.modal_handler_add(self)
+        return {'RUNNING_MODAL'}
+        
+    @classmethod
+    def poll(cls, context):
+        return (context.object is not None and
+                context.object.type == 'CURVE')
+
+def register():
+    for cls in classes:
+        bpy.utils.register_class(operators)
+
+def unregister():
+    for cls in classes:
+        bpy.utils.unregister_class(operators)
+
+if __name__ == "__main__":
+    register()
+
+operators = [Fillet]
diff --git a/curve_tools/internal.py b/curve_tools/internal.py
index 6741bb22d..e967fc6ed 100644
--- a/curve_tools/internal.py
+++ b/curve_tools/internal.py
@@ -408,6 +408,25 @@ def bezierMultiIntersection(segments):
     prepareSegmentIntersections(segments)
     subdivideBezierSegments(segments)
 
+def bezierProjectHandles(segments):
+    insertions = []
+    index_offset = 0
+    for segment in segments:
+        if len(insertions) > 0 and insertions[-1][0] != segment['spline']:
+            index_offset = 0
+        points = bezierSegmentPoints(segment['beginPoint'], segment['endPoint'])
+        paramA, paramB, pointA, pointB = nearestPointOfLines(points[0], points[1]-points[0], points[3], points[2]-points[3])
+        if pointA and pointB:
+            segment['cuts'].append({'param': 0.5})
+            insertions.append((segment['spline'], segment['beginIndex']+1+index_offset, (pointA+pointB)*0.5))
+            index_offset += 1
+    subdivideBezierSegments(segments)
+    for insertion in insertions:
+        bezier_point = insertion[0].bezier_points[insertion[1]]
+        bezier_point.co = insertion[2]
+        bezier_point.handle_left_type = 'VECTOR'
+        bezier_point.handle_right_type = 'VECTOR'
+
 def bezierSubivideAt(points, params):
     if len(params) == 0:
         return []
diff --git a/curve_tools/CurveIntersections.py b/curve_tools/intersections.py
similarity index 94%
rename from curve_tools/CurveIntersections.py
rename to curve_tools/intersections.py
index 062547010..77f198614 100644
--- a/curve_tools/CurveIntersections.py
+++ b/curve_tools/intersections.py
@@ -1,7 +1,7 @@
 import bpy
-from . import Math
-from . import Curves
-from . import Util
+from . import mathematics
+from . import curves
+from . import util
 
 from mathutils import Vector
 
@@ -58,7 +58,7 @@ class BezierSegmentsIntersector:
                 Q0 = self.worldMatrix2 @ self.segment2.CalcPoint(parameter=segPar20)
                 Q1 = self.worldMatrix2 @ self.segment2.CalcPoint(parameter=segPar21)
 
-                intersectionPointData = Math.CalcIntersectionPointLineSegments(P0, P1, Q0, Q1, limitDistance)
+                intersectionPointData = mathematics.CalcIntersectionPointLineSegments(P0, P1, Q0, Q1, limitDistance)
                 if intersectionPointData is None:
                     continue
 
@@ -94,15 +94,15 @@ class BezierSegmentsIntersector:
                 Q0 = self.worldMatrix2 @ self.segment2.CalcPoint(parameter=segPar20)
                 Q1 = self.worldMatrix2 @ self.segment2.CalcPoint(parameter=segPar21)
 
-                intersectionPointData = Math.CalcIntersectionPointLineSegments(P0, P1, Q0, Q1, limitDistance)
+                intersectionPointData = mathematics.CalcIntersectionPointLineSegments(P0, P1, Q0, Q1, limitDistance)
                 if intersectionPointData is None:
                     continue
 
                 # intersection point can't be an existing point
                 intersectionSegment1Parameter = segPar10 + (intersectionPointData[0] / (fltNrSamples1))
                 worldPoint1 = self.worldMatrix1 @ self.segment1.CalcPoint(parameter=intersectionSegment1Parameter)
-                if (Math.IsSamePoint(P0, worldPoint1, limitDistance)) or \
-                   (Math.IsSamePoint(P1, worldPoint1, limitDistance)):
+                if (mathematics.IsSamePoint(P0, worldPoint1, limitDistance)) or \
+                   (mathematics.IsSamePoint(P1, worldPoint1, limitDistance)):
 
                     intersectionPoint1 = None
                 else:
@@ -112,8 +112,8 @@ class BezierSegmentsIntersector:
 
                 intersectionSegment2Parameter = segPar20 + (intersectionPointData[1] / (fltNrSamples2))
                 worldPoint2 = self.worldMatrix2 @ self.segment2.CalcPoint(parameter=intersectionSegment2Parameter)
-                if (Math.IsSamePoint(Q0, worldPoint2, limitDistance)) or \
-                   (Math.IsSamePoint(Q1, worldPoint2, limitDistance)):
+                if (mathematics.IsSamePoint(Q0, worldPoint2, limitDistance)) or \
+                   (mathematics.IsSamePoint(Q1, worldPoint2, limitDistance)):
 
                     intersectionPoint2 = None
                 else:
@@ -143,7 +143,7 @@ class BezierSegmentsIntersector:
                 Q0 = self.worldMatrix2 @ self.segment2.CalcPoint(parameter=segPar20)
                 Q1 = self.worldMatrix2 @ self.segment2.CalcPoint(parameter=segPar21)
 
-                intersectionPointData = Math.CalcIntersectionPointsLineSegmentsDIR(P0, P1, Q0, Q1, algoDIR)
+                intersectionPointData = mathematics.CalcIntersectionPointsLineSegmentsDIR(P0, P1, Q0, Q1, algoDIR)
                 if intersectionPointData is None:
                     continue
 
@@ -183,15 +183,15 @@ class BezierSegmentsIntersector:
                 Q0 = self.worldMatrix2 @ self.segment2.CalcPoint(parameter=segPar20)
                 Q1 = self.worldMatrix2 @ self.segment2.CalcPoint(parameter=segPar21)
 
-                intersectionPointData = Math.CalcIntersectionPointsLineSegmentsDIR(P0, P1, Q0, Q1, algoDIR)
+                intersectionPointData = mathematics.CalcIntersectionPointsLineSegmentsDIR(P0, P1, Q0, Q1, algoDIR)
                 if intersectionPointData is None:
                     continue
 
                 # intersection point can't be an existing point
                 intersectionSegment1Parameter = segPar10 + (intersectionPointData[0] / (fltNrSamples1))
                 worldPoint1 = self.worldMatrix1 @ self.segment1.CalcPoint(parameter=intersectionSegment1Parameter)
-                if (Math.IsSamePoint(P0, worldPoint1, limitDistance)) or \
-                   (Math.IsSamePoint(P1, worldPoint1, limitDistance)):
+                if (mathematics.IsSamePoint(P0, worldPoint1, limitDistance)) or \
+                   (mathematics.IsSamePoint(P1, worldPoint1, limitDistance)):
 
                     intersectionPoint1 = None
                 else:
@@ -201,8 +201,8 @@ class BezierSegmentsIntersector:
 
                 intersectionSegment2Parameter = segPar20 + (intersectionPointData[1] / (fltNrSamples2))
                 worldPoint2 = self.worldMatrix2 @ self.segment2.CalcPoint(parameter=intersectionSegment2Parameter)
-                if (Math.IsSamePoint(Q0, worldPoint2, limitDistance)) or \
-                   (Math.IsSamePoint(Q1, worldPoint2, limitDistance)):
+                if (mathematics.IsSamePoint(Q0, worldPoint2, limitDistance)) or \
+                   (mathematics.IsSamePoint(Q1, worldPoint2, limitDistance)):
 
                     intersectionPoint2 = None
                 else:
@@ -232,7 +232,7 @@ class BezierSegmentsIntersector:
                 Q0 = self.worldMatrix2 @ self.segment2.CalcPoint(parameter=segPar20)
                 Q1 = self.worldMatrix2 @ self.segment2.CalcPoint(parameter=segPar21)
 
-                intersectionPointData = Math.CalcIntersectionPointsLineSegmentsPOV(P0, P1, Q0, Q1, algoPOV)
+                intersectionPointData = mathematics.CalcIntersectionPointsLineSegmentsPOV(P0, P1, Q0, Q1, algoPOV)
                 if intersectionPointData is None:
                     continue
 
@@ -272,15 +272,15 @@ class BezierSegmentsIntersector:
                 Q0 = self.worldMatrix2 @ self.segment2.CalcPoint(parameter=segPar20)
                 Q1 = self.worldMatrix2 @ self.segment2.CalcPoint(parameter=segPar21)
 
-                intersectionPointData = Math.CalcIntersectionPointsLineSegmentsPOV(P0, P1, Q0, Q1, algoPOV)
+                intersectionPointData = mathematics.CalcIntersectionPointsLineSegmentsPOV(P0, P1, Q0, Q1, algoPOV)
                 if intersectionPointData is None:
                     continue
 
                 # intersection point can't be an existing point
                 intersectionSegment1Parameter = segPar10 + (intersectionPointData[0] / fltNrSamples1)
                 worldPoint1 = self.worldMatrix1 @ self.segment1.CalcPoint(parameter=intersectionSegment1Parameter)
-                if (Math.IsSamePoint(P0, worldPoint1, limitDistance)) or \
-                   (Math.IsSamePoint(P1, worldPoint1, limitDistance)):
+                if (mathematics.IsSamePoint(P0, worldPoint1, limitDistance)) or \
+                   (mathematics.IsSamePoint(P1, worldPoint1, limitDistance)):
 
                     intersectionPoint1 = None
                 else:
@@ -290,8 +290,8 @@ class BezierSegmentsIntersector:
 
                 intersectionSegment2Parameter = segPar20 + (intersectionPointData[1] / fltNrSamples2)
                 worldPoint2 = self.worldMatrix2 @ self.segment2.CalcPoint(parameter=intersectionSegment2Parameter)
-                if (Math.IsSamePoint(Q0, worldPoint2, limitDistance)) or \
-                   (Math.IsSamePoint(Q1, worldPoint2, limitDistance)):
+                if (mathematics.IsSamePoint(Q0, worldPoint2, limitDistance)) or \
+                   (mathematics.IsSamePoint(Q1, worldPoint2, limitDistance)):
 
                     intersectionPoint2 = None
                 else:
@@ -341,7 +341,7 @@ class BezierSegmentsIntersector:
                 Q0 = self.worldMatrix2 @ self.segment2.CalcPoint(parameter=segPar20)
                 Q1 = self.worldMatrix2 @ self.segment2.CalcPoint(parameter=segPar21)
 
-                intersectionPointData = Math.CalcIntersectionPointLineSegments(P0, P1, Q0, Q1, limitDistance)
+                intersectionPointData = mathematics.CalcIntersectionPointLineSegments(P0, P1, Q0, Q1, limitDistance)
                 if intersectionPointData is None:
                     continue
 
@@ -382,7 +382,7 @@ class BezierSegmentsIntersector:
                 Q0 = self.worldMatrix2 @ self.segment2.CalcPoint(parameter=segPar20)
                 Q1 = self.worldMatrix2 @ self.segment2.CalcPoint(parameter=segPar21)
 
-                intersectionPointData = Math.CalcIntersectionPointsLineSegmentsDIR(P0, P1, Q0, Q1, algoDIR)
+                intersectionPointData = mathematics.CalcIntersectionPointsLineSegmentsDIR(P0, P1, Q0, Q1, algoDIR)
                 if intersectionPointData is None:
                     continue
 
@@ -423,7 +423,7 @@ class BezierSegmentsIntersector:
                 Q0 = self.worldMatrix2 @ self.segment2.CalcPoint(parameter=segPar20)
                 Q1 = self.worldMatrix2 @ self.segment2.CalcPoint(parameter=segPar21)
 
-                intersectionPointData = Math.CalcIntersectionPointsLineSegmentsPOV(P0, P1, Q0, Q1, algoPOV)
+                intersectionPointData = mathematics.CalcIntersectionPointsLineSegmentsPOV(P0, P1, Q0, Q1, algoPOV)
                 if intersectionPointData is None:
                     continue
 
@@ -508,8 +508,8 @@ class CurvesIntersector:
         if blenderActiveCurve == blenderOtherCurve:
             blenderOtherCurve = selObjects[1]
 
-        aCurve = Curves.Curve(blenderActiveCurve)
-        oCurve = Curves.Curve(blenderOtherCurve)
+        aCurve = curves.Curve(blenderActiveCurve)
+        oCurve = curves.Curve(blenderOtherCurve)
 
         return CurvesIntersector(aCurve, oCurve)
 
@@ -528,7 +528,7 @@ class CurvesIntersector:
 
         algo = bpy.context.scene.curvetools.IntersectCurvesAlgorithm
         if algo == 'From View':
-            regionView3D = Util.GetFirstRegionView3D()
+            regionView3D = util.GetFirstRegionView3D()
             if regionView3D is None:
                 print("### ERROR: regionView3D is None. Stopping.")
                 return
diff --git a/curve_tools/Math.py b/curve_tools/mathematics.py
similarity index 100%
rename from curve_tools/Math.py
rename to curve_tools/mathematics.py
diff --git a/curve_tools/outline.py b/curve_tools/outline.py
new file mode 100644
index 000000000..66cc29e4d
--- /dev/null
+++ b/curve_tools/outline.py
@@ -0,0 +1,118 @@
+'''
+by Yann Bertrand, january 2014.
+
+BEGIN GPL LICENSE BLOCK
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software Foundation,
+Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+END GPL LICENCE BLOCK
+'''
+
+bl_info = {
+    "name": "Curve Outline",
+    "description": "creates an Outline",
+    "category": "Object",
+    "author": "Yann Bertrand (jimflim)",
+    "version": (0, 4),
+    "blender": (2, 69, 0),
+}
+
+import bpy
+from mathutils import Vector
+from mathutils.geometry import intersect_line_line
+
+from . import util
+
+
+def createOutline(curve, outline):
+
+    for spline in curve.data.splines[:]:
+        if spline.type == 'BEZIER':
+            p = spline.bezier_points
+            out = []
+    
+            n = ((p[0].handle_right-p[0].co).normalized()-(p[0].handle_left-p[0].co).normalized()).normalized()
+            n = Vector((-n[1], n[0], n[2]))
+            o = p[0].co+outline*n
+            out.append(o)
+    
+            for i in range(1,len(p)):
+                n = ((p[i].handle_right-p[i].co).normalized()-(p[i].handle_left-p[i].co).normalized()).normalized()
+                n = Vector((-n[1], n[0], n[2]))
+                o = intersect_line_line(out[-1], (out[-1]+p[i].co-p[i-1].co), p[i].co, p[i].co+n)[0]
+                out.append(o)
+    
+            curve.data.splines.new('BEZIER')
+            if spline.use_cyclic_u:
+                curve.data.splines[-1].use_cyclic_u = True
+            p_out = curve.data.splines[-1].bezier_points
+            p_out.add(len(out)-1)
+    
+            for i in range(len(out)):
+                p_out[i].handle_left_type = 'FREE'
+                p_out[i].handle_right_type = 'FREE'
+    
+                p_out[i].co = out[i]
+    
+                if i<len(out)-1:
+                    l = (p[i+1].co-p[i].co).length
+                    l2 = (out[i]-out[i+1]).length
+    
+                if i==0:
+                    p_out[i].handle_left = out[i] + ((p[i].handle_left-p[i].co)*l2/l)
+                if i<len(out)-1:
+                    p_out[i+1].handle_left = out[i+1] + ((p[i+1].handle_left-p[i+1].co)*l2/l)
+                p_out[i].handle_right = out[i] + ((p[i].handle_right-p[i].co)*l2/l)
+    
+            for i in range(len(p)):
+                p_out[i].handle_left_type = p[i].handle_left_type
+                p_out[i].handle_right_type = p[i].handle_right_type
+
+    return
+
+
+class CurveOutline(bpy.types.Operator):
+    """Curve Outliner"""
+    bl_idname = "curvetools.outline"
+    bl_label = "Create Outline"
+    bl_options = {'REGISTER', 'UNDO'}
+    outline: bpy.props.FloatProperty(name="Amount", default=0.1)
+
+    @classmethod
+    def poll(cls, context):
+        return util.Selected1OrMoreCurves()
+
+    def execute(self, context):
+        createOutline(context.object, self.outline)
+        return {'FINISHED'}
+
+    def invoke(self, context, event):
+        return context.window_manager.invoke_props_popup(self, event)
+
+def menu_func(self, context):
+    self.layout.operator(CurveOutline.bl_idname)
+
+def register():
+    for cls in classes:
+        bpy.utils.register_class(operators)
+
+def unregister():
+    for cls in classes:
+        bpy.utils.unregister_class(operators)
+
+if __name__ == "__main__":
+    register()
+
+operators = [CurveOutline]
diff --git a/curve_tools/PathFinder.py b/curve_tools/path_finder.py
similarity index 95%
rename from curve_tools/PathFinder.py
rename to curve_tools/path_finder.py
index ab11a4e72..366f9bcca 100644
--- a/curve_tools/PathFinder.py
+++ b/curve_tools/path_finder.py
@@ -41,12 +41,8 @@ from bpy_extras import object_utils, view3d_utils
 from mathutils import  *
 from math import  *
 
-from . import Properties
-from . import Curves
-from . import CurveIntersections
-from . import Util
-from . import Surfaces
-from . import Math
+from . import mathematics
+from . import util
 
 def get_bezier_points(spline, matrix_world):
     point_list = []
@@ -55,7 +51,7 @@ def get_bezier_points(spline, matrix_world):
         for i in range(0, len_bezier_points - 1):
             point_list.extend([matrix_world @ spline.bezier_points[i].co])
             for t in range(0, 100, 2):
-                h = Math.subdivide_cubic_bezier(spline.bezier_points[i].co,
+                h = mathematics.subdivide_cubic_bezier(spline.bezier_points[i].co,
                                            spline.bezier_points[i].handle_right,
                                            spline.bezier_points[i + 1].handle_left,
                                            spline.bezier_points[i + 1].co,
@@ -64,7 +60,7 @@ def get_bezier_points(spline, matrix_world):
         if spline.use_cyclic_u and len_bezier_points > 2:
             point_list.extend([matrix_world @ spline.bezier_points[len_bezier_points - 1].co])
             for t in range(0, 100, 2):
-                h = Math.subdivide_cubic_bezier(spline.bezier_points[len_bezier_points - 1].co,
+                h = mathematics.subdivide_cubic_bezier(spline.bezier_points[len_bezier_points - 1].co,
                                            spline.bezier_points[len_bezier_points - 1].handle_right,
                                            spline.bezier_points[0].handle_left,
                                            spline.bezier_points[0].co,
@@ -160,7 +156,7 @@ def click(self, context, event):
                         
                     if i < len_bezier_points - 1:
                         for t in range(0, 100, 2):
-                            h = Math.subdivide_cubic_bezier(spline.bezier_points[i].co,
+                            h = mathematics.subdivide_cubic_bezier(spline.bezier_points[i].co,
                                            spline.bezier_points[i].handle_right,
                                            spline.bezier_points[i + 1].handle_left,
                                            spline.bezier_points[i + 1].co,
@@ -172,7 +168,7 @@ def click(self, context, event):
 
                     if spline.use_cyclic_u and len_bezier_points > 2:
                         for t in range(0, 100, 2):
-                            h = Math.subdivide_cubic_bezier(spline.bezier_points[len_bezier_points - 1].co,
+                            h = mathematics.subdivide_cubic_bezier(spline.bezier_points[len_bezier_points - 1].co,
                                            spline.bezier_points[len_bezier_points - 1].handle_right,
                                            spline.bezier_points[0].handle_left,
                                            spline.bezier_points[0].co,
@@ -312,14 +308,18 @@ class PathFinder(bpy.types.Operator):
         
     @classmethod
     def poll(cls, context):
-        return (context.object is not None and
-                context.object.type == 'CURVE')
+        return util.Selected1OrMoreCurves()
 
 def register():
-    bpy.utils.register_class(PathFinder)
+    for cls in classes:
+        bpy.utils.register_class(operators)
 
 def unregister():
-    bpy.utils.unregister_class(PathFinder)
+    for cls in classes:
+        bpy.utils.unregister_class(operators)
 
 if __name__ == "__main__":
     register()
+
+
+operators = [PathFinder]
diff --git a/curve_tools/remove_doubles.py b/curve_tools/remove_doubles.py
new file mode 100644
index 000000000..151b19b0a
--- /dev/null
+++ b/curve_tools/remove_doubles.py
@@ -0,0 +1,135 @@
+import bpy, mathutils
+from . import util
+
+bl_info = {
+    'name': 'Curve Remove Doubles',
+    'author': 'Michael Soluyanov',
+    'version': (1, 1),
+    'blender': (2, 80, 0),
+    'location': 'View3D > Context menu (W/RMB) > Remove Doubles',
+    'description': 'Adds comand "Remove Doubles" for curves',
+    'category': 'Add Curve'
+}
+
+def main(context, distance = 0.01):
+    
+    selected_Curves = util.GetSelectedCurves()
+    if bpy.ops.object.mode_set.poll():
+        bpy.ops.object.mode_set(mode='EDIT')
+        
+    for curve in selected_Curves:
+        bezier_dellist = []
+        dellist = []
+    
+        for spline in curve.data.splines: 
+            if spline.type == 'BEZIER':
+                if len(spline.bezier_points) > 1:
+                    for i in range(0, len(spline.bezier_points)): 
+                        
+                        if i == 0:
+                            ii = len(spline.bezier_points) - 1
+                        else:        
+                            ii = i - 1
+                            
+                        dot = spline.bezier_points[i];
+                        dot1 = spline.bezier_points[ii];   
+                            
+                        while dot1 in bezier_dellist and i != ii:
+                            ii -= 1
+                            if ii < 0: 
+                                ii = len(spline.bezier_points)-1
+                            dot1 = spline.bezier_points[ii]
+                            
+                        if dot.select_control_point and dot1.select_control_point and (i!=0 or spline.use_cyclic_u):   
+                    
+                            if (dot.co-dot1.co).length < distance:
+                                # remove points and recreate hangles
+                                dot1.handle_right_type = "FREE"
+                                dot1.handle_right = dot.handle_right
+                                dot1.co = (dot.co + dot1.co) / 2
+                                bezier_dellist.append(dot)
+                                
+                            else:
+                                # Handles that are on main point position converts to vector,
+                                # if next handle are also vector
+                                if dot.handle_left_type == 'VECTOR' and (dot1.handle_right - dot1.co).length < distance:
+                                    dot1.handle_right_type = "VECTOR"
+                                if dot1.handle_right_type == 'VECTOR' and (dot.handle_left - dot.co).length < distance:
+                                    dot.handle_left_type = "VECTOR"  
+            else:
+                if len(spline.points) > 1:
+                    for i in range(0, len(spline.points)): 
+                        
+                        if i == 0:
+                            ii = len(spline.points) - 1
+                        else:        
+                            ii = i - 1
+                            
+                        dot = spline.points[i];
+                        dot1 = spline.points[ii];   
+                            
+                        while dot1 in dellist and i != ii:
+                            ii -= 1
+                            if ii < 0: 
+                                ii = len(spline.points)-1
+                            dot1 = spline.points[ii]
+                            
+                        if dot.select and dot1.select and (i!=0 or spline.use_cyclic_u):   
+                    
+                            if (dot.co-dot1.co).length < distance:
+                                dot1.co = (dot.co + dot1.co) / 2
+                                dellist.append(dot)
+
+    bpy.ops.curve.select_all(action = 'DESELECT')
+
+    for dot in bezier_dellist:
+        dot.select_control_point = True
+    
+    for dot in dellist:
+        dot.select = True
+    
+    bezier_count = len(bezier_dellist)
+    count = len(dellist)
+    
+    bpy.ops.curve.delete(type = 'VERT')
+    
+    bpy.ops.curve.select_all(action = 'DESELECT')
+    
+    return bezier_count + count
+    
+
+
+class CurveRemvDbs(bpy.types.Operator):
+    """Merge consecutive points that are near to each other"""
+    bl_idname = 'curvetools.remove_doubles'
+    bl_label = 'Remove Doubles'
+    bl_options = {'REGISTER', 'UNDO'}
+
+    distance: bpy.props.FloatProperty(name = 'Distance', default = 0.01)
+
+    @classmethod
+    def poll(cls, context):
+        return util.Selected1Curve()
+
+    def execute(self, context):
+        removed=main(context, self.distance)
+        self.report({'INFO'}, "Removed %d bezier points" % removed)
+        return {'FINISHED'}
+
+
+
+def menu_func(self, context):
+    self.layout.operator(CurveRemvDbs.bl_idname, text='Remove Doubles')
+
+def register():
+    bpy.utils.register_class(CurveRemvDbs)
+    bpy.types.VIEW3D_MT_edit_curve_context_menu.append(menu_func)
+
+def unregister():
+    bpy.utils.unregister_class(CurveRemvDbs)
+    bpy.types.VIEW3D_MT_edit_curve_context_menu.remove(menu_func)
+
+if __name__ == "__main__":
+    register()
+
+operators = [CurveRemvDbs]
diff --git a/curve_tools/ShowCurveResolution.py b/curve_tools/show_resolution.py
similarity index 92%
rename from curve_tools/ShowCurveResolution.py
rename to curve_tools/show_resolution.py
index 6386fe4d4..78acc1e34 100644
--- a/curve_tools/ShowCurveResolution.py
+++ b/curve_tools/show_resolution.py
@@ -18,7 +18,7 @@
 #
 
 
-# LOAD MODULE #
+# LOAD MODUL #
 import bpy
 from bpy import *
 from bpy.props import *
@@ -79,7 +79,7 @@ def draw(self, context, splines, curve_vertcolor, matrix_world):
 
 
 class ShowCurveResolution(bpy.types.Operator):
-    bl_idname = "curve.show_resolution"
+    bl_idname = "curvetools.show_resolution"
     bl_label = "Show Curve Resolution"
     bl_description = "Show curve Resolution / [ESC] - remove"
     
@@ -129,3 +129,18 @@ class ShowCurveResolution(bpy.types.Operator):
     def poll(cls, context):
         return (context.object is not None and
                 context.object.type == 'CURVE')
+
+def register():
+    for cls in classes:
+        bpy.utils.register_class(operators)
+
+def unregister():
+    for cls in classes:
+        bpy.utils.unregister_class(operators)
+
+if __name__ == "__main__":
+    register()
+
+operators = [
+    ShowCurveResolution,
+    ]
diff --git a/curve_tools/SplinesSequence.py b/curve_tools/splines_sequence.py
similarity index 96%
rename from curve_tools/SplinesSequence.py
rename to curve_tools/splines_sequence.py
index c64ceec19..5d11aedff 100644
--- a/curve_tools/SplinesSequence.py
+++ b/curve_tools/splines_sequence.py
@@ -89,7 +89,7 @@ def draw(self, context, splines, sequence_color, font_thickness, font_size, matr
             i += font_size + font_size * 0.5
 
 class ShowSplinesSequence(bpy.types.Operator):
-    bl_idname = "curve.show_splines_sequence"
+    bl_idname = "curvetools.show_splines_sequence"
     bl_label = "Show Splines Sequence"
     bl_description = "Show Splines Sequence / [ESC] - remove"
     
@@ -229,7 +229,7 @@ def rearrange(dataCurve, select_spline, command):
             rearrangesplines(dataCurve, select_spline, select_spline - 1)
                 
 class RearrangeSpline(bpy.types.Operator):
-    bl_idname = "curve.rearrange_spline"
+    bl_idname = "curvetools.rearrange_spline"
     bl_label = "Rearrange Spline"
     bl_description = "Rearrange Spline"
     
@@ -273,3 +273,16 @@ class RearrangeSpline(bpy.types.Operator):
     def poll(cls, context):
         return (context.object is not None and
                 context.object.type == 'CURVE')
+
+def register():
+    for cls in classes:
+        bpy.utils.register_class(operators)
+
+def unregister():
+    for cls in classes:
+        bpy.utils.unregister_class(operators)
+
+if __name__ == "__main__":
+    register()
+
+operators = [ShowSplinesSequence, RearrangeSpline]
diff --git a/curve_tools/toolpath.py b/curve_tools/toolpath.py
index 8353f7ac2..2b4222804 100644
--- a/curve_tools/toolpath.py
+++ b/curve_tools/toolpath.py
@@ -21,7 +21,7 @@ from mathutils import Vector, Matrix
 from . import internal
 
 class OffsetCurve(bpy.types.Operator):
-    bl_idname = 'curve.add_toolpath_offset_curve'
+    bl_idname = 'curvetools.add_toolpath_offset_curve'
     bl_description = bl_label = 'Offset Curve'
     bl_options = {'REGISTER', 'UNDO'}
 
@@ -64,7 +64,7 @@ class OffsetCurve(bpy.types.Operator):
         return {'FINISHED'}
 
 class SliceMesh(bpy.types.Operator):
-    bl_idname = 'curve.add_toolpath_slice_mesh'
+    bl_idname = 'curvetools.add_toolpath_slice_mesh'
     bl_description = bl_label = 'Slice Mesh'
     bl_options = {'REGISTER', 'UNDO'}
 
@@ -116,7 +116,7 @@ class SliceMesh(bpy.types.Operator):
         return {'FINISHED'}
 
 class DiscretizeCurve(bpy.types.Operator):
-    bl_idname = 'curve.add_toolpath_discretize_curve'
+    bl_idname = 'curvetools.add_toolpath_discretize_curve'
     bl_description = bl_label = 'Discretize Curve'
     bl_options = {'REGISTER', 'UNDO'}
 
@@ -151,7 +151,7 @@ class DiscretizeCurve(bpy.types.Operator):
         return {'FINISHED'}
 
 class Truncate(bpy.types.Operator):
-    bl_idname = 'curve.add_toolpath_truncate'
+    bl_idname = 'curvetools.add_toolpath_truncate'
     bl_description = bl_label = 'Truncate'
     bl_options = {'REGISTER', 'UNDO'}
 
@@ -197,7 +197,7 @@ class Truncate(bpy.types.Operator):
         return {'FINISHED'}
 
 class RectMacro(bpy.types.Operator):
-    bl_idname = 'curve.add_toolpath_rect_macro'
+    bl_idname = 'curvetools.add_toolpath_rect_macro'
     bl_description = bl_label = 'Rect Macro'
     bl_options = {'REGISTER', 'UNDO'}
 
@@ -233,7 +233,7 @@ class RectMacro(bpy.types.Operator):
         return {'FINISHED'}
 
 class DrillMacro(bpy.types.Operator):
-    bl_idname = 'curve.add_toolpath_drill_macro'
+    bl_idname = 'curvetools.add_toolpath_drill_macro'
     bl_description = bl_label = 'Drill Macro'
     bl_options = {'REGISTER', 'UNDO'}
 
@@ -284,4 +284,15 @@ class DrillMacro(bpy.types.Operator):
         internal.addPolygonSpline(bpy.context.object, False, vertices, weights)
         return {'FINISHED'}
 
+def register():
+    for cls in classes:
+        bpy.utils.register_class(operators)
+
+def unregister():
+    for cls in classes:
+        bpy.utils.unregister_class(operators)
+
+if __name__ == "__main__":
+    register()
+
 operators = [OffsetCurve, SliceMesh, DiscretizeCurve, Truncate, RectMacro, DrillMacro]
-- 
GitLab