Addon for spiral around a toroid shape?

i’m looking for some addon to add a spiral on a toroidal shape

anyone has an addon for this ?

should be able to adjust the number of turns and curve bevel size

thanks
happy bl

Interesting problem. I’ll post the code I have so far at the bottom, but it needs alot of cleanup and a few features added to be more useful.

Basic torus:

Uniform shapes allow fairly quick and smooth creation of a bezier curve wrapping around the torus. You will notice a twist issue in the curve at the end point along the x-axis.

basic torus reduced

Tall torus with subsurf modifier

Because this script is creating a bezier path through the center of the torus (to be used in a follow path modifier) and calculating how many points are on a sample slice through the torus the additional points can quickly cause blender to become unresponsive during some of the operations. You can disable the modifier in viewport (without removing or applying it) to aid in response times.

tall torus with subsurf reduced

Mishapen torus

As mentioned above a bezier follow path is created in the process prior to the final bezier coil so odd shapes are followed reasonably accurately.

ugly torus

Cube with hole cut out and bevel modifier

Due to the fact that (at the moment) when making the final coil path (add coil button) each wrap is generated in an even circular pattern (instead of adding points at the specific torus loop edges) sharp corners are not currently accurately followed.
This would require storing the angle of each point in the sample slice plane from the center of the slice plane and when creating the coil rotate by that specific angle list.

cube

Limitations

Currently there are no checks to verify the selected “torus” object actually has a hole through it.

The “torus” object must currently have the hole through the z-axis. (This would be a good option for someone to add requiring additional property in property group and modifying most if not all operations to check which axis rotation should occur through)

The “torus” object must have a unity world_matrix. (There is an option/operator to set force the object matrix if not already but this will reset scaling, rotation, translation of the object and is not enforced. The other operators will be allowed but the results will be broken.)

Code

For anyone who wants to play with it or clean it up / add functionality / fix improve etc. as I will likely not take this further.

bl_info = {
    "name": "Test Addon",
    "author": "Nezumi",
    "version": (0, 0, 1),
    "blender": (3, 1, 0),
    "location": "View3D > UI > Test Panel",
    "description": "",
    "warning": "",
    "wiki_url": "",
    "category": "Development",
    "support": "TESTING"
}

import bpy
from mathutils import Vector, Euler, Matrix
from math import radians
import bmesh


def point_filter(self, object):
    return object.type == 'MESH'


class TEST_PG_props(bpy.types.PropertyGroup):
    my_obj: bpy.props.PointerProperty(
        type=bpy.types.Object, poll=point_filter
    )
    my_wraps: bpy.props.IntProperty(
        name="wraps",
        description="# wraps for 360 deg of torus",
        default=4,
        min=1,
        soft_max=100,
    )
    pts_per_wrap: bpy.props.IntProperty(
        name="points per wrap",
        description="# points in cross section of torus",
        default=4,
        min=1,
        soft_max=100,
    )
    coil_radius: bpy.props.FloatProperty(
        name="diameter",
        description="diameter of wire coil",
        default=0.1,
        min=0.0,
        soft_max=2,
    )


def recurLayerCollection(layerColl, collName):
    found = None
    if (layerColl.name == collName):
        return layerColl
    for layer in layerColl.children:
        found = recurLayerCollection(layer, collName)
        if found:
            return found


def ensure_collection(context, master_coll, target_coll_name):
    target_coll = recurLayerCollection(master_coll, target_coll_name)
    if target_coll:
        context.view_layer.active_layer_collection = target_coll
    else:
        # create a new collection in the master scene collection
        target_coll = bpy.data.collections.new(target_coll_name)
        context.scene.collection.children.link(target_coll)
        target_coll = recurLayerCollection(master_coll, target_coll_name)


def make_mesh(context, name="", coll_name="", verts=[], edges=[], faces=[]):
    my_props = context.scene.test_pg
    mesh_data = bpy.data.meshes.new(name)
    mesh_obj = bpy.data.objects.new(mesh_data.name, mesh_data)
    mesh_obj.location = Vector(my_props.my_obj.location)
    if coll_name:
        coll = bpy.data.collections.get(coll_name)
    else:
        coll = context.scene.collection
    coll.objects.link(mesh_obj)
    context.view_layer.objects.active = mesh_obj
    mesh_data.from_pydata(verts, edges, faces)
    return mesh_obj


def make_slice_plane(context):
    act_obj = context.view_layer.objects.active
    my_props = context.scene.test_pg
    coll = bpy.data.collections.get(f'{my_props.my_obj.name}_scratch')

    # make mesh slice plane
    bbox_x = []
    bbox_y = []
    bbox_z = []

    for i in my_props.my_obj.bound_box:
        bbox_x.append(i[0])
        bbox_y.append(i[1])
        bbox_z.append(i[2])
    my_x = max(abs(i) for i in bbox_x)
    my_y = max(abs(i) for i in bbox_y)
    my_z = max(abs(i) for i in bbox_z)
    ref_plane_w = max([my_x, my_y]) * 2
    ref_plane_h = (my_z + 0.1) * 2
    vts = [
        (0, 0, ref_plane_h),
        (0, 0, -ref_plane_h),
        (ref_plane_w, 0, ref_plane_h),
        (ref_plane_w, 0, -ref_plane_h),
        ]
    eds = []
    fcs = [
        [0, 1, 3, 2, ],
        ]

    s_plane = make_mesh(
        context,
        name=f'{my_props.my_obj.name}_slice_plane',
        coll_name=f'{my_props.my_obj.name}_scratch',
        verts=vts,
        edges=eds,
        faces=fcs)
    mod = s_plane.modifiers.new('Boolean', 'BOOLEAN')
    mod.operation = 'INTERSECT'
    mod.object = my_props.my_obj
    mod.solver = 'FAST'

    # create single wrap for vert count
    coil_ref = s_plane.copy()
    coil_ref.data = s_plane.data.copy()
    coil_ref.name = f'{my_props.my_obj.name}_coil_ref'
    coil_ref.data.name = f'{my_props.my_obj.name}_coil_ref'
    coll.objects.link(coil_ref)

    bpy.ops.object.select_all(action='DESELECT')
    coil_ref.select_set(True)
    context.view_layer.objects.active = coil_ref

    bpy.ops.object.modifier_apply(modifier="Boolean")
    bpy.ops.object.convert(target='CURVE')
    coil_ref.select_set(False)

    if act_obj:
        context.view_layer.objects.active = act_obj
        act_obj.select_set(True)


def key_rotation(context, obj, axis=0, step=1):
    rot = 0
    for frm in range(context.scene.frame_end):
        context.scene.frame_set(frm+1)
        obj.rotation_euler[axis] = rot
        obj.keyframe_insert(data_path="rotation_euler", frame=frm+1)
        rot += radians(360/step)


def make_bez_path(context, bez_name="", duration=0, verts=[]):
    my_props = context.scene.test_pg
    my_curve_data = bpy.data.curves.new(bez_name, 'CURVE')
    my_curve_data.dimensions = '3D'
    my_curve_data.resolution_u = 1
    my_curve_data.use_path = True
    my_curve_data.path_duration = duration
    bez = my_curve_data.splines.new('BEZIER')
    bez.bezier_points.add(len(verts)-1)
    for i in range(len(verts)):
        x, y, z = verts[i]
        bez.bezier_points[i].co = (x, y, z)
        bez.bezier_points[i].handle_left_type = 'AUTO'
        bez.bezier_points[i].handle_right_type = 'AUTO'
    bez.use_cyclic_u = True
    my_curve_obj = bpy.data.objects.new(bez_name, my_curve_data)
    my_curve_obj.location = Vector(my_props.my_obj.location)
    return my_curve_obj


def rotate_slice_plane(context):
    act_obj = context.view_layer.objects.active
    my_props = context.scene.test_pg
    coll = bpy.data.collections[f'{my_props.my_obj.name}_scratch']
    coords = []
    ref_coil = bpy.data.objects[f'{my_props.my_obj.name}_coil_ref']
    pts = my_props.my_wraps * len(ref_coil.data.splines[0].points)
    s_plane = bpy.data.objects[f'{my_props.my_obj.name}_slice_plane']
    x, y, z = (radians(0), radians(0), radians(360/pts))
    R = Euler((x, y, z)).to_matrix().to_4x4()

    context.scene.frame_start = 1
    context.scene.frame_end = pts
    cur_frame = context.scene.frame_start
    rot = 0

    # keyframe slice_plane
    key_rotation(context, s_plane, axis=2, step=pts)

    # calculate ctr of torus at slice_plane
    depsgraph = bpy.context.evaluated_depsgraph_get()
    for frm in range(context.scene.frame_end):
        context.scene.frame_set(frm+1)
        context.view_layer.update()
        ob = s_plane.evaluated_get(depsgraph)
        local_bbox_center = 0.125 * sum(
            (Vector(b) for b in ob.bound_box),
            Vector())
        global_bbox_center = ob.matrix_world @ local_bbox_center
        coords.append(global_bbox_center)
        ob.matrix_world = R @ ob.matrix_world

    # make bezier curve as path at ctr of torus
    my_curve_obj = make_bez_path(
        context,
        bez_name=f'{my_props.my_obj.name}_torus_path_ctr',
        duration=pts,
        verts=coords
        )
    coll.objects.link(my_curve_obj)
    if act_obj:
        context.view_layer.objects.active = act_obj
        act_obj.select_set(True)


def add_line_segment(context):
    act_obj = context.view_layer.objects.active
    my_props = context.scene.test_pg
    my_curve_obj = context.view_layer.objects[
        f'{my_props.my_obj.name}_torus_path_ctr']
    ref_coil = context.view_layer.objects[f'{my_props.my_obj.name}_coil_ref']
    coll = bpy.data.collections[f'{my_props.my_obj.name}_scratch']

    # add line segment (2 points from ctr torus extended on x-axis 0.01)
    vts = [
        (0.0, 0.0, 0.0),
        (0.1, 0.0, 0.0),
        ]
    eds = [
        [0, 1, ],
        ]
    fcs = [
        ]

    coil_path = make_mesh(
        context,
        name=f'{my_props.my_obj.name}_coil_path',
        coll_name=f'{my_props.my_obj.name}_scratch',
        verts=vts,
        edges=eds,
        faces=fcs)

    mod = coil_path.modifiers.new('Shrinkwrap', 'SHRINKWRAP')
    mod.wrap_method = 'PROJECT'
    mod.wrap_mode = 'OUTSIDE_SURFACE'
    mod.target = my_props.my_obj
    mod.offset = my_props.coil_radius

    coil_path_const = coil_path.constraints.new('FOLLOW_PATH')
    coil_path_const.target = my_curve_obj
    coil_path_const.use_curve_follow = True

    # animate follow path constraint
    bpy.ops.object.select_all(action='DESELECT')
    coil_path.select_set(True)
    context.view_layer.objects.active = coil_path
    bpy.ops.constraint.followpath_path_animate(
        constraint="Follow Path",
        owner='OBJECT')
    coil_path.select_set(False)

    # rotate empty around follow axis (y-axis)
    key_rotation(
        context,
        coil_path,
        axis=1,
        step=len(ref_coil.data.splines[0].points))
    if act_obj:
        context.view_layer.objects.active = act_obj
        act_obj.select_set(True)


def add_coil(context):
    act_obj = context.view_layer.objects.active
    my_props = context.scene.test_pg
    coll_name = f'{my_props.my_obj.name}_scratch'
    coll = bpy.data.collections[coll_name]
    coil_path = coll.objects[f'{my_props.my_obj.name}_coil_path']
    ctr_path = coll.objects[f'{my_props.my_obj.name}_torus_path_ctr']
    bm = bmesh.new()

    # calculate coil path around torus
    coords = []
    depsgraph = bpy.context.evaluated_depsgraph_get()
    for frm in range(context.scene.frame_end):
        context.scene.frame_set(frm+1)
        bm.from_object(coil_path, depsgraph)
        rme = bpy.data.meshes.new("Rib")
        bm.to_mesh(rme)
        coords.append(coil_path.matrix_world @ rme.vertices[1].co)
        bm.clear()
    bpy.ops.outliner.orphans_purge()

    # make bezier curve as coil path around torus
    my_coil_obj = make_bez_path(
        context,
        bez_name=f'{my_props.my_obj.name}_coil',
        verts=coords
        )
    my_coil_obj.data.resolution_u = 4
    my_coil_obj.data.bevel_depth = my_props.coil_radius / 2
    coll.objects.link(my_coil_obj)
    if act_obj:
        context.view_layer.objects.active = act_obj
        act_obj.select_set(True)


class OBJECT_OT_make_slice_plane(bpy.types.Operator):
    bl_idname = 'object.make_slice_plane'
    bl_label = "Make Slice Plane"

    @classmethod
    def poll(cls, context):
        my_props = context.scene.test_pg
        cond1 = my_props.my_obj
        return cond1

    def execute(self, context):
        my_props = context.scene.test_pg
        print(f'{self.bl_idname} button pressed')
        master_coll = context.view_layer.layer_collection
        current_coll = recurLayerCollection(
            master_coll,
            context.collection.name)
        target_coll_name = f'{my_props.my_obj.name}_scratch'
        ensure_collection(context, master_coll, target_coll_name)
        my_coll = bpy.data.collections[target_coll_name]
        if len(my_coll.objects) > 0:
            while my_coll.objects:
                bpy.data.objects.remove(my_coll.objects[0], do_unlink=True)
            bpy.ops.outliner.orphans_purge()

        make_slice_plane(context)
        return {'FINISHED'}


class OBJECT_OT_rotate_slice_plane(bpy.types.Operator):
    bl_idname = 'object.rotate_slice_plane'
    bl_label = "Rotate Slice Plane"

    @classmethod
    def poll(cls, context):
        my_props = context.scene.test_pg
        cond1 = my_props.my_obj
        return cond1

    def execute(self, context):
        my_props = context.scene.test_pg
        print(f'{self.bl_idname} button pressed')
        rotate_slice_plane(context)
        return {'FINISHED'}


class OBJECT_OT_add_line_segment(bpy.types.Operator):
    bl_idname = 'object.add_line_segment'
    bl_label = "Add line segment"

    @classmethod
    def poll(cls, context):
        my_props = context.scene.test_pg
        cond1 = my_props.my_obj
        return cond1

    def execute(self, context):
        my_props = context.scene.test_pg
        print(f'{self.bl_idname} button pressed')
        add_line_segment(context)
        return {'FINISHED'}


class OBJECT_OT_add_coil(bpy.types.Operator):
    bl_idname = 'object.add_coil'
    bl_label = "add coil"

    @classmethod
    def poll(cls, context):
        my_props = context.scene.test_pg
        cond1 = my_props.my_obj
        return cond1

    def execute(self, context):
        my_props = context.scene.test_pg
        print(f'{self.bl_idname} button pressed')
        add_coil(context)
        return {'FINISHED'}


class OBJECT_OT_apply_scale(bpy.types.Operator):
    bl_idname = 'object.apply_scale'
    bl_label = "Apply Scale"

    @classmethod
    def poll(cls, context):
        my_props = context.scene.test_pg
        cond1 = my_props.my_obj
        return cond1

    def execute(self, context):
        my_props = context.scene.test_pg
        print(f'{self.bl_idname} button pressed')
        act_obj = context.view_layer.objects.active

        bpy.ops.object.select_all(action='DESELECT')
        my_props.my_obj.select_set(True)
        context.view_layer.objects.active = my_props.my_obj
        bpy.ops.object.transform_apply(
            location=False,
            rotation=False,
            scale=True)

        context.view_layer.objects.active = act_obj
        act_obj.select_set(True)
        return {'FINISHED'}


class OBJECT_OT_reset_mw(bpy.types.Operator):
    """Reset matrix world translation, scale, rotation affected"""
    bl_idname = 'object.reset_mw'
    bl_label = "Reset matrix world"

    @classmethod
    def poll(cls, context):
        my_props = context.scene.test_pg
        cond1 = my_props.my_obj
        return cond1

    def execute(self, context):
        my_props = context.scene.test_pg
        print(f'{self.bl_idname} button pressed')
        my_props.my_obj.matrix_world = Matrix()
        return {'FINISHED'}


class VIEW3D_PT_test(bpy.types.Panel):
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = "Test Panel"
    bl_idname = "VIEW3D_PT_test_panel"
    bl_label = "Main Panel"

    def draw(self, context):
        my_props = context.scene.test_pg
        layout = self.layout
        box = layout.box()
        col = box.column(align=True)
        col.label(text="Target Torus")
        col.prop(my_props, "my_obj", text="")
        if my_props.my_obj:
            if my_props.my_obj != context.object:
                col.label(text="caution not active object")
            if my_props.my_obj.scale != Vector((1.0, 1.0, 1.0)):
                col.label(text="warning scale non unity")
                col.operator('object.apply_scale')
            if my_props.my_obj.matrix_world != Matrix():
                col.label(text="matrix world non unity")
                col.operator('object.reset_mw')
        box = layout.box()
        col = box.column(align=False)
        col.label(text="Coil wraps per 360 deg of torus")
        col.prop(my_props, "my_wraps", text="Wraps")
        col.prop(my_props, "coil_radius", text="diameter")

        try:
            coil_ref = bpy.data.objects.get(f'{my_props.my_obj.name}_coil_ref')
            multiplier = len(coil_ref.data.splines[0].points)
        except AttributeError:
            multiplier = 0
        if multiplier > 0:
            col.label(text=f'Coil Points: {multiplier * my_props.my_wraps}')
        else:
            col.label(text=f'Coil Points: not calculated')
        col.operator('object.make_slice_plane')
        col.operator('object.rotate_slice_plane')
        col.operator('object.add_line_segment')
        col.operator('object.add_coil')


classes = [
    VIEW3D_PT_test,
    TEST_PG_props,
    OBJECT_OT_apply_scale,
    OBJECT_OT_reset_mw,
    OBJECT_OT_make_slice_plane,
    OBJECT_OT_rotate_slice_plane,
    OBJECT_OT_add_line_segment,
    OBJECT_OT_add_coil,
    ]


def register():
    for cls in classes:
        bpy.utils.register_class(cls)
    bpy.types.Scene.test_pg = bpy.props.PointerProperty(
        type=TEST_PG_props)


def unregister():
    for cls in classes:
        bpy.utils.unregister_class(cls)
    del bpy.types.Scene.test_pg

if __name__ == "__main__":
    register()

thanks for trying to solve such weird shape !

i began an operator script
have to test the curve adding or mesh not certain yet how to start it

i was not really thinking in term of different shape
more the sort of torus we find on the market for electrical coils
but interesting

thanks
happy bl

Use the Curve-Extra Objects addon (included)
add a torus…
add…Curve > Curve Spirals > Torus
Adjust and /or use a shrinkwrap modifier…( make sure to click on the tab in the shrinkwrap > Apply to Spline
image
or it will be a flat ribbon…

*Note: my machine it too old i cannot use BL 3.0 *
*only last version of 2.9 *

add torus curve !

this seems to add a simple circle curve it is not a 3D torus ?

how do you make it 3D ?

thanks
happy bl

@ RSEhlers

tried you simplified method but could not get it to work
which BL are you using ?

when i try to add spiral curve torus
i get like a circle curve are there other parameters to get a spiral ?

can you upload a sample file

thanks for feedback

happy bl

@ nezumi.blend

got script to work in 2.9

now the torus you select is it a mesh torus or some curve torus ?

thanks
happy bl

for the new addon
when i try with mesh cube or mesh coil torus

i get an error

AttributeError: ‘BooleanModifier’ object has no attribute ‘solver’

do you know how to correct that or how to make it work ?

thanks
happy bl

In the POP-up window lower Left…select TURNS…adjust till you get the proper radis for the spiral curve to fit the original torus ( MESH TORUS).

Add a Shink-Wrap Modifier and adjust it to get a good fit…adjust the Geometry of the curve in the curve tab for Bevel > Round > Depth…TWEAK till all is good…

Apply the Shrinkwrap and convert the spiral curve to mesh if you want to Boolean > Union


Curve-Spiral 2.92file.blend (874.2 KB)

are you saying you need first to start with a mesh torus and a spiral around it

then use your addon ?

or add cube torus with a spiral shrinkwrap around ?

thanks
happy bl

YES add a mesh torus…(I don’t know what a Cube torus even is??) select a single vertex on an edge and cursor to selection…then add the Curve Spiral Torus…

if i do that i get this
probably wrong or my set up is not as yours
where should the curve should be ?

zq1

and you uploaded a new modified script in original thread
so i will re donwload it

thanks
happy bl

when you add spiral curve on tours mesh you place it at origin of torus

how many turns do you use for the torus curve or not important ?
then convert to mesh and use shrinkwrap around mesh torus!

thanks
happy bl

Yes Origins are the Same…as well as location on cursor
Turns are just the amount of loops the Curve Torus is going to wrap around…
The important things are the Inner Radius the Radius and the Number of steps…Match all of them to the original Mesh Torus ( I can’t even find something that produces the Box you showed…( Where are you adding that from? )

I also find that the ends of the curve torus will split apart… making it cyclic will join them but you end up with a bulge…so the best way is to Extrude one and snap to the other… then in the curve parameters use SMOOTHING and it will clean it up…

sorry for delay

can you show image with let say a square torus and the spiral cuve
just before you use your script

at least that would show me the initial set up

the square torus is just one example of a torus shape
there can several different shape - round square

thanks for feedback