Skip to content
Snippets Groups Projects
add_curve_torus_knots.py 23.5 KiB
Newer Older
  • Learn to ignore specific revisions
  • # SPDX-License-Identifier: GPL-2.0-or-later
    
    Brendon Murphy's avatar
    fix
    Brendon Murphy committed
    
    
    Brendon Murphy's avatar
    fix
    Brendon Murphy committed
    bl_info = {
        "name": "Torus Knots",
    
        "author": "Marius Giurgi (DolphinDream), testscreenings",
    
        "version": (0, 3),
        "blender": (2, 80, 0),
    
    Brendon Murphy's avatar
    fix
    Brendon Murphy committed
        "location": "View3D > Add > Curve",
        "description": "Adds many types of (torus) knots",
        "warning": "",
    
        "doc_url": "{BLENDER_MANUAL_URL}/addons/add_curve/extra_objects.html",
    
    Brendon Murphy's avatar
    fix
    Brendon Murphy committed
    
    import bpy
    
    from bpy.props import (
            BoolProperty,
    
            EnumProperty,
            FloatProperty,
    
            sin, cos,
            pi, sqrt
            )
    from mathutils import (
            Vector,
            Matrix,
    
    from bpy_extras.object_utils import (
            AddObjectHelper,
            object_data_add
            )
    
    from bpy.types import Operator
    
    # greatest common denominator
    def gcd(a, b):
        if b == 0:
            return a
        else:
            return gcd(b, a % b)
    
    Brendon Murphy's avatar
    fix
    Brendon Murphy committed
    
    
    
    # #######################################################################
    # ###################### Knot Definitions ###############################
    # #######################################################################
    
        p = self.torus_p  # revolution count (around the torus center)
        q = self.torus_q  # spin count (around the torus tube)
    
    Brendon Murphy's avatar
    fix
    Brendon Murphy committed
    
    
        N = self.torus_res  # curve resolution (number of control points)
    
    # use plus options only when they are enabled
    
            u = self.torus_u  # p multiplier
            v = self.torus_v  # q multiplier
            h = self.torus_h  # height (scale along Z)
            s = self.torus_s  # torus scale (radii scale factor)
        else:  # don't use plus settings
    
        R = self.torus_R * s  # major radius (scaled)
        r = self.torus_r * s  # minor radius (scaled)
    
        # number of decoupled links when (p,q) are NOT co-primes
    
        links = gcd(p, q)  # = 1 when (p,q) are co-primes
    
        # parametrized angle increment (cached outside of the loop for performance)
        # NOTE: the total angle is divided by number of decoupled links to ensure
        #       the curve does not overlap with itself when (p,q) are not co-primes
        da = 2 * pi / links / (N - 1)
    
    Brendon Murphy's avatar
    fix
    Brendon Murphy committed
    
    
        # link phase : each decoupled link is phased equally around the torus center
        # NOTE: linkIndex value is in [0, links-1]
        linkPhase = 2 * pi / q * linkIndex  # = 0 when there is just ONE link
    
    Brendon Murphy's avatar
    fix
    Brendon Murphy committed
    
    
        # user defined phasing
    
            rPhase = self.torus_rP  # user defined revolution phase
            sPhase = self.torus_sP  # user defined spin phase
        else:  # don't use plus settings
    
    Brendon Murphy's avatar
    fix
    Brendon Murphy committed
    
    
        rPhase += linkPhase  # total revolution phase of the current link
    
    
        if DEBUG:
            print("")
            print("Link: %i of %i" % (linkIndex, links))
            print("gcd = %i" % links)
            print("p = %i" % p)
            print("q = %i" % q)
    
            print("link phase = %.2f deg" % (linkPhase * 180 / pi))
    
        # flip directions ? NOTE: flipping both is equivalent to no flip
    
        if self.flip_p:
            p *= -1
        if self.flip_q:
            q *= -1
    
        # create the 3D point array for the current link
    
        for n in range(N - 1):
            # t = 2 * pi / links * n/(N-1) with: da = 2*pi/links/(N-1) => t = n * da
    
            theta = p * t * u + rPhase  # revolution angle
            phi = q * t * v + sPhase  # spin angle
    
            x = (R + r * cos(phi)) * cos(theta)
            y = (R + r * cos(phi)) * sin(theta)
            z = r * sin(phi) * h
    
        # append 3D point
        # NOTE : the array is adjusted later as needed to 4D for POLY and NURBS
    
            newPoints.append([x, y, z])
    
    Brendon Murphy's avatar
    fix
    Brendon Murphy committed
    
        return newPoints
    
    
    # ------------------------------------------------------------------------------
    # Calculate the align matrix for the new object (based on user preferences)
    
    def align_matrix(self, context):
        if self.absolute_location:
    
            loc = Matrix.Translation(Vector((0, 0, 0)))
    
            loc = Matrix.Translation(context.scene.cursor.location)
    
    # user defined location & translation
    
        userLoc = Matrix.Translation(self.location)
        userRot = self.rotation.to_matrix().to_4x4()
    
    Brendon Murphy's avatar
    fix
    Brendon Murphy committed
    
    
        obj_align = context.preferences.edit.object_align
    
        if (context.space_data.type == 'VIEW_3D' and obj_align == 'VIEW'):
            rot = context.space_data.region_3d.view_matrix.to_3x3().inverted().to_4x4()
        else:
            rot = Matrix()
    
    
        align_matrix = userLoc @ loc @ rot @ userRot
    
    # ------------------------------------------------------------------------------
    # Set curve BEZIER handles to auto
    
    def setBezierHandles(obj, mode='AUTO'):
    
        scene = bpy.context.scene
        if obj.type != 'CURVE':
            return
    
        #scene.objects.active = obj
        #bpy.ops.object.mode_set(mode='EDIT', toggle=True)
        #bpy.ops.curve.select_all(action='SELECT')
        #bpy.ops.curve.handle_type_set(type=mode)
        #bpy.ops.object.mode_set(mode='OBJECT', toggle=True)
    
    # ------------------------------------------------------------------------------
    # Convert array of vert coordinates to points according to spline type
    
        # array for BEZIER spline output (V3)
    
        if splineType == 'BEZIER':
            for v in Verts:
                vertArray += v
    
    
        # array for non-BEZIER output (V4)
    
        else:
            for v in Verts:
                vertArray += v
                if splineType == 'NURBS':
    
                    vertArray.append(1)  # for NURBS w=1
                else:  # for POLY w=0
    
    # ------------------------------------------------------------------------------
    # Create the Torus Knot curve and object and add it to the scene
    
    Brendon Murphy's avatar
    fix
    Brendon Murphy committed
    def create_torus_knot(self, context):
    
        # pick a name based on (p,q) parameters
    
        aName = "Torus Knot %i x %i" % (self.torus_p, self.torus_q)
    
    
        curve_data = bpy.data.curves.new(name=aName, type='CURVE')
    
    
        # setup materials to be used for the TK links
    
        # create torus knot link(s)
    
            links = gcd(self.torus_p, self.torus_q)
    
            # get vertices for the current link
    
            # output splineType 'POLY' 'NURBS' or 'BEZIER'
    
            # turn verts into proper array (based on spline type)
    
            # create spline from vertArray (based on spline type)
    
            spline = curve_data.splines.new(type=splineType)
            if splineType == 'BEZIER':
    
                spline.bezier_points.add(int(len(vertArray) * 1.0 / 3 - 1))
    
                spline.bezier_points.foreach_set('co', vertArray)
    
                for point in spline.bezier_points:
                    point.handle_right_type = self.handleType
                    point.handle_left_type = self.handleType
    
                spline.points.add(int(len(vertArray) * 1.0 / 4 - 1))
    
                spline.points.foreach_set('co', vertArray)
                spline.use_endpoint_u = True
    
    
            # set a color per link
    
    Brendon Murphy's avatar
    fix
    Brendon Murphy committed
        curve_data.dimensions = '3D'
    
    Brendon Murphy's avatar
    fix
    Brendon Murphy committed
    
    
        if self.geo_surface:
            curve_data.fill_mode = 'FULL'
    
    Brendon Murphy's avatar
    fix
    Brendon Murphy committed
            curve_data.bevel_depth = self.geo_bDepth
            curve_data.bevel_resolution = self.geo_bRes
            curve_data.extrude = self.geo_extrude
    
        # set object in the scene
    
        new_obj = object_data_add(context, curve_data)  # place in active scene
    
        new_obj.select_set(True)  # set as selected
    
        new_obj.matrix_world = self.align_matrix  # apply matrix
    
    Brendon Murphy's avatar
    fix
    Brendon Murphy committed
    
    
    # ------------------------------------------------------------------------------
    # Create materials to be assigned to each TK link
    
        # some predefined colors for the torus knot links
    
        if self.colorSet == "1":  # RGBish
            colors += [[0.0, 0.0, 1.0]]
            colors += [[0.0, 1.0, 0.0]]
            colors += [[1.0, 0.0, 0.0]]
            colors += [[1.0, 1.0, 0.0]]
            colors += [[0.0, 1.0, 1.0]]
            colors += [[1.0, 0.0, 1.0]]
            colors += [[1.0, 0.5, 0.0]]
            colors += [[0.0, 1.0, 0.5]]
            colors += [[0.5, 0.0, 1.0]]
        else:  # RainBow
            colors += [[0.0, 0.0, 1.0]]
            colors += [[0.0, 0.5, 1.0]]
            colors += [[0.0, 1.0, 1.0]]
            colors += [[0.0, 1.0, 0.5]]
            colors += [[0.0, 1.0, 0.0]]
            colors += [[0.5, 1.0, 0.0]]
            colors += [[1.0, 1.0, 0.0]]
            colors += [[1.0, 0.5, 0.0]]
            colors += [[1.0, 0.0, 0.0]]
    
    Brendon Murphy's avatar
    fix
    Brendon Murphy committed
    
    
        me = curveData
        links = gcd(self.torus_p, self.torus_q)
    
        for i in range(links):
            matName = "TorusKnot-Link-%i" % i
            matListNames = bpy.data.materials.keys()
    
            # create the material
    
                if DEBUG:
                    print("Creating new material : %s" % matName)
    
                if DEBUG:
                    print("Material %s already exists" % matName)
    
    Brendon Murphy's avatar
    fix
    Brendon Murphy committed
    
    
            # set material color
    
            if self.options_plus and self.random_colors:
    
                mat.diffuse_color = (random(), random(), random(), 1.0)
    
                cID = i % (len(colors))  # cycle through predefined colors
    
                mat.diffuse_color = (*colors[cID], 1.0)
    
                mat.diffuse_color = (mat.diffuse_color[0] * self.saturation, mat.diffuse_color[1] * self.saturation, mat.diffuse_color[2] * self.saturation, 1.0)
    
                mat.diffuse_color = (mat.diffuse_color[0] * 0.75, mat.diffuse_color[1] * 0.75, mat.diffuse_color[2] * 0.75, 1.0)
    
    # ------------------------------------------------------------------------------
    # Main Torus Knot class
    
    class torus_knot_plus(Operator, AddObjectHelper):
    
    Brendon Murphy's avatar
    fix
    Brendon Murphy committed
        bl_idname = "curve.torus_knot_plus"
        bl_label = "Torus Knot +"
        bl_options = {'REGISTER', 'UNDO', 'PRESET'}
    
        bl_description = "Adds many types of tours knots"
        bl_context = "object"
    
        def mode_update_callback(self, context):
    
            # keep the equivalent radii sets (R,r)/(eR,iR) in sync
    
            if self.mode == 'EXT_INT':
                self.torus_eR = self.torus_R + self.torus_r
                self.torus_iR = self.torus_R - self.torus_r
    
    
        # align_matrix for the invoke
    
        options_plus : BoolProperty(
    
                name="Extra Options",
                default=False,
                description="Show more options (the plus part)",
                )
    
        absolute_location : BoolProperty(
    
                name="Absolute Location",
    
                default=False,
                description="Set absolute location instead of relative to 3D cursor",
                )
    
        use_colors : BoolProperty(
    
                name="Use Colors",
                default=False,
                description="Show torus links in colors",
                )
    
        colorSet : EnumProperty(
    
                items=(('1', "RGBish", "RGBsish ordered colors"),
                        ('2', "Rainbow", "Rainbow ordered colors")),
    
        random_colors : BoolProperty(
    
                name="Randomize Colors",
                default=False,
                description="Randomize link colors",
                )
    
        saturation : FloatProperty(
    
                name="Saturation",
                default=0.75,
                min=0.0, max=1.0,
                description="Color saturation",
                )
    
        geo_surface : BoolProperty(
    
                name="Surface",
                default=True,
                description="Create surface",
                )
    
        geo_bDepth : FloatProperty(
    
                name="Bevel Depth",
                default=0.04,
                min=0, soft_min=0,
                description="Bevel Depth",
                )
    
        geo_bRes : IntProperty(
    
                name="Bevel Resolution",
                default=2,
                min=0, soft_min=0,
                max=5, soft_max=5,
                description="Bevel Resolution"
                )
    
        geo_extrude : FloatProperty(
    
                name="Extrude",
                default=0.0,
                min=0, soft_min=0,
                description="Amount of curve extrusion"
                )
    
        geo_offset : FloatProperty(
    
                name="Offset",
                default=0.0,
                min=0, soft_min=0,
                description="Offset the surface relative to the curve"
                )
    
        # TORUS KNOT Options
    
        torus_p : IntProperty(
    
                name="p",
                default=2,
                min=1, soft_min=1,
    
                description="Number of Revolutions around the torus hole before closing the knot"
    
        torus_q : IntProperty(
    
                name="q",
                default=3,
                min=1, soft_min=1,
    
                description="Number of Spins through the torus hole before closing the knot"
    
        flip_p : BoolProperty(
    
                name="Flip p",
                default=False,
    
                description="Flip Revolution direction"
    
        flip_q : BoolProperty(
    
                name="Flip q",
                default=False,
    
                description="Flip Spin direction"
    
        multiple_links : BoolProperty(
    
                name="Multiple Links",
                default=True,
    
                description="Generate all links or just one link when q and q are not co-primes"
    
        torus_u : IntProperty(
    
                name="Rev. Multiplier",
    
                default=1,
                min=1, soft_min=1,
    
                description="Revolutions Multiplier"
    
        torus_v : IntProperty(
    
                name="Spin Multiplier",
    
                default=1,
                min=1, soft_min=1,
    
                description="Spin multiplier"
    
        torus_rP : FloatProperty(
    
                name="Revolution Phase",
                default=0.0,
                min=0.0, soft_min=0.0,
                description="Phase revolutions by this radian amount"
                )
    
        torus_sP : FloatProperty(
    
                name="Spin Phase",
                default=0.0,
                min=0.0, soft_min=0.0,
                description="Phase spins by this radian amount"
                )
    
        # TORUS DIMENSIONS options
    
        mode : EnumProperty(
    
                name="Torus Dimensions",
                items=(("MAJOR_MINOR", "Major/Minor",
                        "Use the Major/Minor radii for torus dimensions."),
                        ("EXT_INT", "Exterior/Interior",
                        "Use the Exterior/Interior radii for torus dimensions.")),
                update=mode_update_callback,
                )
    
        torus_R : FloatProperty(
    
                name="Major Radius",
                min=0.00, max=100.0,
                default=1.0,
                subtype='DISTANCE',
                unit='LENGTH',
                description="Radius from the torus origin to the center of the cross section"
                )
    
        torus_r : FloatProperty(
    
                name="Minor Radius",
                min=0.00, max=100.0,
                default=.25,
                subtype='DISTANCE',
                unit='LENGTH',
                description="Radius of the torus' cross section"
                )
    
        torus_iR : FloatProperty(
    
                name="Interior Radius",
                min=0.00, max=100.0,
                default=.75,
                subtype='DISTANCE',
                unit='LENGTH',
                description="Interior radius of the torus (closest to the torus center)"
                )
    
        torus_eR : FloatProperty(
    
                name="Exterior Radius",
                min=0.00, max=100.0,
                default=1.25,
                subtype='DISTANCE',
                unit='LENGTH',
                description="Exterior radius of the torus (farthest from the torus center)"
                )
    
        torus_s : FloatProperty(
    
                name="Scale",
                min=0.01, max=100.0,
                default=1.00,
                description="Scale factor to multiply the radii"
                )
    
        torus_h : FloatProperty(
    
                name="Height",
                default=1.0,
                min=0.0, max=100.0,
                description="Scale along the local Z axis"
                )
    
        torus_res : IntProperty(
    
                name="Curve Resolution",
                default=100,
                min=3, soft_min=3,
                description="Number of control vertices in the curve"
                )
    
        segment_res : IntProperty(
    
                name="Segment Resolution",
                default=12,
                min=1, soft_min=1,
                description="Curve subdivisions per segment"
                )
    
                ('POLY', "Poly", "Poly type"),
                ('NURBS', "Nurbs", "Nurbs type"),
                ('BEZIER', "Bezier", "Bezier type")]
    
        outputType : EnumProperty(
    
                name="Output splines",
                default='BEZIER',
                description="Type of splines to output",
                items=SplineTypes,
                )
    
                ('VECTOR', "Vector", "Bezier Handles type - Vector"),
    
                ('AUTO', "Auto", "Bezier Handles type - Automatic"),
    
        handleType : EnumProperty(
    
                default='AUTO',
    
                items=bezierHandles,
                description="Bezier handle type",
                )
    
        adaptive_resolution : BoolProperty(
    
                name="Adaptive Resolution",
                default=False,
                description="Auto adjust curve resolution based on TK length",
                )
    
        edit_mode : BoolProperty(
                name="Show in edit mode",
                default=True,
                description="Show in edit mode"
                )
    
    Brendon Murphy's avatar
    fix
    Brendon Murphy committed
    
        def draw(self, context):
            layout = self.layout
    
    
            # extra parameters toggle
            layout.prop(self, "options_plus")
    
    Brendon Murphy's avatar
    fix
    Brendon Murphy committed
    
    
            # TORUS KNOT Parameters
    
            col = layout.column()
            col.label(text="Torus Knot Parameters:")
    
    Brendon Murphy's avatar
    fix
    Brendon Murphy committed
            box = layout.box()
    
            split = box.split(factor=0.85, align=True)
    
            split.prop(self, "torus_p", text="Revolutions")
            split.prop(self, "flip_p", toggle=True, text="",
    
                       icon='ARROW_LEFTRIGHT')
    
            split = box.split(factor=0.85, align=True)
    
            split.prop(self, "torus_q", text="Spins")
            split.prop(self, "flip_q", toggle=True, text="",
    
                       icon='ARROW_LEFTRIGHT')
    
    
            links = gcd(self.torus_p, self.torus_q)
            info = "Multiple Links"
    
            if links > 1:
                info += "  ( " + str(links) + " )"
    
    Brendon Murphy's avatar
    fix
    Brendon Murphy committed
            if self.options_plus:
    
                col = box.column(align=True)
                col.prop(self, "torus_u")
                col.prop(self, "torus_v")
    
                col = box.column(align=True)
                col.prop(self, "torus_rP")
                col.prop(self, "torus_sP")
    
            # TORUS DIMENSIONS options
    
            col = layout.column(align=True)
            col.label(text="Torus Dimensions:")
            box = layout.box()
            col = box.column(align=True)
            col.row().prop(self, "mode", expand=True)
    
    
            if self.mode == "MAJOR_MINOR":
    
                col = box.column(align=True)
                col.prop(self, "torus_R")
                col.prop(self, "torus_r")
    
            else:  # EXTERIOR-INTERIOR
    
                col = box.column(align=True)
                col.prop(self, "torus_eR")
                col.prop(self, "torus_iR")
    
            if self.options_plus:
                box = box.box()
    
                col = box.column(align=True)
                col.prop(self, "torus_s")
                col.prop(self, "torus_h")
    
            col = layout.column(align=True)
            col.label(text="Curve Options:")
            box = layout.box()
    
            col = box.column()
            col.label(text="Output Curve Type:")
    
            col.row().prop(self, "outputType", expand=True)
    
    Brendon Murphy's avatar
    fix
    Brendon Murphy committed
    
    
            depends = box.column()
    
            depends.prop(self, "torus_res")
            # deactivate the "curve resolution" if "adaptive resolution" is enabled
    
            box.prop(self, "adaptive_resolution")
            box.prop(self, "segment_res")
    
    Brendon Murphy's avatar
    fix
    Brendon Murphy committed
            col = layout.column()
            col.label(text="Geometry Options:")
            box = layout.box()
    
            box.prop(self, "geo_surface")
    
                col = box.column(align=True)
                col.prop(self, "geo_bDepth")
                col.prop(self, "geo_bRes")
    
    Brendon Murphy's avatar
    fix
    Brendon Murphy committed
    
    
                col = box.column(align=True)
                col.prop(self, "geo_extrude")
                col.prop(self, "geo_offset")
    
            # COLOR options
    
    Brendon Murphy's avatar
    fix
    Brendon Murphy committed
            col = layout.column()
    
            col.label(text="Color Options:")
            box = layout.box()
    
            box.prop(self, "use_colors")
    
            if self.use_colors and self.options_plus:
                box = box.box()
    
                box.prop(self, "colorSet")
                box.prop(self, "random_colors")
                box.prop(self, "saturation")
    
            col = layout.column()
            col.row().prop(self, "edit_mode", expand=True)
    
            col = layout.column()
            col.label(text="Transform Options:")
            box = col.box()
    
            box.prop(self, "location")
            box.prop(self, "absolute_location")
            box.prop(self, "rotation")
    
    Brendon Murphy's avatar
    fix
    Brendon Murphy committed
    
        @classmethod
        def poll(cls, context):
    
            return context.scene is not None
    
    Brendon Murphy's avatar
    fix
    Brendon Murphy committed
    
        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
    
                # adjust the equivalent radii pair : (R,r) <=> (eR,iR)
    
                self.torus_R = (self.torus_eR + self.torus_iR) * 0.5
                self.torus_r = (self.torus_eR - self.torus_iR) * 0.5
    
                # adjust curve resolution automatically based on (p,q,R,r) values
    
                p = self.torus_p
                q = self.torus_q
                R = self.torus_R
                r = self.torus_r
    
    
                # get an approximate length of the whole TK curve
                # upper bound approximation
                maxTKLen = 2 * pi * sqrt(p * p * (R + r) * (R + r) + q * q * r * r)
                # lower bound approximation
                minTKLen = 2 * pi * sqrt(p * p * (R - r) * (R - r) + q * q * r * r)
                avgTKLen = (minTKLen + maxTKLen) / 2  # average approximation
    
    
                if DEBUG:
                    print("Approximate average TK length = %.2f" % avgTKLen)
    
                # x N factor = control points per unit length
                self.torus_res = max(3, avgTKLen / links * 8)
    
            # update align matrix
    
            self.align_matrix = align_matrix(self, context)
    
    
    Brendon Murphy's avatar
    fix
    Brendon Murphy committed
            create_torus_knot(self, context)
    
            if use_enter_edit_mode:
                bpy.ops.object.mode_set(mode = 'EDIT')
    
            # restore pre operator state
            bpy.context.preferences.edit.use_enter_edit_mode = use_enter_edit_mode
    
    Brendon Murphy's avatar
    fix
    Brendon Murphy committed
    
    
            if self.edit_mode:
                bpy.ops.object.mode_set(mode = 'EDIT')
            else:
                bpy.ops.object.mode_set(mode = 'OBJECT')
    
    
    Brendon Murphy's avatar
    fix
    Brendon Murphy committed
            return {'FINISHED'}
    
    
    # Register
    classes = [
        torus_knot_plus
    ]
    
    def register():
        from bpy.utils import register_class
        for cls in classes:
            register_class(cls)
    
    def unregister():
        from bpy.utils import unregister_class
        for cls in reversed(classes):
            unregister_class(cls)
    
    if __name__ == "__main__":