Skip to content
Snippets Groups Projects
add_curve_celtic_links.py 9.38 KiB
Newer Older
  • Learn to ignore specific revisions
  • # SPDX-License-Identifier: MIT
    # Copyright 2013 Adam Newgas
    
    
    # Blender plugin for generating celtic knot curves from 3d meshes
    
    bl_info = {
        "name": "Celtic Knot",
        "description": "",
        "author": "Adam Newgas",
    
        "version": (0, 1, 3),
        "blender": (2, 80, 0),
    
        "location": "View3D > Add > Curve",
        "warning": "",
    
        "doc_url": "https://github.com/BorisTheBrave/celtic-knot/wiki",
        "category": "Add Curve",
    }
    
    
    import bpy
    import bmesh
    
    from bpy.types import Operator
    from bpy.props import (
            EnumProperty,
            FloatProperty,
            )
    
    from collections import defaultdict
    
    from math import (
            pi, sin,
            cos,
            )
    
    class CelticKnotOperator(Operator):
    
        bl_idname = "curve.celtic_links"
        bl_label = "Celtic Links"
    
        bl_description = "Select a low poly Mesh Object to cover with Knitted Links"
    
        bl_options = {'REGISTER', 'UNDO', 'PRESET'}
    
    
        weave_up : FloatProperty(
    
                name="Weave Up",
                description="Distance to shift curve upwards over knots",
                subtype="DISTANCE",
                unit="LENGTH"
                )
    
        weave_down : FloatProperty(
    
                name="Weave Down",
                description="Distance to shift curve downward under knots",
                subtype="DISTANCE",
                unit="LENGTH"
                )
        handle_types = [
                ('ALIGNED', "Aligned", "Points at a fixed crossing angle"),
                ('AUTO', "Auto", "Automatic control points")
                ]
    
        handle_type : EnumProperty(
    
                items=handle_types,
                name="Handle Type",
                description="Controls what type the bezier control points use",
                default='AUTO'
                )
    
    
        handle_type_map = {"AUTO": "AUTOMATIC", "ALIGNED": "ALIGNED"}
    
        crossing_angle : FloatProperty(
    
                name="Crossing Angle",
                description="Aligned only: the angle between curves in a knot",
                default=pi / 4,
                min=0, max=pi / 2,
                subtype="ANGLE",
                unit="ROTATION"
                )
    
        crossing_strength : FloatProperty(
    
                name="Crossing Strength",
    
                description="Aligned only: strength of bezier control points",
    
                soft_min=0,
                subtype="DISTANCE",
                unit="LENGTH"
                )
    
        geo_bDepth : FloatProperty(
    
                name="Bevel Depth",
                default=0.04,
                min=0, soft_min=0,
                description="Bevel Depth",
                )
    
        @classmethod
        def poll(cls, context):
            ob = context.active_object
    
            return ((ob is not None) and (ob.mode == "OBJECT") and
                    (ob.type == "MESH") and (context.mode == "OBJECT"))
    
        def draw(self, context):
            layout = self.layout
            layout.prop(self, "handle_type")
    
            col = layout.column(align=True)
            col.prop(self, "weave_up")
            col.prop(self, "weave_down")
    
            col = layout.column(align=True)
            col.active = False if self.handle_type == 'AUTO' else True
            col.prop(self, "crossing_angle")
            col.prop(self, "crossing_strength")
    
            layout.prop(self, "geo_bDepth")
    
    
        def execute(self, context):
    
            # turn off 'Enter Edit Mode'
            use_enter_edit_mode = bpy.context.preferences.edit.use_enter_edit_mode
            bpy.context.preferences.edit.use_enter_edit_mode = False
    
            # Cache some values
            s = sin(self.crossing_angle) * self.crossing_strength
            c = cos(self.crossing_angle) * self.crossing_strength
            handle_type = self.handle_type
            weave_up = self.weave_up
            weave_down = self.weave_down
    
            # Create the new object
            orig_obj = obj = context.active_object
            curve = bpy.data.curves.new("Celtic", "CURVE")
            curve.dimensions = "3D"
            curve.twist_mode = "MINIMUM"
            curve.fill_mode = "FULL"
            curve.bevel_depth = 0.015
            curve.extrude = 0.003
            curve.bevel_resolution = 4
            obj = obj.data
            midpoints = []
    
            # Compute all the midpoints of each edge
            for e in obj.edges.values():
                v1 = obj.vertices[e.vertices[0]]
                v2 = obj.vertices[e.vertices[1]]
                m = (v1.co + v2.co) / 2.0
                midpoints.append(m)
    
            bm = bmesh.new()
            bm.from_mesh(obj)
            # Stores which loops the curve has already passed through
            loops_entered = defaultdict(lambda: False)
            loops_exited = defaultdict(lambda: False)
            # Loops on the boundary of a surface
    
            def ignorable_loop(loop):
                return len(loop.link_loops) == 0
    
            # Starting at loop, build a curve one vertex at a time
            # until we start where we came from
            # Forward means that for any two edges the loop crosses
            # sharing a face, it is passing through in clockwise order
            # else anticlockwise
    
            def make_loop(loop, forward):
                current_spline = curve.splines.new("BEZIER")
                current_spline.use_cyclic_u = True
                first = True
                # Data for the spline
                # It's faster to store in an array and load into blender
                # at once
                cos = []
                handle_lefts = []
                handle_rights = []
                while True:
                    if forward:
                        if loops_exited[loop]:
                            break
                        loops_exited[loop] = True
                        # Follow the face around, ignoring boundary edges
                        while True:
                            loop = loop.link_loop_next
                            if not ignorable_loop(loop):
                                break
    
                        assert loops_entered[loop] is False
    
                        loops_entered[loop] = True
                        v = loop.vert.index
                        prev_loop = loop
                        # Find next radial loop
                        assert loop.link_loops[0] != loop
                        loop = loop.link_loops[0]
                        forward = loop.vert.index == v
                    else:
                        if loops_entered[loop]:
                            break
                        loops_entered[loop] = True
                        # Follow the face around, ignoring boundary edges
                        while True:
                            v = loop.vert.index
                            loop = loop.link_loop_prev
                            if not ignorable_loop(loop):
                                break
    
                        assert loops_exited[loop] is False
    
                        loops_exited[loop] = True
                        prev_loop = loop
                        # Find next radial loop
                        assert loop.link_loops[-1] != loop
                        loop = loop.link_loops[-1]
                        forward = loop.vert.index == v
    
                        current_spline.bezier_points.add(1)
    
                    first = False
                    midpoint = midpoints[loop.edge.index]
                    normal = loop.calc_normal() + prev_loop.calc_normal()
                    normal.normalize()
                    offset = weave_up if forward else weave_down
                    midpoint = midpoint + offset * normal
                    cos.extend(midpoint)
    
                    if handle_type != "AUTO":
                        tangent = loop.link_loop_next.vert.co - loop.vert.co
                        tangent.normalize()
                        binormal = normal.cross(tangent).normalized()
                        if not forward:
                            tangent *= -1
                        s_binormal = s * binormal
                        c_tangent = c * tangent
                        handle_left = midpoint - s_binormal - c_tangent
                        handle_right = midpoint + s_binormal + c_tangent
                        handle_lefts.extend(handle_left)
                        handle_rights.extend(handle_right)
    
                points = current_spline.bezier_points
                points.foreach_set("co", cos)
                if handle_type != "AUTO":
                    points.foreach_set("handle_left", handle_lefts)
                    points.foreach_set("handle_right", handle_rights)
    
            # Attempt to start a loop at each untouched loop in the entire mesh
            for face in bm.faces:
                for loop in face.loops:
                    if ignorable_loop(loop):
                        continue
                    if not loops_exited[loop]:
                        make_loop(loop, True)
                    if not loops_entered[loop]:
                        make_loop(loop, False)
    
            # Create an object from the curve
            from bpy_extras import object_utils
            object_utils.object_data_add(context, curve, operator=None)
            # Set the handle type (this is faster than setting it pointwise)
            bpy.ops.object.editmode_toggle()
            bpy.ops.curve.select_all(action="SELECT")
            bpy.ops.curve.handle_type_set(type=self.handle_type_map[handle_type])
            # Some blender versions lack the default
            bpy.ops.curve.radius_set(radius=1.0)
            bpy.ops.object.editmode_toggle()
            # Restore active selection
            curve_obj = context.active_object
    
    
            # apply the bevel setting since it was unused
            try:
                curve_obj.data.bevel_depth = self.geo_bDepth
            except:
                pass
    
            bpy.context.view_layer.objects.active = orig_obj
    
            # restore pre operator state
            bpy.context.preferences.edit.use_enter_edit_mode = use_enter_edit_mode
    
    
            return {'FINISHED'}
    
    
    def register():
    
        bpy.utils.register_class(CelticKnotOperator)
    
        bpy.utils.unregister_class(CelticKnotOperator)
    
    
    
    if __name__ == "__main__":
        register()