Skip to content
Snippets Groups Projects
cad.py 9.79 KiB
Newer Older
  • Learn to ignore specific revisions
  • # SPDX-License-Identifier: GPL-2.0-or-later
    
    
    bl_info = {
        'name': 'Curve CAD Tools',
        'author': 'Alexander Meißner',
        'version': (1, 0, 0),
        'blender': (2, 80, 0),
        'category': 'Curve',
    
        'doc_url': 'https://github.com/Lichtso/curve_cad',
    
        'tracker_url': 'https://github.com/lichtso/curve_cad/issues'
    }
    
    import bpy
    from . import internal
    
    
    class Fillet(bpy.types.Operator):
    
        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)
    
    
        @classmethod
        def poll(cls, context):
    
            return util.Selected1OrMoreCurves()
    
    
        def execute(self, context):
            splines = internal.getSelectedSplines(True, True, True)
            if len(splines) == 0:
                self.report({'WARNING'}, 'Nothing selected')
                return {'CANCELLED'}
            for spline in splines:
                internal.filletSpline(spline, self.radius, self.chamfer_mode, self.limit_half_way)
                bpy.context.object.data.splines.remove(spline)
            return {'FINISHED'}
    
    class Boolean(bpy.types.Operator):
    
        bl_idname = 'curvetools.bezier_cad_boolean'
    
        bl_description = bl_label = 'Boolean'
        bl_options = {'REGISTER', 'UNDO'}
    
        operation: bpy.props.EnumProperty(name='Type', items=[
            ('UNION', 'Union', 'Boolean OR', 0),
            ('INTERSECTION', 'Intersection', 'Boolean AND', 1),
            ('DIFFERENCE', 'Difference', 'Active minus Selected', 2)
        ])
    
        @classmethod
        def poll(cls, context):
    
    
        def execute(self, context):
    
            current_mode = bpy.context.object.mode
    
            bpy.ops.object.mode_set(mode = 'EDIT')
    
            if bpy.context.object.data.dimensions != '2D':
                self.report({'WARNING'}, 'Can only be applied in 2D')
                return {'CANCELLED'}
            splines = internal.getSelectedSplines(True, True)
            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')
    
            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):
    
                self.report({'WARNING'}, 'Invalid selection. Only work to selected two spline.')
    
                return {'CANCELLED'}
    
            bpy.ops.object.mode_set (mode = current_mode)
    
            return {'FINISHED'}
    
    class Intersection(bpy.types.Operator):
    
        bl_idname = 'curvetools.bezier_cad_intersection'
    
        bl_description = bl_label = 'Intersection'
        bl_options = {'REGISTER', 'UNDO'}
    
        @classmethod
        def poll(cls, context):
    
            return util.Selected1OrMoreCurves()
    
    
        def execute(self, context):
            segments = internal.bezierSegments(bpy.context.object.data.splines, True)
            if len(segments) < 2:
                self.report({'WARNING'}, 'Invalid selection')
                return {'CANCELLED'}
    
            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 util.Selected1OrMoreCurves()
    
    
        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 = 'curvetools.bezier_cad_merge_ends'
    
        bl_description = bl_label = 'Merge Ends'
        bl_options = {'REGISTER', 'UNDO'}
    
    
        max_dist: bpy.props.FloatProperty(name='Distance', description='Threshold of the maximum distance at which two control points are merged', unit='LENGTH', min=0.0, default=0.1)
    
    
        @classmethod
        def poll(cls, context):
    
            return util.Selected1OrMoreCurves()
    
    
        def execute(self, context):
    
            splines = [spline for spline in internal.getSelectedSplines(True, False) if spline.use_cyclic_u == False]
    
            while len(splines) > 0:
                spline = splines.pop()
                closest_pair = ([spline, spline], [spline.bezier_points[0], spline.bezier_points[-1]], [False, True])
                min_dist = (spline.bezier_points[0].co-spline.bezier_points[-1].co).length
                for other_spline in splines:
                    for j in range(-1, 1):
                        for i in range(-1, 1):
                            dist = (spline.bezier_points[i].co-other_spline.bezier_points[j].co).length
                            if min_dist > dist:
                                min_dist = dist
                                closest_pair = ([spline, other_spline], [spline.bezier_points[i], other_spline.bezier_points[j]], [i == -1, j == -1])
                if min_dist > self.max_dist:
    
                if closest_pair[0][0] != closest_pair[0][1]:
                    splines.remove(closest_pair[0][1])
                spline = internal.mergeEnds(closest_pair[0], closest_pair[1], closest_pair[2])
                if spline.use_cyclic_u == False:
                    splines.append(spline)
    
    
            return {'FINISHED'}
    
    class Subdivide(bpy.types.Operator):
    
        bl_idname = 'curvetools.bezier_cad_subdivide'
    
        bl_description = bl_label = 'Subdivide'
        bl_options = {'REGISTER', 'UNDO'}
    
        params: bpy.props.StringProperty(name='Params', default='0.25 0.5 0.75')
    
        @classmethod
        def poll(cls, context):
    
            return util.Selected1OrMoreCurves()
    
    
        def execute(self, context):
    
            current_mode = bpy.context.object.mode
    
            bpy.ops.object.mode_set(mode = 'EDIT')
    
    
            segments = internal.bezierSegments(bpy.context.object.data.splines, True)
            if len(segments) == 0:
                self.report({'WARNING'}, 'Nothing selected')
                return {'CANCELLED'}
    
            cuts = []
            for param in self.params.split(' '):
                cuts.append({'param': max(0.0, min(float(param), 1.0))})
            cuts.sort(key=(lambda cut: cut['param']))
            for segment in segments:
                segment['cuts'].extend(cuts)
            internal.subdivideBezierSegments(segments)
    
            bpy.ops.object.mode_set (mode = current_mode)
    
            return {'FINISHED'}
    
    class Array(bpy.types.Operator):
    
        bl_idname = 'curvetools.bezier_cad_array'
    
        bl_description = bl_label = 'Array'
        bl_options = {'REGISTER', 'UNDO'}
    
    
        offset: bpy.props.FloatVectorProperty(name='Offset', unit='LENGTH', description='Vector between to copies', subtype='DIRECTION', default=(0.0, 0.0, -1.0), size=3)
    
        count: bpy.props.IntProperty(name='Count', description='Number of copies', min=1, default=2)
        connect: bpy.props.BoolProperty(name='Connect', description='Concatenate individual copies', default=False)
        serpentine: bpy.props.BoolProperty(name='Serpentine', description='Switch direction of every second copy', default=False)
    
        @classmethod
        def poll(cls, context):
    
            return util.Selected1OrMoreCurves()
    
    
        def execute(self, context):
            splines = internal.getSelectedSplines(True, True)
            if len(splines) == 0:
                self.report({'WARNING'}, 'Nothing selected')
                return {'CANCELLED'}
            internal.arrayModifier(splines, self.offset, self.count, self.connect, self.serpentine)
            return {'FINISHED'}
    
    class Circle(bpy.types.Operator):
    
        bl_idname = 'curvetools.bezier_cad_circle'
    
        bl_description = bl_label = 'Circle'
        bl_options = {'REGISTER', 'UNDO'}
    
        @classmethod
        def poll(cls, context):
    
            return util.Selected1OrMoreCurves()
    
    
        def execute(self, context):
            segments = internal.bezierSegments(bpy.context.object.data.splines, True)
            if len(segments) != 1:
                self.report({'WARNING'}, 'Invalid selection')
                return {'CANCELLED'}
    
            segment = internal.bezierSegmentPoints(segments[0]['beginPoint'], segments[0]['endPoint'])
            circle = internal.circleOfBezier(segment)
            if circle == None:
                self.report({'WARNING'}, 'Not a circle')
                return {'CANCELLED'}
            bpy.context.scene.cursor.location = circle.center
            bpy.context.scene.cursor.rotation_mode = 'QUATERNION'
            bpy.context.scene.cursor.rotation_quaternion = circle.orientation.to_quaternion()
            return {'FINISHED'}
    
    class Length(bpy.types.Operator):
    
        bl_idname = 'curvetools.bezier_cad_length'
    
        bl_description = bl_label = 'Length'
    
        @classmethod
        def poll(cls, context):
    
            return util.Selected1OrMoreCurves()
    
    
        def execute(self, context):
            segments = internal.bezierSegments(bpy.context.object.data.splines, True)
            if len(segments) == 0:
                self.report({'WARNING'}, 'Nothing selected')
                return {'CANCELLED'}
    
            length = 0
            for segment in segments:
                length += internal.bezierLength(internal.bezierSegmentPoints(segment['beginPoint'], segment['endPoint']))
            self.report({'INFO'}, bpy.utils.units.to_string(bpy.context.scene.unit_settings.system, 'LENGTH', length))
            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 = [Fillet, Boolean, Intersection, HandleProjection, MergeEnds, Subdivide, Array, Circle, Length]